llvm-project/clang/unittests/Format/SortImportsTestJS.cpp
Martin Probst b2780cd744 clang-format: [JS] handle "off" in imports
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
2021-04-30 14:18:52 +02:00

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