Index: include/clang/Format/Format.h =================================================================== --- include/clang/Format/Format.h +++ include/clang/Format/Format.h @@ -765,6 +765,14 @@ StringRef FileName, unsigned *Cursor = nullptr); +/// \brief Returns the replacements necessary to sort all ``import`` and +/// ``export`` blocks are affected by ``Ranges``. +tooling::Replacements sortJavaScriptIncludes(const FormatStyle &Style, + StringRef Code, + ArrayRef Ranges, + StringRef FileName, + unsigned *Cursor = nullptr); + /// \brief Returns the replacements corresponding to applying and formatting /// \p Replaces. tooling::Replacements formatReplacements(StringRef Code, Index: lib/Format/Format.cpp =================================================================== --- lib/Format/Format.cpp +++ lib/Format/Format.cpp @@ -1954,6 +1954,256 @@ std::set DeletedLines; }; +// An imported symbol in a JavaScript ES6 import/export, possibly aliased. +struct JsImportedSymbol { + StringRef Symbol; + StringRef Alias; +}; + +struct JsImportExport { + bool IsExport; + // JS imports are sorted into these categories, in order. + enum JsImportCategory { + ABSOLUTE, // from 'something' + RELATIVE_PARENT, // from '../*' + RELATIVE, // from './*' + }; + JsImportCategory Category; + // Empty for `export {a, b};`. + StringRef URL; + // Prefix from "import * as prefix". Empty for symbol imports. Implies an + // empty names list. + StringRef Prefix; + // Symbols from `import {SymbolA, SymbolB, ...} from ...;`. + SmallVector Symbols; + // Textual position of the import/export, including preceding and trailing + // comments. + SourceLocation Start; + SourceLocation End; +}; + +bool operator<(const JsImportExport &LHS, const JsImportExport &RHS) { + if (LHS.IsExport != RHS.IsExport) + return LHS.IsExport < RHS.IsExport; + if (LHS.Category != RHS.Category) + return LHS.Category < RHS.Category; + // NB: empty URLs sort *last* (for export {...};). + if (LHS.URL.empty() != RHS.URL.empty()) + return LHS.URL.empty() < RHS.URL.empty(); + if (LHS.URL != RHS.URL) + return LHS.URL < RHS.URL; + // NB: '*' imports (with prefix) sort before {a, b, ...} imports. + if (LHS.Prefix.empty() != RHS.Prefix.empty()) + return LHS.Prefix.empty() < RHS.Prefix.empty(); + if (LHS.Prefix != RHS.Prefix) + return LHS.Prefix > RHS.Prefix; + return false; +} + +// JavaScriptImportSorter sorts JavaScript ES6 imports and exports. It is +// implemented as a TokenAnalyzer because ES6 imports have substantial syntactic +// structure, making it messy to sort them using regular expressions. +class JavaScriptImportSorter : public TokenAnalyzer { +public: + JavaScriptImportSorter(const Environment &Env, const FormatStyle &Style) + : TokenAnalyzer(Env, Style), + FileContents(Env.getSourceManager().getBufferData(Env.getFileID())) {} + + tooling::Replacements + analyze(TokenAnnotator &Annotator, + SmallVectorImpl &AnnotatedLines, + FormatTokenLexer &Tokens, tooling::Replacements &Result) override { + AffectedRangeMgr.computeAffectedLines(AnnotatedLines.begin(), + AnnotatedLines.end()); + + const AdditionalKeywords &Keywords = Tokens.getKeywords(); + + SmallVector Imports; + SourceLocation LastStart; + for (auto Line : AnnotatedLines) { + if (!Line->Affected) + break; + Current = Line->First; + LineEnd = Line->Last; + JsImportExport ImpExp; + skipComments(); + if (LastStart.isInvalid() || Imports.empty()) { + // After the first file level comment, consider line comments to be part + // of the import that immediately follows them by using the previously + // set LastStart. + LastStart = Line->First->Tok.getLocation(); + } + if (!Current) + continue; // Only comments on this line. + ImpExp.Start = LastStart; + LastStart = SourceLocation(); + if (!parseImportExport(Keywords, ImpExp)) + break; + ImpExp.End = LineEnd->Tok.getEndLoc(); + DEBUG({ + llvm::dbgs() << "Import: {" + << "is_export: " << ImpExp.IsExport + << ", cat: " << ImpExp.Category + << ", url: " << ImpExp.URL + << ", prefix: " << ImpExp.Prefix; + for (size_t i = 0; i < ImpExp.Symbols.size(); ++i) + llvm::dbgs() << ", " << ImpExp.Symbols[i].Symbol << " as " + << ImpExp.Symbols[i].Alias; + llvm::dbgs() << ", text: " << getSourceText(ImpExp.Start, ImpExp.End); + llvm::dbgs() << "}\n"; + }); + Imports.push_back(ImpExp); + } + + if (Imports.empty()) + return Result; + + SmallVector Indices; + for (unsigned i = 0, e = Imports.size(); i != e; ++i) + Indices.push_back(i); + std::stable_sort( + Indices.begin(), Indices.end(), [&](unsigned LHSI, unsigned RHSI) { + return Imports[LHSI] < Imports[RHSI]; + }); + + bool OutOfOrder = false; + for (unsigned i = 0, e = Indices.size(); i != e; ++i) { + if (i != Indices[i]) { + OutOfOrder = true; + break; + } + } + if (!OutOfOrder) + return Result; + + // Replace all existing import/export statements. + std::string ImportsText; + for (unsigned i = 0, e = Indices.size(); i != e; ++i) { + JsImportExport ImpExp = Imports[Indices[i]]; + StringRef ImportStmt = getSourceText(ImpExp.Start, ImpExp.End); + ImportsText += ImportStmt; + ImportsText += "\n"; + // Separate groups outside of exports with two line breaks. + if (i + 1 < e && !ImpExp.IsExport && + ImpExp.Category != Imports[Indices[i + 1]].Category) + ImportsText += "\n"; + } + SourceLocation InsertionPoint = Imports[0].Start; + SourceLocation End = Imports[Imports.size() - 1].End; + DEBUG(llvm::dbgs() << "Replacing imports:\n" + << getSourceText(InsertionPoint, End) << "\nwith:\n" + << ImportsText); + Result.insert(tooling::Replacement( + Env.getSourceManager(), + CharSourceRange::getCharRange(InsertionPoint, End), ImportsText)); + + return Result; + } + +private: + FormatToken *Current; + FormatToken *LineEnd; + StringRef FileContents; + + void skipComments() { + Current = skipComments(Current); + } + + FormatToken *skipComments(FormatToken *Tok) { + while (Tok && Tok->is(tok::comment)) + Tok = Tok->Next; + return Tok; + } + + bool nextToken() { + Current = Current->Next; + skipComments(); + return Current && Current != LineEnd->Next; + } + + StringRef getSourceText(SourceLocation Start, SourceLocation End) { + const SourceManager &SM = Env.getSourceManager(); + return FileContents.substr(SM.getFileOffset(Start), + SM.getFileOffset(End) - SM.getFileOffset(Start)); + } + + bool parseImportExport(const AdditionalKeywords &Keywords, + JsImportExport &ImpExp) { + if (!Current || !Current->isOneOf(Keywords.kw_import, tok::kw_export)) + return false; + ImpExp.IsExport = Current->is(tok::kw_export); + nextToken(); + + if (!parseImportExportSpecifier(Keywords, ImpExp) || !nextToken()) + return false; + if (Current->is(Keywords.kw_from)) { + // imports have a 'from' clause, exports might not. + if (!nextToken()) + return false; + if (!Current->isStringLiteral()) + return false; + // URL = TokenText without the quotes. + ImpExp.URL = Current->TokenText.substr(1, Current->TokenText.size() - 2); + if (ImpExp.URL.startswith("..")) { + ImpExp.Category = JsImportExport::JsImportCategory::RELATIVE_PARENT; + } else if (ImpExp.URL.startswith(".")) { + ImpExp.Category = JsImportExport::JsImportCategory::RELATIVE; + } else { + ImpExp.Category = JsImportExport::JsImportCategory::ABSOLUTE; + } + } else { + // w/o URL groups with "empty". + ImpExp.Category = JsImportExport::JsImportCategory::RELATIVE; + } + return true; + } + + bool parseImportExportSpecifier(const AdditionalKeywords &Keywords, + JsImportExport &ImpExp) { + // * as prefix from '...'; + if (Current->is(tok::star)) { + if (!nextToken()) + return false; + if (!Current->is(Keywords.kw_as) || !nextToken()) + return false; + if (!Current->is(tok::identifier)) + return false; + ImpExp.Prefix = Current->TokenText; + return true; + } + + if (!Current->is(tok::l_brace)) + return false; + + // {sym as alias, sym2 as ...} from '...'; + if (!nextToken()) + return false; + while (true) { + if (!Current->is(tok::identifier)) + return false; + + JsImportedSymbol Symbol; + Symbol.Symbol = Current->TokenText; + nextToken(); + + if (Current->is(Keywords.kw_as)) { + nextToken(); + if (!Current->is(tok::identifier)) + return false; + Symbol.Alias = Current->TokenText; + nextToken(); + } + ImpExp.Symbols.push_back(Symbol); + + if (Current->is(tok::r_brace)) + return true; + if (!Current->is(tok::comma)) + return false; + nextToken(); + } + } +}; + struct IncludeDirective { StringRef Filename; StringRef Text; @@ -2038,6 +2288,8 @@ tooling::Replacements Replaces; if (!Style.SortIncludes) return Replaces; + if (Style.Language == FormatStyle::LanguageKind::LK_JavaScript) + return sortJavaScriptIncludes(Style, Code, Ranges, FileName); unsigned Prev = 0; unsigned SearchFrom = 0; @@ -2120,6 +2372,20 @@ return Replaces; } +// Sorts a block of includes given by 'Includes' alphabetically adding the +// necessary replacement to 'Replaces'. 'Includes' must be in strict source +// order. +tooling::Replacements sortJavaScriptIncludes(const FormatStyle &Style, + StringRef Code, + ArrayRef Ranges, + StringRef FileName, + unsigned *Cursor) { + std::unique_ptr Env = + Environment::CreateVirtualEnvironment(Code, FileName, Ranges); + JavaScriptImportSorter Sorter(*Env, Style); + return Sorter.process(); +} + template static tooling::Replacements processReplacements(T ProcessFunc, StringRef Code, Index: lib/Format/FormatToken.h =================================================================== --- lib/Format/FormatToken.h +++ lib/Format/FormatToken.h @@ -535,6 +535,7 @@ kw_NS_ENUM = &IdentTable.get("NS_ENUM"); kw_NS_OPTIONS = &IdentTable.get("NS_OPTIONS"); + kw_as = &IdentTable.get("as"); kw_async = &IdentTable.get("async"); kw_await = &IdentTable.get("await"); kw_finally = &IdentTable.get("finally"); @@ -585,6 +586,7 @@ IdentifierInfo *kw___except; // JavaScript keywords. + IdentifierInfo *kw_as; IdentifierInfo *kw_async; IdentifierInfo *kw_await; IdentifierInfo *kw_finally; Index: unittests/Format/CMakeLists.txt =================================================================== --- unittests/Format/CMakeLists.txt +++ unittests/Format/CMakeLists.txt @@ -9,6 +9,7 @@ FormatTestJS.cpp FormatTestProto.cpp FormatTestSelective.cpp + SortImportsTestJS.cpp SortIncludesTest.cpp ) Index: unittests/Format/SortImportsTestJS.cpp =================================================================== --- /dev/null +++ unittests/Format/SortImportsTestJS.cpp @@ -0,0 +1,130 @@ +//===- unittest/Format/SortImportsTestJS.cpp - JS import sort unit tests --===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +#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::vector GetCodeRange(StringRef Code) { + return std::vector(1, tooling::Range(0, Code.size())); + } + + std::string sort(StringRef Code, StringRef FileName = "input.js") { + auto Ranges = GetCodeRange(Code); + std::string Sorted = applyAllReplacements( + Code, sortJavaScriptIncludes(Style, Code, Ranges, FileName)); + return applyAllReplacements(Sorted, + reformat(Style, Sorted, Ranges, FileName)); + } + + void verifySort(llvm::StringRef Expected, llvm::StringRef Code) { + std::string Result = sort(Code); + EXPECT_EQ(Expected.str(), Result) << "Formatted:\n" << Result; + } + + unsigned newCursor(llvm::StringRef Code, unsigned Cursor) { + sortJavaScriptIncludes(Style, Code, GetCodeRange(Code), "input.js", + &Cursor); + return Cursor; + } + + FormatStyle Style = getGoogleStyle(FormatStyle::LK_JavaScript); +}; + +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, 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"); +} + +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, 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"); +} + +} // end namespace +} // end namespace format +} // end namespace clang