mprobst created this revision.
mprobst added a reviewer: djasper.
mprobst added a subscriber: cfe-commits.
Herald added a subscriber: klimek.

This change automatically sorts ES6 imports and exports into four groups:
absolute imports, parent imports, relative imports, and then exports. Exports
are sorted in the same order, but not grouped further.

http://reviews.llvm.org/D20198

Files:
  include/clang/Format/Format.h
  lib/Format/Format.cpp
  lib/Format/FormatToken.h
  unittests/Format/CMakeLists.txt
  unittests/Format/SortImportsTestJS.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<tooling::Range> GetCodeRange(StringRef Code) {
+    return std::vector<tooling::Range>(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
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: 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: lib/Format/Format.cpp
===================================================================
--- lib/Format/Format.cpp
+++ lib/Format/Format.cpp
@@ -1954,6 +1954,256 @@
   std::set<unsigned> 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<JsImportedSymbol, 1> 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<AnnotatedLine *> &AnnotatedLines,
+          FormatTokenLexer &Tokens, tooling::Replacements &Result) override {
+    AffectedRangeMgr.computeAffectedLines(AnnotatedLines.begin(),
+                                          AnnotatedLines.end());
+
+    const AdditionalKeywords &Keywords = Tokens.getKeywords();
+
+    SmallVector<JsImportExport, 16> 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<unsigned, 16> 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<tooling::Range> Ranges,
+                                             StringRef FileName,
+                                             unsigned *Cursor) {
+  std::unique_ptr<Environment> Env =
+      Environment::CreateVirtualEnvironment(Code, FileName, Ranges);
+  JavaScriptImportSorter Sorter(*Env, Style);
+  return Sorter.process();
+}
+
 template <typename T>
 static tooling::Replacements
 processReplacements(T ProcessFunc, StringRef Code,
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<tooling::Range> Ranges,
+                                             StringRef FileName,
+                                             unsigned *Cursor = nullptr);
+
 /// \brief Returns the replacements corresponding to applying and formatting
 /// \p Replaces.
 tooling::Replacements formatReplacements(StringRef Code,
_______________________________________________
cfe-commits mailing list
cfe-commits@lists.llvm.org
http://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to