
Previously, the JavaScript import sorter would ignore `// clang-format off` and `on` comments. This change fixes that. It tracks whether formatting is enabled for a stretch of imports, and then only sorts and merges the imports where formatting is enabled, in individual chunks. This means that there's no meaningful total order when module references are mixed with blocks that have formatting disabled. The alternative approach would have been to sort all imports that have formatting enabled in one group. However that raises the question where to insert the formatting-off block, which can also impact symbol visibility (in particular for exports). In practice, sorting in chunks probably isn't a big problem. This change also simplifies the general algorithm: instead of tracking indices separately and sorting them, it just sorts the vector of module references. And instead of attempting to do fine grained tracking of whether the code changed order, it just prints out the module references text, and compares that to the previous text. Given that source files typically have dozens, but not even hundreds of imports, the performance impact seems negligible. Differential Revision: https://reviews.llvm.org/D101515
437 lines
14 KiB
C++
437 lines
14 KiB
C++
//===- unittest/Format/SortImportsTestJS.cpp - JS import sort unit tests --===//
|
|
//
|
|
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
|
// See https://llvm.org/LICENSE.txt for license information.
|
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
#include "FormatTestUtils.h"
|
|
#include "clang/Format/Format.h"
|
|
#include "llvm/Support/Debug.h"
|
|
#include "gtest/gtest.h"
|
|
|
|
#define DEBUG_TYPE "format-test"
|
|
|
|
namespace clang {
|
|
namespace format {
|
|
namespace {
|
|
|
|
class SortImportsTestJS : public ::testing::Test {
|
|
protected:
|
|
std::string sort(StringRef Code, unsigned Offset = 0, unsigned Length = 0) {
|
|
StringRef FileName = "input.js";
|
|
if (Length == 0U)
|
|
Length = Code.size() - Offset;
|
|
std::vector<tooling::Range> Ranges(1, tooling::Range(Offset, Length));
|
|
auto Sorted =
|
|
applyAllReplacements(Code, sortIncludes(Style, Code, Ranges, FileName));
|
|
EXPECT_TRUE(static_cast<bool>(Sorted));
|
|
auto Formatted = applyAllReplacements(
|
|
*Sorted, reformat(Style, *Sorted, Ranges, FileName));
|
|
EXPECT_TRUE(static_cast<bool>(Formatted));
|
|
return *Formatted;
|
|
}
|
|
|
|
void verifySort(llvm::StringRef Expected, llvm::StringRef Code,
|
|
unsigned Offset = 0, unsigned Length = 0) {
|
|
std::string Result = sort(Code, Offset, Length);
|
|
EXPECT_EQ(Expected.str(), Result) << "Expected:\n"
|
|
<< Expected << "\nActual:\n"
|
|
<< Result;
|
|
}
|
|
|
|
FormatStyle Style = getGoogleStyle(FormatStyle::LK_JavaScript);
|
|
};
|
|
|
|
TEST_F(SortImportsTestJS, AlreadySorted) {
|
|
verifySort("import {sym} from 'a';\n"
|
|
"import {sym} from 'b';\n"
|
|
"import {sym} from 'c';\n"
|
|
"\n"
|
|
"let x = 1;",
|
|
"import {sym} from 'a';\n"
|
|
"import {sym} from 'b';\n"
|
|
"import {sym} from 'c';\n"
|
|
"\n"
|
|
"let x = 1;");
|
|
}
|
|
|
|
TEST_F(SortImportsTestJS, BasicSorting) {
|
|
verifySort("import {sym} from 'a';\n"
|
|
"import {sym} from 'b';\n"
|
|
"import {sym} from 'c';\n"
|
|
"\n"
|
|
"let x = 1;",
|
|
"import {sym} from 'a';\n"
|
|
"import {sym} from 'c';\n"
|
|
"import {sym} from 'b';\n"
|
|
"let x = 1;");
|
|
}
|
|
|
|
TEST_F(SortImportsTestJS, DefaultBinding) {
|
|
verifySort("import A from 'a';\n"
|
|
"import B from 'b';\n"
|
|
"\n"
|
|
"let x = 1;",
|
|
"import B from 'b';\n"
|
|
"import A from 'a';\n"
|
|
"let x = 1;");
|
|
}
|
|
|
|
TEST_F(SortImportsTestJS, DefaultAndNamedBinding) {
|
|
verifySort("import A, {a} from 'a';\n"
|
|
"import B, {b} from 'b';\n"
|
|
"\n"
|
|
"let x = 1;",
|
|
"import B, {b} from 'b';\n"
|
|
"import A, {a} from 'a';\n"
|
|
"let x = 1;");
|
|
}
|
|
|
|
TEST_F(SortImportsTestJS, WrappedImportStatements) {
|
|
verifySort("import {sym1, sym2} from 'a';\n"
|
|
"import {sym} from 'b';\n"
|
|
"\n"
|
|
"1;",
|
|
"import\n"
|
|
" {sym}\n"
|
|
" from 'b';\n"
|
|
"import {\n"
|
|
" sym1,\n"
|
|
" sym2\n"
|
|
"} from 'a';\n"
|
|
"1;");
|
|
}
|
|
|
|
TEST_F(SortImportsTestJS, SeparateMainCodeBody) {
|
|
verifySort("import {sym} from 'a';"
|
|
"\n"
|
|
"let x = 1;\n",
|
|
"import {sym} from 'a'; let x = 1;\n");
|
|
}
|
|
|
|
TEST_F(SortImportsTestJS, Comments) {
|
|
verifySort("/** @fileoverview This is a great file. */\n"
|
|
"// A very important import follows.\n"
|
|
"import {sym} from 'a'; /* more comments */\n"
|
|
"import {sym} from 'b'; // from //foo:bar\n",
|
|
"/** @fileoverview This is a great file. */\n"
|
|
"import {sym} from 'b'; // from //foo:bar\n"
|
|
"// A very important import follows.\n"
|
|
"import {sym} from 'a'; /* more comments */\n");
|
|
verifySort("import {sym} from 'a';\n"
|
|
"import {sym} from 'b';\n"
|
|
"\n"
|
|
"/** Comment on variable. */\n"
|
|
"const x = 1;\n",
|
|
"import {sym} from 'b';\n"
|
|
"import {sym} from 'a';\n"
|
|
"\n"
|
|
"/** Comment on variable. */\n"
|
|
"const x = 1;\n");
|
|
}
|
|
|
|
TEST_F(SortImportsTestJS, SortStar) {
|
|
verifySort("import * as foo from 'a';\n"
|
|
"import {sym} from 'a';\n"
|
|
"import * as bar from 'b';\n",
|
|
"import {sym} from 'a';\n"
|
|
"import * as foo from 'a';\n"
|
|
"import * as bar from 'b';\n");
|
|
}
|
|
|
|
TEST_F(SortImportsTestJS, AliasesSymbols) {
|
|
verifySort("import {sym1 as alias1} from 'b';\n"
|
|
"import {sym2 as alias2, sym3 as alias3} from 'c';\n",
|
|
"import {sym2 as alias2, sym3 as alias3} from 'c';\n"
|
|
"import {sym1 as alias1} from 'b';\n");
|
|
}
|
|
|
|
TEST_F(SortImportsTestJS, SortSymbols) {
|
|
verifySort("import {sym1, sym2 as a, sym3} from 'b';\n",
|
|
"import {sym2 as a, sym1, sym3} from 'b';\n");
|
|
verifySort("import {sym1 /* important! */, /*!*/ sym2 as a} from 'b';\n",
|
|
"import {/*!*/ sym2 as a, sym1 /* important! */} from 'b';\n");
|
|
verifySort("import {sym1, sym2} from 'b';\n", "import {\n"
|
|
" sym2 \n"
|
|
",\n"
|
|
" sym1 \n"
|
|
"} from 'b';\n");
|
|
}
|
|
|
|
TEST_F(SortImportsTestJS, GroupImports) {
|
|
verifySort("import {a} from 'absolute';\n"
|
|
"\n"
|
|
"import {b} from '../parent';\n"
|
|
"import {b} from '../parent/nested';\n"
|
|
"\n"
|
|
"import {b} from './relative/path';\n"
|
|
"import {b} from './relative/path/nested';\n"
|
|
"\n"
|
|
"let x = 1;\n",
|
|
"import {b} from './relative/path/nested';\n"
|
|
"import {b} from './relative/path';\n"
|
|
"import {b} from '../parent/nested';\n"
|
|
"import {b} from '../parent';\n"
|
|
"import {a} from 'absolute';\n"
|
|
"let x = 1;\n");
|
|
}
|
|
|
|
TEST_F(SortImportsTestJS, Exports) {
|
|
verifySort("import {S} from 'bpath';\n"
|
|
"\n"
|
|
"import {T} from './cpath';\n"
|
|
"\n"
|
|
"export {A, B} from 'apath';\n"
|
|
"export {P} from '../parent';\n"
|
|
"export {R} from './relative';\n"
|
|
"export {S};\n"
|
|
"\n"
|
|
"let x = 1;\n"
|
|
"export y = 1;\n",
|
|
"export {R} from './relative';\n"
|
|
"import {T} from './cpath';\n"
|
|
"export {S};\n"
|
|
"export {A, B} from 'apath';\n"
|
|
"import {S} from 'bpath';\n"
|
|
"export {P} from '../parent';\n"
|
|
"let x = 1;\n"
|
|
"export y = 1;\n");
|
|
verifySort("import {S} from 'bpath';\n"
|
|
"\n"
|
|
"export {T} from 'epath';\n",
|
|
"export {T} from 'epath';\n"
|
|
"import {S} from 'bpath';\n");
|
|
}
|
|
|
|
TEST_F(SortImportsTestJS, SideEffectImports) {
|
|
verifySort("import 'ZZside-effect';\n"
|
|
"import 'AAside-effect';\n"
|
|
"\n"
|
|
"import {A} from 'absolute';\n"
|
|
"\n"
|
|
"import {R} from './relative';\n",
|
|
"import {R} from './relative';\n"
|
|
"import 'ZZside-effect';\n"
|
|
"import {A} from 'absolute';\n"
|
|
"import 'AAside-effect';\n");
|
|
}
|
|
|
|
TEST_F(SortImportsTestJS, AffectedRange) {
|
|
// Affected range inside of import statements.
|
|
verifySort("import {sym} from 'a';\n"
|
|
"import {sym} from 'b';\n"
|
|
"import {sym} from 'c';\n"
|
|
"\n"
|
|
"let x = 1;",
|
|
"import {sym} from 'c';\n"
|
|
"import {sym} from 'b';\n"
|
|
"import {sym} from 'a';\n"
|
|
"let x = 1;",
|
|
0, 30);
|
|
// Affected range outside of import statements.
|
|
verifySort("import {sym} from 'c';\n"
|
|
"import {sym} from 'b';\n"
|
|
"import {sym} from 'a';\n"
|
|
"\n"
|
|
"let x = 1;",
|
|
"import {sym} from 'c';\n"
|
|
"import {sym} from 'b';\n"
|
|
"import {sym} from 'a';\n"
|
|
"\n"
|
|
"let x = 1;",
|
|
70, 1);
|
|
}
|
|
|
|
TEST_F(SortImportsTestJS, SortingCanShrink) {
|
|
// Sort excluding a suffix.
|
|
verifySort("import {B} from 'a';\n"
|
|
"import {A} from 'b';\n"
|
|
"\n"
|
|
"1;",
|
|
"import {A} from 'b';\n"
|
|
"\n"
|
|
"import {B} from 'a';\n"
|
|
"\n"
|
|
"1;");
|
|
}
|
|
|
|
TEST_F(SortImportsTestJS, TrailingComma) {
|
|
verifySort("import {A, B,} from 'aa';\n", "import {B, A,} from 'aa';\n");
|
|
}
|
|
|
|
TEST_F(SortImportsTestJS, SortCaseInsensitive) {
|
|
verifySort("import {A} from 'aa';\n"
|
|
"import {A} from 'Ab';\n"
|
|
"import {A} from 'b';\n"
|
|
"import {A} from 'Bc';\n"
|
|
"\n"
|
|
"1;",
|
|
"import {A} from 'b';\n"
|
|
"import {A} from 'Bc';\n"
|
|
"import {A} from 'Ab';\n"
|
|
"import {A} from 'aa';\n"
|
|
"\n"
|
|
"1;");
|
|
verifySort("import {aa, Ab, b, Bc} from 'x';\n"
|
|
"\n"
|
|
"1;",
|
|
"import {b, Bc, Ab, aa} from 'x';\n"
|
|
"\n"
|
|
"1;");
|
|
}
|
|
|
|
TEST_F(SortImportsTestJS, SortMultiLine) {
|
|
// Reproduces issue where multi-line import was not parsed correctly.
|
|
verifySort("import {A} from 'a';\n"
|
|
"import {A} from 'b';\n"
|
|
"\n"
|
|
"1;",
|
|
"import\n"
|
|
"{\n"
|
|
"A\n"
|
|
"}\n"
|
|
"from\n"
|
|
"'b';\n"
|
|
"import {A} from 'a';\n"
|
|
"\n"
|
|
"1;");
|
|
}
|
|
|
|
TEST_F(SortImportsTestJS, SortDefaultImports) {
|
|
// Reproduces issue where multi-line import was not parsed correctly.
|
|
verifySort("import {A} from 'a';\n"
|
|
"import {default as B} from 'b';\n",
|
|
"import {default as B} from 'b';\n"
|
|
"import {A} from 'a';\n");
|
|
}
|
|
|
|
TEST_F(SortImportsTestJS, MergeImports) {
|
|
// basic operation
|
|
verifySort("import {X, Y} from 'a';\n"
|
|
"import {Z} from 'z';\n"
|
|
"\n"
|
|
"X + Y + Z;\n",
|
|
"import {X} from 'a';\n"
|
|
"import {Z} from 'z';\n"
|
|
"import {Y} from 'a';\n"
|
|
"\n"
|
|
"X + Y + Z;\n");
|
|
|
|
// merge only, no resorting.
|
|
verifySort("import {A, B} from 'foo';\n", "import {A} from 'foo';\n"
|
|
"import {B} from 'foo';");
|
|
|
|
// empty imports
|
|
verifySort("import {A} from 'foo';\n", "import {} from 'foo';\n"
|
|
"import {A} from 'foo';");
|
|
|
|
// ignores import *
|
|
verifySort("import * as foo from 'foo';\n"
|
|
"import {A} from 'foo';\n",
|
|
"import * as foo from 'foo';\n"
|
|
"import {A} from 'foo';\n");
|
|
|
|
// ignores default import
|
|
verifySort("import X from 'foo';\n"
|
|
"import {A} from 'foo';\n",
|
|
"import X from 'foo';\n"
|
|
"import {A} from 'foo';\n");
|
|
|
|
// keeps comments
|
|
// known issue: loses the 'also a' comment.
|
|
verifySort("// a\n"
|
|
"import {/* x */ X, /* y */ Y} from 'a';\n"
|
|
"// z\n"
|
|
"import {Z} from 'z';\n"
|
|
"\n"
|
|
"X + Y + Z;\n",
|
|
"// a\n"
|
|
"import {/* y */ Y} from 'a';\n"
|
|
"// z\n"
|
|
"import {Z} from 'z';\n"
|
|
"// also a\n"
|
|
"import {/* x */ X} from 'a';\n"
|
|
"\n"
|
|
"X + Y + Z;\n");
|
|
|
|
// do not merge imports and exports
|
|
verifySort("import {A} from 'foo';\n"
|
|
"\n"
|
|
"export {B} from 'foo';\n",
|
|
"import {A} from 'foo';\n"
|
|
"export {B} from 'foo';");
|
|
// do merge exports
|
|
verifySort("export {A, B} from 'foo';\n", "export {A} from 'foo';\n"
|
|
"export {B} from 'foo';");
|
|
|
|
// do not merge side effect imports with named ones
|
|
verifySort("import './a';\n"
|
|
"\n"
|
|
"import {bar} from './a';\n",
|
|
"import {bar} from './a';\n"
|
|
"import './a';\n");
|
|
}
|
|
|
|
TEST_F(SortImportsTestJS, RespectsClangFormatOff) {
|
|
verifySort("// clang-format off\n"
|
|
"import {B} from './b';\n"
|
|
"import {A} from './a';\n"
|
|
"// clang-format on\n",
|
|
"// clang-format off\n"
|
|
"import {B} from './b';\n"
|
|
"import {A} from './a';\n"
|
|
"// clang-format on\n");
|
|
|
|
verifySort("import {A} from './sorted1_a';\n"
|
|
"import {B} from './sorted1_b';\n"
|
|
"// clang-format off\n"
|
|
"import {B} from './unsorted_b';\n"
|
|
"import {A} from './unsorted_a';\n"
|
|
"// clang-format on\n"
|
|
"import {A} from './sorted2_a';\n"
|
|
"import {B} from './sorted2_b';\n",
|
|
"import {B} from './sorted1_b';\n"
|
|
"import {A} from './sorted1_a';\n"
|
|
"// clang-format off\n"
|
|
"import {B} from './unsorted_b';\n"
|
|
"import {A} from './unsorted_a';\n"
|
|
"// clang-format on\n"
|
|
"import {B} from './sorted2_b';\n"
|
|
"import {A} from './sorted2_a';\n");
|
|
|
|
// Boundary cases
|
|
verifySort("// clang-format on\n", "// clang-format on\n");
|
|
verifySort("// clang-format off\n", "// clang-format off\n");
|
|
verifySort("// clang-format on\n"
|
|
"// clang-format off\n",
|
|
"// clang-format on\n"
|
|
"// clang-format off\n");
|
|
verifySort("// clang-format off\n"
|
|
"// clang-format on\n"
|
|
"import {A} from './a';\n"
|
|
"import {B} from './b';\n",
|
|
"// clang-format off\n"
|
|
"// clang-format on\n"
|
|
"import {B} from './b';\n"
|
|
"import {A} from './a';\n");
|
|
// section ends with comment
|
|
verifySort("// clang-format on\n"
|
|
"import {A} from './a';\n"
|
|
"import {B} from './b';\n"
|
|
"import {C} from './c';\n"
|
|
"\n" // inserted empty line is working as intended: splits imports
|
|
// section from main code body
|
|
"// clang-format off\n",
|
|
"// clang-format on\n"
|
|
"import {C} from './c';\n"
|
|
"import {B} from './b';\n"
|
|
"import {A} from './a';\n"
|
|
"// clang-format off\n");
|
|
}
|
|
|
|
} // end namespace
|
|
} // end namespace format
|
|
} // end namespace clang
|