Implement `gn analyze`.

This CL implements `analyze` natively in GN, fixing some
long-standing issues that the implementation in MB had
due to its not having access to the full build graph.

In particular, `analyze` will now work correctly when given
"all" as a compile target, and the list of compile targets
will now be pruned/filtered correctly.

Note that the input to analyze is different from the input that
MB expects, in that GN expects lists of GN labels and
source-absolute paths to files, rather than lists of ninja
targets and relative paths. MB will be responsible for doing
the conversion until we have updated the build recipes to pass
the format that GN expects.

R=brettw@chromium.org
BUG=555273

Review-Url: https://codereview.chromium.org/2265833002
Cr-Original-Commit-Position: refs/heads/master@{#415050}
Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src
Cr-Mirrored-Commit: 6d8fdaabe1a8faed9a5573047a2cebdd8456a996
diff --git a/tools/gn/BUILD.gn b/tools/gn/BUILD.gn
index 81678fa..1461a79 100644
--- a/tools/gn/BUILD.gn
+++ b/tools/gn/BUILD.gn
@@ -15,6 +15,8 @@
     "action_target_generator.h",
     "action_values.cc",
     "action_values.h",
+    "analyzer.cc",
+    "analyzer.h",
     "args.cc",
     "args.h",
     "binary_target_generator.cc",
@@ -33,6 +35,7 @@
     "bundle_file_rule.h",
     "c_include_iterator.cc",
     "c_include_iterator.h",
+    "command_analyze.cc",
     "command_args.cc",
     "command_check.cc",
     "command_clean.cc",
@@ -274,6 +277,7 @@
 test("gn_unittests") {
   sources = [
     "action_target_generator_unittest.cc",
+    "analyzer_unittest.cc",
     "args_unittest.cc",
     "builder_unittest.cc",
     "c_include_iterator_unittest.cc",
diff --git a/tools/gn/analyzer.cc b/tools/gn/analyzer.cc
new file mode 100644
index 0000000..38acb6b
--- /dev/null
+++ b/tools/gn/analyzer.cc
@@ -0,0 +1,405 @@
+// Copyright 2016 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.
+
+#include "tools/gn/analyzer.h"
+
+#include <algorithm>
+#include <iterator>
+#include <set>
+#include <vector>
+
+#include "base/json/json_reader.h"
+#include "base/json/json_writer.h"
+#include "base/memory/ptr_util.h"
+#include "base/strings/string_util.h"
+#include "base/strings/string_util.h"
+#include "base/values.h"
+#include "tools/gn/builder.h"
+#include "tools/gn/deps_iterator.h"
+#include "tools/gn/err.h"
+#include "tools/gn/filesystem_utils.h"
+#include "tools/gn/loader.h"
+#include "tools/gn/location.h"
+#include "tools/gn/source_file.h"
+#include "tools/gn/target.h"
+
+using LabelSet = Analyzer::LabelSet;
+using SourceFileSet = Analyzer::SourceFileSet;
+using TargetSet = Analyzer::TargetSet;
+
+namespace {
+
+struct Inputs {
+  std::vector<SourceFile> source_vec;
+  std::vector<Label> compile_vec;
+  std::vector<Label> test_vec;
+  bool compile_included_all;
+  SourceFileSet source_files;
+  LabelSet compile_labels;
+  LabelSet test_labels;
+};
+
+struct Outputs {
+  std::string status;
+  std::string error;
+  LabelSet compile_labels;
+  LabelSet test_labels;
+  LabelSet invalid_labels;
+};
+
+LabelSet LabelsFor(const TargetSet& targets) {
+  LabelSet labels;
+  for (const auto& target : targets)
+    labels.insert(target->label());
+  return labels;
+}
+
+bool AnyBuildFilesWereModified(const SourceFileSet& source_files) {
+  for (const auto& file : source_files) {
+    if (base::EndsWith(file->value(), ".gn", base::CompareCase::SENSITIVE) ||
+        base::EndsWith(file->value(), ".gni", base::CompareCase::SENSITIVE))
+      return true;
+  }
+  return false;
+}
+
+TargetSet Intersect(const TargetSet& l, const TargetSet& r) {
+  TargetSet result;
+  std::set_intersection(l.begin(), l.end(), r.begin(), r.end(),
+                        std::inserter(result, result.begin()));
+  return result;
+}
+
+std::vector<std::string> GetStringVector(const base::DictionaryValue& dict,
+                                         const std::string& key,
+                                         Err* err) {
+  std::vector<std::string> strings;
+  const base::ListValue* lst;
+  bool ret = dict.GetList(key, &lst);
+  if (!ret) {
+    *err = Err(Location(), "Input does not have a key named \"" + key +
+                               "\" with a list value.");
+    return strings;
+  }
+
+  for (size_t i = 0; i < lst->GetSize(); i++) {
+    std::string s;
+    ret = lst->GetString(i, &s);
+    if (!ret) {
+      *err = Err(Location(), "Item " + std::to_string(i) + " of \"" + key +
+                                 "\" is not a string.");
+      strings.clear();
+      return strings;
+    }
+    strings.push_back(std::move(s));
+  }
+  *err = Err();
+  return strings;
+}
+
+void WriteString(base::DictionaryValue& dict,
+                 const std::string& key,
+                 const std::string& value) {
+  dict.SetString(key, value);
+};
+
+void WriteLabels(const Label& default_toolchain,
+                 base::DictionaryValue& dict,
+                 const std::string& key,
+                 const LabelSet& labels) {
+  std::vector<std::string> strings;
+  auto value = base::WrapUnique(new base::ListValue());
+  for (const auto l : labels)
+    strings.push_back(l.GetUserVisibleName(default_toolchain));
+  std::sort(strings.begin(), strings.end());
+  value->AppendStrings(strings);
+  dict.Set(key, std::move(value));
+}
+
+Label AbsoluteOrSourceAbsoluteStringToLabel(const Label& default_toolchain,
+                                            const std::string& s, Err* err) {
+  if (!IsPathSourceAbsolute(s) && !IsPathAbsolute(s)) {
+    *err = Err(Location(),
+               "\"" + s + "\" is not a source-absolute or absolute path.");
+    return Label();
+  }
+  return Label::Resolve(SourceDir("//"), default_toolchain, Value(nullptr, s),
+                        err);
+}
+
+Err JSONToInputs(const Label& default_toolchain,
+                 const std::string input,
+                 Inputs* inputs) {
+  int error_code_out;
+  std::string error_msg_out;
+  int error_line_out;
+  int error_column_out;
+  std::unique_ptr<base::Value> value = base::JSONReader().ReadAndReturnError(
+      input, base::JSONParserOptions::JSON_PARSE_RFC, &error_code_out,
+      &error_msg_out, &error_line_out, &error_column_out);
+  if (!value)
+    return Err(Location(), "Input is not valid JSON:" + error_msg_out);
+
+  const base::DictionaryValue* dict;
+  if (!value->GetAsDictionary(&dict))
+    return Err(Location(), "Input is not a dictionary.");
+
+  Err err;
+  std::vector<std::string> strings;
+  strings = GetStringVector(*dict, "files", &err);
+  if (err.has_error())
+    return err;
+  for (auto s : strings) {
+    if (!IsPathSourceAbsolute(s) && !IsPathAbsolute(s))
+      return Err(Location(),
+                 "\"" + s + "\" is not a source-absolute or absolute path.");
+    inputs->source_vec.push_back(SourceFile(s));
+  }
+
+  strings = GetStringVector(*dict, "compile_targets", &err);
+  if (err.has_error())
+    return err;
+
+  inputs->compile_included_all = false;
+  for (auto& s : strings) {
+    if (s == "all") {
+      inputs->compile_included_all = true;
+    } else {
+      inputs->compile_vec.push_back(
+          AbsoluteOrSourceAbsoluteStringToLabel(default_toolchain, s, &err));
+      if (err.has_error())
+        return err;
+    }
+  }
+
+  strings = GetStringVector(*dict, "test_targets", &err);
+  if (err.has_error())
+    return err;
+  for (auto& s : strings) {
+    inputs->test_vec.push_back(
+        AbsoluteOrSourceAbsoluteStringToLabel(default_toolchain, s, &err));
+    if (err.has_error())
+      return err;
+  }
+
+  for (auto& s : inputs->source_vec)
+    inputs->source_files.insert(&s);
+  for (auto& l : inputs->compile_vec)
+    inputs->compile_labels.insert(l);
+  for (auto& l : inputs->test_vec)
+    inputs->test_labels.insert(l);
+  return Err();
+}
+
+std::string OutputsToJSON(const Outputs& outputs,
+                          const Label& default_toolchain) {
+  std::string output;
+  auto value = base::MakeUnique<base::DictionaryValue>();
+
+  if (outputs.error.size()) {
+    WriteString(*value, "error", outputs.error);
+    WriteLabels(default_toolchain, *value, "invalid_targets",
+                outputs.invalid_labels);
+  } else {
+    WriteString(*value, "status", outputs.status);
+    WriteLabels(default_toolchain, *value, "compile_targets",
+                outputs.compile_labels);
+    WriteLabels(default_toolchain, *value, "test_targets", outputs.test_labels);
+  }
+
+  base::JSONWriter::Write(*value.get(), &output);
+  return output;
+}
+
+}  // namespace
+
+Analyzer::Analyzer(const Builder& builder)
+    : all_targets_(builder.GetAllResolvedTargets()),
+      default_toolchain_(builder.loader()->GetDefaultToolchain()) {
+  for (const auto* target : all_targets_) {
+    labels_to_targets_[target->label()] = target;
+    for (const auto& dep_pair : target->GetDeps(Target::DEPS_ALL))
+      dep_map_.insert(std::make_pair(dep_pair.ptr, target));
+  }
+  for (const auto* target : all_targets_) {
+    if (dep_map_.find(target) == dep_map_.end())
+      roots_.insert(target);
+  }
+}
+
+Analyzer::~Analyzer() {}
+
+std::string Analyzer::Analyze(const std::string& input, Err* err) const {
+  Inputs inputs;
+  Outputs outputs;
+
+  Err local_err = JSONToInputs(default_toolchain_, input, &inputs);
+  if (local_err.has_error()) {
+    outputs.error = local_err.message();
+    if (err)
+      *err = Err();
+    return "";
+  }
+
+  LabelSet invalid_labels;
+  for (const auto& label : InvalidLabels(inputs.compile_labels))
+    invalid_labels.insert(label);
+  for (const auto& label : InvalidLabels(inputs.test_labels))
+    invalid_labels.insert(label);
+  if (!invalid_labels.empty()) {
+    outputs.error = "Invalid targets";
+    outputs.invalid_labels = invalid_labels;
+    if (err)
+      *err = Err();
+    return OutputsToJSON(outputs, default_toolchain_);
+  }
+
+  TargetSet affected_targets = AllAffectedTargets(inputs.source_files);
+  if (affected_targets.empty()) {
+    outputs.status = "No dependency";
+    if (err)
+      *err = Err();
+    return OutputsToJSON(outputs, default_toolchain_);
+  }
+
+  // TODO: We can do smarter things when we detect changes to build files.
+  // For example, if all of the ninja files are unchanged, we know that we
+  // can ignore changes to these files. Also, for most .gn files, we can
+  // treat a change as simply affecting every target, config, or toolchain
+  // defined in that file.
+  if (AnyBuildFilesWereModified(inputs.source_files)) {
+    outputs.status = "Found dependency (all)";
+    outputs.compile_labels = inputs.compile_labels;
+    outputs.test_labels = inputs.test_labels;
+    if (err)
+      *err = Err();
+    return OutputsToJSON(outputs, default_toolchain_);
+  }
+
+  TargetSet compile_targets = TargetsFor(inputs.compile_labels);
+  if (inputs.compile_included_all) {
+    for (auto& root : roots_)
+      compile_targets.insert(root);
+  }
+  TargetSet filtered_targets = Filter(compile_targets);
+  outputs.compile_labels =
+      LabelsFor(Intersect(filtered_targets, affected_targets));
+
+  TargetSet test_targets = TargetsFor(inputs.test_labels);
+  outputs.test_labels = LabelsFor(Intersect(test_targets, affected_targets));
+
+  if (outputs.compile_labels.empty() && outputs.test_labels.empty())
+    outputs.status = "No dependency";
+  else
+    outputs.status = "Found dependency";
+  *err = Err();
+  return OutputsToJSON(outputs, default_toolchain_);
+}
+
+TargetSet Analyzer::AllAffectedTargets(
+    const SourceFileSet& source_files) const {
+  TargetSet direct_matches;
+  for (const auto& source_file : source_files)
+    AddTargetsDirectlyReferringToFileTo(source_file, &direct_matches);
+  TargetSet all_matches;
+  for (const auto& match : direct_matches)
+    AddAllRefsTo(match, &all_matches);
+  return all_matches;
+}
+
+LabelSet Analyzer::InvalidLabels(const LabelSet& labels) const {
+  LabelSet invalid_labels;
+  for (const Label& label : labels) {
+    if (labels_to_targets_.find(label) == labels_to_targets_.end())
+      invalid_labels.insert(label);
+  }
+  return invalid_labels;
+}
+
+TargetSet Analyzer::TargetsFor(const LabelSet& labels) const {
+  TargetSet targets;
+  for (const auto& label : labels) {
+    auto it = labels_to_targets_.find(label);
+    if (it != labels_to_targets_.end())
+      targets.insert(it->second);
+  }
+  return targets;
+}
+
+TargetSet Analyzer::Filter(const TargetSet& targets) const {
+  TargetSet seen;
+  TargetSet filtered;
+  for (const auto* target : targets)
+    FilterTarget(target, &seen, &filtered);
+  return filtered;
+}
+
+void Analyzer::FilterTarget(const Target* target,
+                            TargetSet* seen,
+                            TargetSet* filtered) const {
+  if (seen->find(target) == seen->end()) {
+    seen->insert(target);
+    if (target->output_type() != Target::GROUP) {
+      filtered->insert(target);
+    } else {
+      for (const auto& pair : target->GetDeps(Target::DEPS_ALL))
+        FilterTarget(pair.ptr, seen, filtered);
+    }
+  }
+}
+
+bool Analyzer::TargetRefersToFile(const Target* target,
+                                  const SourceFile* file) const {
+  for (const auto& cur_file : target->sources()) {
+    if (cur_file == *file)
+      return true;
+  }
+  for (const auto& cur_file : target->public_headers()) {
+    if (cur_file == *file)
+      return true;
+  }
+  for (const auto& cur_file : target->inputs()) {
+    if (cur_file == *file)
+      return true;
+  }
+  for (const auto& cur_file : target->data()) {
+    if (cur_file == file->value())
+      return true;
+    if (cur_file.back() == '/' &&
+        base::StartsWith(file->value(), cur_file, base::CompareCase::SENSITIVE))
+      return true;
+  }
+
+  if (target->action_values().script().value() == file->value())
+    return true;
+
+  std::vector<SourceFile> outputs;
+  target->action_values().GetOutputsAsSourceFiles(target, &outputs);
+  for (const auto& cur_file : outputs) {
+    if (cur_file == *file)
+      return true;
+  }
+  return false;
+}
+
+void Analyzer::AddTargetsDirectlyReferringToFileTo(const SourceFile* file,
+                                                   TargetSet* matches) const {
+  for (const auto& target : all_targets_) {
+    // Only handles targets in the default toolchain.
+    if ((target->label().GetToolchainLabel() == default_toolchain_) &&
+        TargetRefersToFile(target, file))
+      matches->insert(target);
+  }
+}
+
+void Analyzer::AddAllRefsTo(const Target* target, TargetSet* results) const {
+  if (results->find(target) != results->end())
+    return;  // Already found this target.
+  results->insert(target);
+
+  auto dep_begin = dep_map_.lower_bound(target);
+  auto dep_end = dep_map_.upper_bound(target);
+  for (auto cur_dep = dep_begin; cur_dep != dep_end; cur_dep++)
+    AddAllRefsTo(cur_dep->second, results);
+}
diff --git a/tools/gn/analyzer.h b/tools/gn/analyzer.h
new file mode 100644
index 0000000..130ebe9
--- /dev/null
+++ b/tools/gn/analyzer.h
@@ -0,0 +1,95 @@
+// Copyright 2016 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.
+
+#ifndef TOOLS_GN_ANALYZER_H_
+#define TOOLS_GN_ANALYZER_H_
+
+#include <set>
+#include <string>
+#include <vector>
+
+#include "tools/gn/builder.h"
+#include "tools/gn/label.h"
+#include "tools/gn/source_file.h"
+#include "tools/gn/target.h"
+
+// An Analyzer can answer questions about a build graph. It is used
+// to answer queries for the `refs` and `analyze` commands, where we
+// need to look at the graph in ways that can't easily be determined
+// from just a single Target.
+class Analyzer {
+ public:
+  using LabelSet = std::set<Label>;
+  using SourceFileSet = std::set<const SourceFile*>;
+  using TargetSet = std::set<const Target*>;
+
+  explicit Analyzer(const Builder& builder);
+  ~Analyzer();
+
+  // Figures out from a Buider and a JSON-formatted string containing lists
+  // of files and targets, which targets would be affected by modifications
+  // to the files . See the help text for the analyze command (kAnalyze_Help)
+  // for the specification of the input and output string formats and the
+  // expected behavior of the method.
+  std::string Analyze(const std::string& input, Err* err) const;
+
+ private:
+  // Returns the roots of the build graph: the set of targets that
+  // no other target depends on.
+  TargetSet& roots() { return roots_; };
+
+  // Returns the set of all targets that might be affected, directly or
+  // indirectly, by modifications to the given source files.
+  TargetSet AllAffectedTargets(const SourceFileSet& source_files) const;
+
+  // Returns the set of labels that do not refer to objects in the graph.
+  LabelSet InvalidLabels(const LabelSet& labels) const;
+
+  // Returns the set of all targets that have a label in the given set.
+  // Invalid (or missing) labels will be ignored.
+  TargetSet TargetsFor(const LabelSet& labels) const;
+
+  // Returns a filtered set of the given targets, meaning that for each of the
+  // given targets,
+  // - if the target is not a group, add it to the set
+  // - if the target is a group, recursively filter each dependency and add
+  //   its filtered results to the set.
+  //
+  // For example, if we had:
+  //
+  //   group("foobar") { deps = [ ":foo", ":bar" ] }
+  //   group("bar") { deps = [ ":baz", ":quux" ] }
+  //   executable("foo") { ... }
+  //   executable("baz") { ... }
+  //   executable("quux") { ... }
+  //
+  // Then the filtered version of {"foobar"} would be {":foo", ":baz",
+  // ":quux"}. This is used by the analyze command in order to only build
+  // the affected dependencies of a group (and not also build the unaffected
+  // ones).
+  //
+  // This filtering behavior is also known as "pruning" the list of targets.
+  TargetSet Filter(const TargetSet& targets) const;
+
+  // Filter an individual target and adds the results to filtered
+  // (see Filter(), above).
+  void FilterTarget(const Target*, TargetSet* seen, TargetSet* filtered) const;
+
+  bool TargetRefersToFile(const Target* target, const SourceFile* file) const;
+
+  void AddTargetsDirectlyReferringToFileTo(const SourceFile* file,
+                                           TargetSet* matches) const;
+
+  void AddAllRefsTo(const Target* target, TargetSet* matches) const;
+
+  std::vector<const Target*> all_targets_;
+  std::map<const Label, const Target*> labels_to_targets_;
+  Label default_toolchain_;
+  std::set<const Target*> roots_;
+
+  // Maps targets to the list of targets that depend on them.
+  std::multimap<const Target*, const Target*> dep_map_;
+};
+
+#endif  // TOOLS_GN_ANALYZER_H_
diff --git a/tools/gn/analyzer_unittest.cc b/tools/gn/analyzer_unittest.cc
new file mode 100644
index 0000000..6910a67
--- /dev/null
+++ b/tools/gn/analyzer_unittest.cc
@@ -0,0 +1,173 @@
+// Copyright 2016 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.
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "tools/gn/analyzer.h"
+#include "tools/gn/build_settings.h"
+#include "tools/gn/builder.h"
+#include "tools/gn/loader.h"
+#include "tools/gn/settings.h"
+#include "tools/gn/source_file.h"
+
+
+namespace {
+
+class MockLoader : public Loader {
+ public:
+  MockLoader() {}
+
+  void Load(const SourceFile& file,
+            const LocationRange& origin,
+            const Label& toolchain_name) override {}
+  void ToolchainLoaded(const Toolchain* toolchain) override {}
+  Label GetDefaultToolchain() const override {
+    return Label(SourceDir("//tc/"), "default");
+  }
+  const Settings* GetToolchainSettings(const Label& label) const override {
+    return nullptr;
+  }
+
+ private:
+  ~MockLoader() override {}
+};
+
+class AnalyzerTest : public testing::Test {
+ public:
+  AnalyzerTest()
+      : loader_(new MockLoader),
+        builder_(loader_.get()),
+        settings_(&build_settings_, std::string()) {
+    build_settings_.SetBuildDir(SourceDir("//out/"));
+    settings_.set_toolchain_label(Label(SourceDir("//tc/"), "default"));
+    settings_.set_default_toolchain_label(settings_.toolchain_label());
+    tc_dir_ = settings_.toolchain_label().dir();
+    tc_name_ = settings_.toolchain_label().name();
+  }
+
+  Target* MakeTarget(const std::string dir,
+                     const std::string name,
+                     Target::OutputType type,
+                     const std::vector<std::string>& sources,
+                     const std::vector<Target*>& deps) {
+    Label lbl(SourceDir(dir), name, tc_dir_, tc_name_);
+    Target* target = new Target(&settings_, lbl);
+    target->set_output_type(type);
+    for (const auto& s : sources)
+      target->sources().push_back(SourceFile(s));
+    for (const auto* d : deps)
+      target->public_deps().push_back(LabelTargetPair(d));
+    builder_.ItemDefined(std::unique_ptr<Item>(target));
+    return target;
+  }
+
+  void AddSource(Target* a, std::string path) {}
+
+  void AddDep(Target* a, Target* b) {}
+
+  void SetUpABasicBuildGraph() {
+    std::vector<std::string> no_sources;
+    std::vector<Target*> no_deps;
+
+    // All of the targets below are owned by the builder, so none of them
+    // get leaked.
+
+    // Ignore the returned target since nothing depends on it.
+    MakeTarget("//", "a", Target::EXECUTABLE, {"//a.cc"}, no_deps);
+
+    Target* b =
+        MakeTarget("//d", "b", Target::SOURCE_SET, {"//d/b.cc"}, no_deps);
+
+    Target* b_unittests = MakeTarget("//d", "b_unittests", Target::EXECUTABLE,
+                                     {"//d/b_unittest.cc"}, {b});
+
+    Target* c = MakeTarget("//d", "c", Target::EXECUTABLE, {"//d/c.cc"}, {b});
+
+    Target* b_unittests_and_c =
+        MakeTarget("//d", "b_unittests_and_c", Target::GROUP, no_sources,
+                   {b_unittests, c});
+
+    Target* e =
+        MakeTarget("//d", "e", Target::EXECUTABLE, {"//d/e.cc"}, no_deps);
+
+    // Also ignore this returned target since nothing depends on it.
+    MakeTarget("//d", "d", Target::GROUP, no_sources, {b_unittests_and_c, e});
+  }
+
+  void RunBasicTest(const std::string& input,
+                    const std::string& expected_output) {
+    SetUpABasicBuildGraph();
+    Err err;
+    std::string actual_output = Analyzer(builder_).Analyze(input, &err);
+    EXPECT_EQ(err.has_error(), false);
+    EXPECT_EQ(expected_output, actual_output);
+  }
+
+ protected:
+  scoped_refptr<MockLoader> loader_;
+  Builder builder_;
+  BuildSettings build_settings_;
+  Settings settings_;
+  SourceDir tc_dir_;
+  std::string tc_name_;
+};
+
+}  // namespace
+
+// TODO: clean this up when raw string literals are allowed.
+
+TEST_F(AnalyzerTest, AllWasPruned) {
+  RunBasicTest(
+      "{"
+      "  \"files\": [ \"//d/b.cc\" ],"
+      "  \"compile_targets\": [ \"all\" ],"
+      "  \"test_targets\": [ ]"
+      "}",
+      "{"
+      "\"compile_targets\":[\"//d:b_unittests\",\"//d:c\"],"
+      "\"status\":\"Found dependency\","
+      "\"test_targets\":[]"
+      "}");
+}
+
+TEST_F(AnalyzerTest, NoDependency) {
+  RunBasicTest(
+      "{"
+      "  \"files\":[ \"//missing.cc\" ],"
+      "  \"compile_targets\": [ \"all\" ],"
+      "  \"test_targets\": [ \"//:a\" ]"
+      "}",
+      "{"
+      "\"compile_targets\":[],"
+      "\"status\":\"No dependency\","
+      "\"test_targets\":[]"
+      "}");
+}
+
+TEST_F(AnalyzerTest, NoFilesNoTargets) {
+  RunBasicTest(
+      "{"
+      "  \"files\": [],"
+      "  \"compile_targets\": [],"
+      "  \"test_targets\": []"
+      "}",
+      "{"
+      "\"compile_targets\":[],"
+      "\"status\":\"No dependency\","
+      "\"test_targets\":[]"
+      "}");
+}
+
+TEST_F(AnalyzerTest, OneTestTargetModified) {
+  RunBasicTest(
+      "{"
+      "  \"files\": [ \"//a.cc\" ],"
+      "  \"compile_targets\": [],"
+      "  \"test_targets\": [ \"//:a\" ]"
+      "}",
+      "{"
+      "\"compile_targets\":[],"
+      "\"status\":\"Found dependency\","
+      "\"test_targets\":[\"//:a\"]"
+      "}");
+}
diff --git a/tools/gn/command_analyze.cc b/tools/gn/command_analyze.cc
new file mode 100644
index 0000000..a9c0d9a
--- /dev/null
+++ b/tools/gn/command_analyze.cc
@@ -0,0 +1,139 @@
+// Copyright (c) 2013 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.
+
+#include <algorithm>
+#include <iterator>
+#include <set>
+#include <vector>
+
+#include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "tools/gn/analyzer.h"
+#include "tools/gn/commands.h"
+#include "tools/gn/filesystem_utils.h"
+#include "tools/gn/location.h"
+#include "tools/gn/setup.h"
+
+namespace commands {
+
+const char kAnalyze[] = "analyze";
+const char kAnalyze_HelpShort[] =
+    "analyze: Analyze which targets are affected by a list of files.";
+const char kAnalyze_Help[] =
+    "gn analyze <out_dir> <input_path> <output_path>\n"
+    "\n"
+    "  Analyze which targets are affected by a list of files.\n"
+    "\n"
+    "  This command takes three arguments:\n"
+    "\n"
+    "  out_dir is the path to the build directory.\n"
+    "\n"
+    "  input_path is a path to a file containing a JSON object with three\n"
+    "  fields:\n"
+    "\n"
+    "   - \"files\": A list of the filenames to check.\n"
+    "\n"
+    "   - \"compile_targets\": A list of the labels targets that we wish to\n"
+    "     rebuild, but aren't necessarily needed for testing. The\n"
+    "     important difference between this field and \"test_targets\"\n"
+    "     is that if an item in the compile_targets list is a group, then\n"
+    "     any dependencies of that group will be returned if they are out\n"
+    "     of date, but the group itself does not need to be. If the\n"
+    "     dependencies themselves are groups, the same filtering is\n"
+    "     repeated. This filtering can be used to avoid rebuilding\n"
+    "     dependencies of a group that are unaffected by the input files.\n"
+    "     The list may contain the string \"all\" to refer to a\n"
+    "     pseudo-group that contains every root target in the build graph.\n"
+    "\n"
+    "     This filtering behavior is also known as \"pruning\" the list\n"
+    "     of compile targets.\n"
+    "\n"
+    "   - \"test_targets\": A list of the labels for targets that\n"
+    "     are needed to run the tests we wish to run. Unlike\n"
+    "     \"compile_targets\", this list may not contain the string \"all\",\n"
+    "     because having a test be dependent on everything in the build\n"
+    "     would be silly.\n"
+    "\n"
+    "  output_path is a path indicating where the results of the command\n"
+    "  are to be written. The results will be a file containing a JSON\n"
+    "  object with one or more of following fields:\n"
+    "\n"
+    "   - \"compile_targets\": A list of the labels derived from the input\n"
+    "     compile_targets list that are affected by the input files.\n"
+    "     Due to the way the filtering works for compile targets as\n"
+    "     described above, this list may contain targets that do not appear\n"
+    "     in the input list.\n"
+    "\n"
+    "   - \"test_targets\": A list of the labels from the input\n"
+    "     test_targets list that are affected by the input files. This list\n"
+    "     will be a proper subset of the input list.\n"
+    "\n"
+    "   - \"invalid_targets\": A list of any names from the input that\n"
+    "     do not exist in the build graph. If this list is non-empty,\n"
+    "     the \"error\" field will also be set to \"Invalid targets\".\n"
+    "\n"
+    "   - \"status\": A string containing one of three values:\n"
+    "\n"
+    "       - \"Found dependency\"\n"
+    "       - \"No dependency\"\n"
+    "       - \"Found dependency (all)\"\n"
+    "\n"
+    "     In the first case, the lists returned in compile_targets and\n"
+    "     test_targets should be passed to ninja to build. In the second\n"
+    "     case, nothing was affected and no build is necessary. In the third\n"
+    "     case, GN could not determine the correct answer and returned the\n"
+    "     input as the output in order to be safe.\n"
+    "\n"
+    "   - \"error\": This will only be present if an error occurred, and\n"
+    "     will contain a string describing the error. This includes cases\n"
+    "     where the input file is not in the right format, or contains\n"
+    "     invalid targets.\n"
+
+    "  The command returns 1 if it is unable to read the input file or write\n"
+    "  the output file, or if there is something wrong with the build such\n"
+    "  that gen would also fail, and 0 otherwise. In particular, it returns\n"
+    "  0 even if the \"error\" key is non-empty and a non-fatal error\n"
+    "  occurred. In other words, it tries really hard to always write\n"
+    "  something to the output JSON and convey errors that way rather than\n"
+    "  via return codes.\n";
+
+int RunAnalyze(const std::vector<std::string>& args) {
+  if (args.size() != 3) {
+    Err(Location(), "You're holding it wrong.",
+        "Usage: \"gn analyze <out_dir> <input_path> <output_path>")
+        .PrintToStdout();
+    return 1;
+  }
+
+  std::string input;
+  bool ret = base::ReadFileToString(UTF8ToFilePath(args[1]), &input);
+  if (!ret) {
+    Err(Location(), "Input file " + args[1] + " not found.").PrintToStdout();
+    return 1;
+  }
+
+  Setup* setup = new Setup;
+  setup->build_settings().set_check_for_bad_items(false);
+  if (!setup->DoSetup(args[0], false) || !setup->Run())
+    return 1;
+
+  Analyzer analyzer(setup->builder());
+
+  Err err;
+  std::string output = Analyzer(setup->builder()).Analyze(input, &err);
+  if (err.has_error()) {
+    err.PrintToStdout();
+    return 1;
+  }
+
+  WriteFile(UTF8ToFilePath(args[2]), output, &err);
+  if (err.has_error()) {
+    err.PrintToStdout();
+    return 1;
+  }
+
+  return 0;
+}
+
+}  // namespace commands
diff --git a/tools/gn/commands.cc b/tools/gn/commands.cc
index bf89c95..39dd079 100644
--- a/tools/gn/commands.cc
+++ b/tools/gn/commands.cc
@@ -360,6 +360,7 @@
                                        k##cmd##_Help, \
                                        &Run##cmd);
 
+    INSERT_COMMAND(Analyze)
     INSERT_COMMAND(Args)
     INSERT_COMMAND(Check)
     INSERT_COMMAND(Clean)
diff --git a/tools/gn/commands.h b/tools/gn/commands.h
index 01bf0ee..9593d4e 100644
--- a/tools/gn/commands.h
+++ b/tools/gn/commands.h
@@ -29,6 +29,11 @@
 
 typedef int (*CommandRunner)(const std::vector<std::string>&);
 
+extern const char kAnalyze[];
+extern const char kAnalyze_HelpShort[];
+extern const char kAnalyze_Help[];
+int RunAnalyze(const std::vector<std::string>& args);
+
 extern const char kArgs[];
 extern const char kArgs_HelpShort[];
 extern const char kArgs_Help[];
diff --git a/tools/gn/docs/reference.md b/tools/gn/docs/reference.md
index 7771965..540d3ef 100644
--- a/tools/gn/docs/reference.md
+++ b/tools/gn/docs/reference.md
@@ -245,6 +245,85 @@
 
 
 ```
+## **gn analyze <out_dir> <input_path> <output_path>**
+
+```
+  Analyze which targets are affected by a list of files.
+
+  This command takes three arguments:
+
+  out_dir is the path to the build directory.
+
+  input_path is a path to a file containing a JSON object with three
+  fields:
+
+   - "files": A list of the filenames to check.
+
+   - "compile_targets": A list of the labels targets that we wish to
+     rebuild, but aren't necessarily needed for testing. The
+     important difference between this field and "test_targets"
+     is that if an item in the compile_targets list is a group, then
+     any dependencies of that group will be returned if they are out
+     of date, but the group itself does not need to be. If the
+     dependencies themselves are groups, the same filtering is
+     repeated. This filtering can be used to avoid rebuilding
+     dependencies of a group that are unaffected by the input files.
+     The list may contain the string "all" to refer to a
+     pseudo-group that contains every root target in the build graph.
+
+     This filtering behavior is also known as "pruning" the list
+     of compile targets.
+
+   - "test_targets": A list of the labels for targets that
+     are needed to run the tests we wish to run. Unlike
+     "compile_targets", this list may not contain the string "all",
+     because having a test be dependent on everything in the build
+     would be silly.
+
+  output_path is a path indicating where the results of the command
+  are to be written. The results will be a file containing a JSON
+  object with one or more of following fields:
+
+   - "compile_targets": A list of the labels derived from the input
+     compile_targets list that are affected by the input files.
+     Due to the way the filtering works for compile targets as
+     described above, this list may contain targets that do not appear
+     in the input list.
+
+   - "test_targets": A list of the labels from the input
+     test_targets list that are affected by the input files. This list
+     will be a proper subset of the input list.
+
+   - "invalid_targets": A list of any names from the input that
+     do not exist in the build graph. If this list is non-empty,
+     the "error" field will also be set to "Invalid targets".
+
+   - "status": A string containing one of three values:
+
+       - "Found dependency"
+       - "No dependency"
+       - "Found dependency (all)"
+
+     In the first case, the lists returned in compile_targets and
+     test_targets should be passed to ninja to build. In the second
+     case, nothing was affected and no build is necessary. In the third
+     case, GN could not determine the correct answer and returned the
+     input as the output in order to be safe.
+
+   - "error": This will only be present if an error occurred, and
+     will contain a string describing the error. This includes cases
+     where the input file is not in the right format, or contains
+     invalid targets.
+  The command returns 1 if it is unable to read the input file or write
+  the output file, or if there is something wrong with the build such
+  that gen would also fail, and 0 otherwise. In particular, it returns
+  0 even if the "error" key is non-empty and a non-fatal error
+  occurred. In other words, it tries really hard to always write
+  something to the output JSON and convey errors that way rather than
+  via return codes.
+
+
+```
 ## **gn args <out_dir> [\--list] [\--short] [\--args]**
 
 ```
@@ -3507,16 +3586,6 @@
 
 
 ```
-## **toolchain_args**: Set build arguments for toolchain build setup.
-
-```
-  DEPRECATED. Instead use:
-    toolchain_args = { ... }
-
-  See "gn help toolchain" for documentation.
-
-
-```
 ## **write_file**: Write a file to disk.
 
 ```
diff --git a/tools/gn/filesystem_utils.cc b/tools/gn/filesystem_utils.cc
index 6152ae0..44fa0eb 100644
--- a/tools/gn/filesystem_utils.cc
+++ b/tools/gn/filesystem_utils.cc
@@ -336,6 +336,10 @@
   return true;
 }
 
+bool IsPathSourceAbsolute(const base::StringPiece& path) {
+  return (path.size() >= 2 && path[0] == '/' && path[1] == '/');
+}
+
 bool MakeAbsolutePathRelativeIfPossible(const base::StringPiece& source_root,
                                         const base::StringPiece& path,
                                         std::string* dest) {
@@ -754,6 +758,11 @@
   if (ContentsEqual(file_path, data))
     return true;
 
+  return WriteFile(file_path, data, err);
+}
+
+bool WriteFile(const base::FilePath& file_path, const std::string& data,
+               Err* err) {
   // Create the directory if necessary.
   if (!base::CreateDirectory(file_path.DirName())) {
     if (err) {
diff --git a/tools/gn/filesystem_utils.h b/tools/gn/filesystem_utils.h
index f53f24a..c17ab39 100644
--- a/tools/gn/filesystem_utils.h
+++ b/tools/gn/filesystem_utils.h
@@ -100,6 +100,11 @@
 // paths of both the native format: "C:/foo" and ours "/C:/foo"
 bool IsPathAbsolute(const base::StringPiece& path);
 
+// Returns true if the input string is source-absolute. Source-absolute
+// paths begin with two forward slashes and resolve as if they are
+// relative to the source root.
+bool IsPathSourceAbsolute(const base::StringPiece& path);
+
 // Given an absolute path, checks to see if is it is inside the source root.
 // If it is, fills a source-absolute path into the given output and returns
 // true. If it isn't, clears the dest and returns false.
@@ -175,6 +180,11 @@
                         const std::string& data,
                         Err* err);
 
+// Writes given stream contents to the given file. Returns true if data was
+// successfully written, false otherwise. |err| is set on error if not nullptr.
+bool WriteFile(const base::FilePath& file_path, const std::string& data,
+               Err* err);
+
 // -----------------------------------------------------------------------------
 
 enum class BuildDirType {
diff --git a/tools/gn/gn.gyp b/tools/gn/gn.gyp
index 1aa23d3..b006f68 100644
--- a/tools/gn/gn.gyp
+++ b/tools/gn/gn.gyp
@@ -14,6 +14,8 @@
         'action_target_generator.h',
         'action_values.cc',
         'action_values.h',
+        'analyzer.cc',
+        'analyzer.h',
         'args.cc',
         'args.h',
         'binary_target_generator.cc',
@@ -32,6 +34,7 @@
         'bundle_file_rule.h',
         'c_include_iterator.cc',
         'c_include_iterator.h',
+        'command_analyze.cc',
         'command_args.cc',
         'command_check.cc',
         'command_clean.cc',
@@ -242,6 +245,7 @@
       'type': '<(gtest_target_type)',
       'sources': [
         'action_target_generator_unittest.cc',
+        'analyzer_unittest.cc',
         'builder_unittest.cc',
         'c_include_iterator_unittest.cc',
         'command_format_unittest.cc',
diff --git a/tools/gn/label.h b/tools/gn/label.h
index cbeb177..12ae529 100644
--- a/tools/gn/label.h
+++ b/tools/gn/label.h
@@ -32,7 +32,7 @@
   Label(const Label& other);
   ~Label();
 
-  // Resolives a string from a build file that may be relative to the
+  // Resolves a string from a build file that may be relative to the
   // current directory into a fully qualified label. On failure returns an
   // is_null() label and sets the error.
   static Label Resolve(const SourceDir& current_dir,