Sort import() lines by group

Test: Test cases 071.gn 072.gn 073.gn added

Change-Id: I70417520e4534f65bd827e8cfe56d97b3d9f68ca
Reviewed-on: https://gn-review.googlesource.com/c/2980
Reviewed-by: Brett Wilson <brettw@google.com>
Commit-Queue: Scott Graham <scottmg@chromium.org>
diff --git a/tools/gn/command_format.cc b/tools/gn/command_format.cc
index 3b7a54e..d656dd8 100644
--- a/tools/gn/command_format.cc
+++ b/tools/gn/command_format.cc
@@ -111,7 +111,6 @@
   // Format a list of values using the given style.
   enum SequenceStyle {
     kSequenceStyleList,
-    kSequenceStyleBlock,
     kSequenceStyleBracedBlock,
   };
 
@@ -148,6 +147,11 @@
   // (both sorted alphabetically).
   void SortIfSourcesOrDeps(const BinaryOpNode* binop);
 
+  // Sort contiguous import() function calls in the given ordered list of
+  // statements (the body of a block or scope).
+  template <class PARSENODE>
+  void SortImports(std::vector<std::unique_ptr<PARSENODE>>& statements);
+
   // Heuristics to decide if there should be a blank line added between two
   // items. For various "small" items, it doesn't look nice if there's too much
   // vertical whitespace added.
@@ -343,6 +347,90 @@
   }
 }
 
+template <class PARSENODE>
+void Printer::SortImports(std::vector<std::unique_ptr<PARSENODE>>& statements) {
+  // Build a set of ranges by indices of FunctionCallNode's that are imports.
+
+  std::vector<std::vector<size_t>> import_statements;
+
+  auto is_import = [](const PARSENODE* p) {
+    const FunctionCallNode* func_call = p->AsFunctionCall();
+    return func_call && func_call->function().value() == "import";
+  };
+
+  std::vector<size_t> current_group;
+  for (size_t i = 0; i < statements.size(); ++i) {
+    if (is_import(statements[i].get())) {
+      if (i > 0 && (!is_import(statements[i - 1].get()) ||
+                    ShouldAddBlankLineInBetween(statements[i - 1].get(),
+                                                statements[i].get()))) {
+        if (!current_group.empty()) {
+          import_statements.push_back(current_group);
+          current_group.clear();
+        }
+      }
+      current_group.push_back(i);
+    }
+  }
+
+  if (!current_group.empty())
+    import_statements.push_back(current_group);
+
+  struct CompareByImportFile {
+    bool operator()(const std::unique_ptr<PARSENODE>& a,
+                    const std::unique_ptr<PARSENODE>& b) const {
+      const auto& a_args = a->AsFunctionCall()->args()->contents();
+      const auto& b_args = b->AsFunctionCall()->args()->contents();
+      base::StringPiece a_name;
+      base::StringPiece b_name;
+      if (!a_args.empty())
+        a_name = a_args[0]->AsLiteral()->value().value();
+      if (!b_args.empty())
+        b_name = b_args[0]->AsLiteral()->value().value();
+
+      auto is_absolute = [](base::StringPiece import) {
+        return import.size() >= 3 && import[0] == '"' && import[1] == '/' &&
+               import[2] == '/';
+      };
+      int a_is_rel = !is_absolute(a_name);
+      int b_is_rel = !is_absolute(b_name);
+
+      return std::tie(a_is_rel, a_name) < std::tie(b_is_rel, b_name);
+    }
+  };
+
+  int line_after_previous = -1;
+
+  for (const auto& group : import_statements) {
+    size_t begin = group[0];
+    size_t end = group.back() + 1;
+
+    // Save the original line number so that ranges can be re-assigned. They're
+    // contiguous because of the partitioning code above. Later formatting
+    // relies on correct line number to know whether to insert blank lines,
+    // which is why these need to be fixed up. Additionally, to handle multiple
+    // imports on one line, they're assigned sequential line numbers, and
+    // subsequent blocks will be gapped from them.
+    int start_line =
+        std::max(statements[begin]->GetRange().begin().line_number(),
+                 line_after_previous + 1);
+
+    std::sort(statements.begin() + begin, statements.begin() + end,
+              CompareByImportFile());
+
+    const PARSENODE* prev = nullptr;
+    for (size_t i = begin; i < end; ++i) {
+      const PARSENODE* node = statements[i].get();
+      int line_number =
+          prev ? prev->GetRange().end().line_number() + 1 : start_line;
+      const_cast<FunctionCallNode*>(node->AsFunctionCall())
+          ->SetNewLocation(line_number);
+      prev = node;
+      line_after_previous = line_number + 1;
+    }
+  }
+}
+
 bool Printer::ShouldAddBlankLineInBetween(const ParseNode* a,
                                           const ParseNode* b) {
   LocationRange a_range = a->GetRange();
@@ -382,6 +470,9 @@
     }
   }
 
+  SortImports(const_cast<std::vector<std::unique_ptr<ParseNode>>&>(
+      block->statements()));
+
   size_t i = 0;
   for (const auto& stmt : block->statements()) {
     Expr(stmt.get(), kPrecedenceLowest, std::string());
@@ -692,8 +783,10 @@
   else if (style == kSequenceStyleBracedBlock)
     Print("{");
 
-  if (style == kSequenceStyleBlock || style == kSequenceStyleBracedBlock)
+  if (style == kSequenceStyleBracedBlock) {
     force_multiline = true;
+    SortImports(const_cast<std::vector<std::unique_ptr<PARSENODE>>&>(list));
+  }
 
   force_multiline |= ListWillBeMultiline(list, end);
 
diff --git a/tools/gn/command_format_unittest.cc b/tools/gn/command_format_unittest.cc
index cd69e5d..db63a8c 100644
--- a/tools/gn/command_format_unittest.cc
+++ b/tools/gn/command_format_unittest.cc
@@ -106,3 +106,6 @@
 FORMAT_TEST(068)
 FORMAT_TEST(069)
 FORMAT_TEST(070)
+FORMAT_TEST(071)
+FORMAT_TEST(072)
+FORMAT_TEST(073)
diff --git a/tools/gn/format_test_data/028.golden b/tools/gn/format_test_data/028.golden
index a1d54c5..9506896 100644
--- a/tools/gn/format_test_data/028.golden
+++ b/tools/gn/format_test_data/028.golden
@@ -1,6 +1,6 @@
 # Don't separate these.
-import("wee.gni")
 import("waa.gni")
+import("wee.gni")
 
 import("woo.gni")
 
diff --git a/tools/gn/format_test_data/071.gn b/tools/gn/format_test_data/071.gn
new file mode 100644
index 0000000..665d099
--- /dev/null
+++ b/tools/gn/format_test_data/071.gn
@@ -0,0 +1,26 @@
+# Copyright 2018 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("z.gni")
+import("x.gni")
+import("y.gni")
+
+import("b.gni")
+import("a.gni")
+
+
+
+import("m.gni")
+import("d1.gni")
+# Comment here
+import("c1.gni")
+
+import("../something/relative.gni")
+import("//build/stuff.gni")
+import("nopath.gni")
+import("//abc/things.gni")
+
+import("")
+import()
+import("a")
diff --git a/tools/gn/format_test_data/071.golden b/tools/gn/format_test_data/071.golden
new file mode 100644
index 0000000..6d8f98f
--- /dev/null
+++ b/tools/gn/format_test_data/071.golden
@@ -0,0 +1,25 @@
+# Copyright 2018 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("x.gni")
+import("y.gni")
+import("z.gni")
+
+import("a.gni")
+import("b.gni")
+
+import("d1.gni")
+import("m.gni")
+
+# Comment here
+import("c1.gni")
+
+import("//abc/things.gni")
+import("//build/stuff.gni")
+import("../something/relative.gni")
+import("nopath.gni")
+
+import()
+import("")
+import("a")
diff --git a/tools/gn/format_test_data/072.gn b/tools/gn/format_test_data/072.gn
new file mode 100644
index 0000000..10b5a81
--- /dev/null
+++ b/tools/gn/format_test_data/072.gn
@@ -0,0 +1,8 @@
+import("b") import("c") import("a") import("d")
+
+import("z") declare_args() {} import("y") import("x") import("w")
+
+import("3") import("2") import("1")
+
+import("x") import("y")
+import("z") import("w")
diff --git a/tools/gn/format_test_data/072.golden b/tools/gn/format_test_data/072.golden
new file mode 100644
index 0000000..b4e5a4b
--- /dev/null
+++ b/tools/gn/format_test_data/072.golden
@@ -0,0 +1,21 @@
+import("a")
+import("b")
+import("c")
+import("d")
+
+import("z")
+declare_args() {
+}
+
+import("w")
+import("x")
+import("y")
+
+import("1")
+import("2")
+import("3")
+
+import("w")
+import("x")
+import("y")
+import("z")
diff --git a/tools/gn/format_test_data/073.gn b/tools/gn/format_test_data/073.gn
new file mode 100644
index 0000000..e3a6561
--- /dev/null
+++ b/tools/gn/format_test_data/073.gn
@@ -0,0 +1,23 @@
+import("//root/a")
+import("//root/c")
+import("//root/b")
+
+if (stuff) {
+import("3") import("2") import("1")
+  if (things) {
+      import("x")
+      import("z") import("y") import("w") import("q")
+
+      import("f") import("e") if (other) {import("z") import("a")} import("d")
+
+      template("wee") {
+        import("6")
+        import("7")
+        import("5")
+      }
+  }
+} else {
+  import("i")
+  import("h")
+  import("g")
+}
diff --git a/tools/gn/format_test_data/073.golden b/tools/gn/format_test_data/073.golden
new file mode 100644
index 0000000..e32977b
--- /dev/null
+++ b/tools/gn/format_test_data/073.golden
@@ -0,0 +1,34 @@
+import("//root/a")
+import("//root/b")
+import("//root/c")
+
+if (stuff) {
+  import("1")
+  import("2")
+  import("3")
+  if (things) {
+    import("q")
+    import("w")
+    import("x")
+    import("y")
+    import("z")
+
+    import("e")
+    import("f")
+    if (other) {
+      import("a")
+      import("z")
+    }
+
+    import("d")
+    template("wee") {
+      import("5")
+      import("6")
+      import("7")
+    }
+  }
+} else {
+  import("g")
+  import("h")
+  import("i")
+}
diff --git a/tools/gn/parse_tree.cc b/tools/gn/parse_tree.cc
index e2201d4..6dde01d 100644
--- a/tools/gn/parse_tree.cc
+++ b/tools/gn/parse_tree.cc
@@ -485,6 +485,21 @@
     block_->Print(out, indent + 1);
 }
 
+void FunctionCallNode::SetNewLocation(int line_number) {
+  Location func_old_loc = function_.location();
+  Location func_new_loc =
+      Location(func_old_loc.file(), line_number, func_old_loc.column_number(),
+               func_old_loc.byte());
+  function_.set_location(func_new_loc);
+
+  Location args_old_loc = args_->Begin().location();
+  Location args_new_loc =
+      Location(args_old_loc.file(), line_number, args_old_loc.column_number(),
+               args_old_loc.byte());
+  const_cast<Token&>(args_->Begin()).set_location(args_new_loc);
+  const_cast<Token&>(args_->End()->value()).set_location(args_new_loc);
+}
+
 // IdentifierNode --------------------------------------------------------------
 
 IdentifierNode::IdentifierNode() = default;
diff --git a/tools/gn/parse_tree.h b/tools/gn/parse_tree.h
index 415041e..9927307 100644
--- a/tools/gn/parse_tree.h
+++ b/tools/gn/parse_tree.h
@@ -334,6 +334,8 @@
   const BlockNode* block() const { return block_.get(); }
   void set_block(std::unique_ptr<BlockNode> b) { block_ = std::move(b); }
 
+  void SetNewLocation(int line_number);
+
  private:
   Token function_;
   std::unique_ptr<ListNode> args_;
@@ -385,6 +387,7 @@
   void Print(std::ostream& out, int indent) const override;
 
   void set_begin_token(const Token& t) { begin_token_ = t; }
+  const Token& Begin() const { return begin_token_; }
   void set_end(std::unique_ptr<EndNode> e) { end_ = std::move(e); }
   const EndNode* End() const { return end_.get(); }