Fix analyze not noticing when BUILD.gn files change

The dependency tracking for template scopes was not taking into account
invoker scopes. So, if a BUILD.gn file changed, targets defined in it
through templates were not being considered "stale" by analyze.

Bug: chromium/999002
Change-Id: Ibd0a82308e5ffc99e9856e628ee0c242b2890350
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/7660
Reviewed-by: Brett Wilson <brettw@chromium.org>
Commit-Queue: Brett Wilson <brettw@chromium.org>
diff --git a/src/gn/input_file_manager.cc b/src/gn/input_file_manager.cc
index f529990..1c1fe01 100644
--- a/src/gn/input_file_manager.cc
+++ b/src/gn/input_file_manager.cc
@@ -36,6 +36,7 @@
 bool DoLoadFile(const LocationRange& origin,
                 const BuildSettings* build_settings,
                 const SourceFile& name,
+                InputFileManager::SyncLoadFileCallback load_file_callback,
                 InputFile* file,
                 std::vector<Token>* tokens,
                 std::unique_ptr<ParseNode>* root,
@@ -52,7 +53,13 @@
   // Read.
   base::FilePath primary_path = build_settings->GetFullPath(name);
   ScopedTrace load_trace(TraceItem::TRACE_FILE_LOAD, name.value());
-  if (!file->Load(primary_path)) {
+  if (load_file_callback) {
+    if (!load_file_callback(name, file)) {
+      *err = Err(origin, "Can't load input file.",
+                 "File not mocked by load_file_callback:\n  " + name.value());
+      return false;
+    }
+  } else if (!file->Load(primary_path)) {
     if (!build_settings->secondary_source_path().empty()) {
       // Fall back to secondary source tree.
       base::FilePath secondary_path =
@@ -283,7 +290,7 @@
   std::vector<Token> tokens;
   std::unique_ptr<ParseNode> root;
   bool success =
-      DoLoadFile(origin, build_settings, name, file, &tokens, &root, err);
+      DoLoadFile(origin, build_settings, name, load_file_callback_, file, &tokens, &root, err);
   // Can't return early. We have to ensure that the completion event is
   // signaled in all cases bacause another thread could be blocked on this one.
 
diff --git a/src/gn/input_file_manager.h b/src/gn/input_file_manager.h
index c3633e8..f5ac565 100644
--- a/src/gn/input_file_manager.h
+++ b/src/gn/input_file_manager.h
@@ -40,6 +40,10 @@
   // refer to the root block of the file. On failure, this will be NULL.
   using FileLoadCallback = std::function<void(const ParseNode*)>;
 
+  // Callback to emulate SyncLoadFile in tests.
+  using SyncLoadFileCallback =
+      std::function<bool(const SourceFile& file_name, InputFile* file)>;
+
   InputFileManager();
 
   // Loads the given file and executes the callback on the worker pool.
@@ -90,6 +94,10 @@
   // Fills the vector with all input files.
   void GetAllPhysicalInputFileNames(std::vector<base::FilePath>* result) const;
 
+  void set_load_file_callback(SyncLoadFileCallback load_file_callback) {
+    load_file_callback_ = load_file_callback;
+  }
+
  private:
   friend class base::RefCountedThreadSafe<InputFileManager>;
 
@@ -149,6 +157,9 @@
   // See AddDynamicInput().
   std::vector<std::unique_ptr<InputFileData>> dynamic_inputs_;
 
+  // Used by unit tests to mock out SyncLoadFile().
+  SyncLoadFileCallback load_file_callback_;
+
   DISALLOW_COPY_AND_ASSIGN(InputFileManager);
 };
 
diff --git a/src/gn/loader_unittest.cc b/src/gn/loader_unittest.cc
index 3bd598b..3896b59 100644
--- a/src/gn/loader_unittest.cc
+++ b/src/gn/loader_unittest.cc
@@ -54,9 +54,10 @@
  public:
   using Callback = std::function<void(const ParseNode*)>;
 
-  MockInputFileManager() = default;
+  MockInputFileManager();
+  ~MockInputFileManager();
 
-  LoaderImpl::AsyncLoadFileCallback GetCallback();
+  LoaderImpl::AsyncLoadFileCallback GetAsyncCallback();
 
   // Sets a given response for a given source file.
   void AddCannedResponse(const SourceFile& source_file,
@@ -75,6 +76,8 @@
     std::unique_ptr<ParseNode> root;
   };
 
+  InputFileManager::SyncLoadFileCallback GetSyncCallback();
+
   bool AsyncLoadFile(const LocationRange& origin,
                      const BuildSettings* build_settings,
                      const SourceFile& file_name,
@@ -90,7 +93,15 @@
   std::vector<std::pair<SourceFile, Callback>> pending_;
 };
 
-LoaderImpl::AsyncLoadFileCallback MockInputFileManager::GetCallback() {
+MockInputFileManager::MockInputFileManager() {
+  g_scheduler->input_file_manager()->set_load_file_callback(GetSyncCallback());
+}
+
+MockInputFileManager::~MockInputFileManager() {
+  g_scheduler->input_file_manager()->set_load_file_callback(nullptr);
+}
+
+LoaderImpl::AsyncLoadFileCallback MockInputFileManager::GetAsyncCallback() {
   return
       [this](const LocationRange& origin, const BuildSettings* build_settings,
              const SourceFile& file_name, const Callback& callback, Err* err) {
@@ -98,6 +109,17 @@
       };
 }
 
+InputFileManager::SyncLoadFileCallback MockInputFileManager::GetSyncCallback() {
+  return
+      [this](const SourceFile& file_name, InputFile* file) {
+        CannedResponseMap::const_iterator found = canned_responses_.find(file_name);
+        if (found == canned_responses_.end())
+          return false;
+        file->SetContents(found->second->input_file->contents());
+        return true;
+      };
+}
+
 // Sets a given response for a given source file.
 void MockInputFileManager::AddCannedResponse(const SourceFile& source_file,
                                              const std::string& source) {
@@ -112,6 +134,8 @@
 
   // Parse.
   canned->root = Parser::Parse(canned->tokens, &err);
+  if (err.has_error())
+    err.PrintToStdout();
   EXPECT_FALSE(err.has_error());
 
   canned_responses_[source_file] = std::move(canned);
@@ -167,7 +191,7 @@
   mock_ifm_.AddCannedResponse(build_config,
                               "set_default_toolchain(\"//tc:tc\")");
 
-  loader->set_async_load_file(mock_ifm_.GetCallback());
+  loader->set_async_load_file(mock_ifm_.GetAsyncCallback());
 
   // Request the root build file be loaded. This should kick off the default
   // build config loading.
@@ -228,7 +252,6 @@
   scoped_refptr<LoaderImpl> loader(new LoaderImpl(&build_settings_));
   mock_ifm_.AddCannedResponse(build_config,
                               "set_default_toolchain(\"//tc:tc\")");
-  mock_ifm_.AddCannedResponse(SourceFile("//test.gni"), "concurrent_jobs = 1");
   std::string root_build_content =
       "executable(\"a\") { sources = [ \"a.cc\" ] }\n"
       "config(\"b\") { configs = [\"//t:t\"] }\n"
@@ -236,7 +259,7 @@
       "pool(\"d\") { depth = 1 }";
   mock_ifm_.AddCannedResponse(root_build, root_build_content);
 
-  loader->set_async_load_file(mock_ifm_.GetCallback());
+  loader->set_async_load_file(mock_ifm_.GetAsyncCallback());
 
   // Request the root build file be loaded. This should kick off the default
   // build config loading.
@@ -265,3 +288,50 @@
 
   EXPECT_FALSE(scheduler().is_failed());
 }
+
+TEST_F(LoaderTest, TemplateBuildDependencyFilesAreCollected) {
+  SourceFile build_config("//build/config/BUILDCONFIG.gn");
+  SourceFile root_build("//BUILD.gn");
+  build_settings_.set_build_config_file(build_config);
+  build_settings_.set_item_defined_callback(
+      [builder = &mock_builder_](std::unique_ptr<Item> item) {
+        builder->OnItemDefined(std::move(item));
+      });
+
+  scoped_refptr<LoaderImpl> loader(new LoaderImpl(&build_settings_));
+  mock_ifm_.AddCannedResponse(build_config,
+                              "set_default_toolchain(\"//tc:tc\")");
+  mock_ifm_.AddCannedResponse(
+      SourceFile("//test.gni"),
+      "template(\"tmpl\") {\n"
+      "  executable(target_name) { sources = invoker.sources }\n"
+      "}\n");
+  mock_ifm_.AddCannedResponse(root_build,
+                              "import(\"//test.gni\")\n"
+                              "tmpl(\"a\") {sources = [ \"a.cc\" ]}\n");
+
+  loader->set_async_load_file(mock_ifm_.GetAsyncCallback());
+
+  // Request the root build file be loaded. This should kick off the default
+  // build config loading.
+  loader->Load(root_build, LocationRange(), Label());
+  EXPECT_TRUE(mock_ifm_.HasOnePending(build_config));
+
+  // Completing the build config load should kick off the root build file load.
+  mock_ifm_.IssueAllPending();
+  MsgLoop::Current()->RunUntilIdleForTesting();
+  EXPECT_TRUE(mock_ifm_.HasOnePending(root_build));
+
+  // Completing the root build file should define a target which must have
+  // set of source files hashes.
+  mock_ifm_.IssueAllPending();
+  MsgLoop::Current()->RunUntilIdleForTesting();
+
+  std::vector<const Item*> items = mock_builder_.GetAllItems();
+  EXPECT_TRUE(items[0]->AsTarget());
+  // Ensure the target as a dep on BUILD.gn even though it was defined via
+  // a template.
+  EXPECT_TRUE(ItemContainsBuildDependencyFile(items[0], root_build));
+
+  EXPECT_FALSE(scheduler().is_failed());
+}
diff --git a/src/gn/scope.cc b/src/gn/scope.cc
index adbfa9f..a220c8e 100644
--- a/src/gn/scope.cc
+++ b/src/gn/scope.cc
@@ -435,8 +435,7 @@
   }
 
   // Propogate build dependency files,
-  dest->build_dependency_files_.insert(build_dependency_files_.begin(),
-                                       build_dependency_files_.end());
+  dest->AddBuildDependencyFiles(build_dependency_files_);
 
   return true;
 }
@@ -540,6 +539,12 @@
   build_dependency_files_.insert(build_dependency_file);
 }
 
+void Scope::AddBuildDependencyFiles(
+    const SourceFileSet& build_dependency_files) {
+  build_dependency_files_.insert(build_dependency_files.begin(),
+                                 build_dependency_files.end());
+}
+
 Scope::ItemVector* Scope::GetItemCollector() {
   if (item_collector_)
     return item_collector_;
diff --git a/src/gn/scope.h b/src/gn/scope.h
index c5dcd38..e800621 100644
--- a/src/gn/scope.h
+++ b/src/gn/scope.h
@@ -293,6 +293,7 @@
     return build_dependency_files_;
   }
   void AddBuildDependencyFile(const SourceFile& build_dependency_file);
+  void AddBuildDependencyFiles(const SourceFileSet& build_dependency_files);
 
   // The item collector is where Items (Targets, Configs, etc.) go that have
   // been defined. If a scope can generate items, this non-owning pointer will
diff --git a/src/gn/template.cc b/src/gn/template.cc
index 3a65122..d5b2ecd 100644
--- a/src/gn/template.cc
+++ b/src/gn/template.cc
@@ -64,6 +64,11 @@
   Scope template_scope(closure_.get());
   template_scope.set_source_dir(scope->GetSourceDir());
 
+  // Propogate build dependency files from invoker scope (template scope already
+  // propagated via parent scope).
+  template_scope.AddBuildDependencyFiles(
+      invocation_scope->build_dependency_files());
+
   ScopePerFileProvider per_file_provider(&template_scope, true);
 
   // Targets defined in the template go in the collector for the invoking file.