Add an assert_no_deps variable to GN.

This asserts that there is no dependency path to a given target.

BUG=

Review URL: https://codereview.chromium.org/1621053002

Cr-Original-Commit-Position: refs/heads/master@{#371539}
Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src
Cr-Mirrored-Commit: 81aa4e8f010454835da6caa14507bcee73578e83
diff --git a/tools/gn/setup.cc b/tools/gn/setup.cc
index 459b87b..79814e6 100644
--- a/tools/gn/setup.cc
+++ b/tools/gn/setup.cc
@@ -31,6 +31,7 @@
 #include "tools/gn/tokenizer.h"
 #include "tools/gn/trace.h"
 #include "tools/gn/value.h"
+#include "tools/gn/value_extractors.h"
 
 #if defined(OS_WIN)
 #include <windows.h>
@@ -655,21 +656,13 @@
   const Value* check_targets_value =
       dotfile_scope_.GetValue("check_targets", true);
   if (check_targets_value) {
-    // Fill the list of targets to check.
-    if (!check_targets_value->VerifyTypeIs(Value::LIST, &err)) {
+    check_patterns_.reset(new std::vector<LabelPattern>);
+    ExtractListOfLabelPatterns(*check_targets_value, current_dir,
+                               check_patterns_.get(), &err);
+    if (err.has_error()) {
       err.PrintToStdout();
       return false;
     }
-
-    check_patterns_.reset(new std::vector<LabelPattern>);
-    for (const auto& item : check_targets_value->list_value()) {
-      check_patterns_->push_back(
-          LabelPattern::GetPattern(current_dir, item, &err));
-      if (err.has_error()) {
-        err.PrintToStdout();
-        return false;
-      }
-    }
   }
 
   // Fill exec_script_whitelist.
diff --git a/tools/gn/target.cc b/tools/gn/target.cc
index 905ee4f..7dd1921 100644
--- a/tools/gn/target.cc
+++ b/tools/gn/target.cc
@@ -129,6 +129,61 @@
   return false;
 }
 
+// check_this indicates if the given target should be matched against the
+// patterns. It should be set to false for the first call since assert_no_deps
+// shouldn't match the target itself.
+//
+// visited should point to an empty set, this will be used to prevent
+// multiple visits.
+//
+// *failure_path_str will be filled with a string describing the path of the
+// dependency failure, and failure_pattern will indicate the pattern in
+// assert_no that matched the target.
+//
+// Returns true if everything is OK. failure_path_str and failure_pattern_index
+// will be unchanged in this case.
+bool RecursiveCheckAssertNoDeps(const Target* target,
+                                bool check_this,
+                                const std::vector<LabelPattern>& assert_no,
+                                std::set<const Target*>* visited,
+                                std::string* failure_path_str,
+                                const LabelPattern** failure_pattern) {
+  static const char kIndentPath[] = "  ";
+
+  if (visited->find(target) != visited->end())
+    return true;  // Already checked this target.
+  visited->insert(target);
+
+  if (check_this) {
+    // Check this target against the given list of patterns.
+    for (const LabelPattern& pattern : assert_no) {
+      if (pattern.Matches(target->label())) {
+        // Found a match.
+        *failure_pattern = &pattern;
+        *failure_path_str =
+            kIndentPath + target->label().GetUserVisibleName(false);
+        return false;
+      }
+    }
+  }
+
+  // Recursively check dependencies.
+  for (const auto& pair : target->GetDeps(Target::DEPS_ALL)) {
+    if (pair.ptr->output_type() == Target::EXECUTABLE)
+      continue;
+    if (!RecursiveCheckAssertNoDeps(pair.ptr, true, assert_no, visited,
+                                    failure_path_str, failure_pattern)) {
+      // To reconstruct the path, prepend the current target to the error.
+      std::string prepend_path =
+          kIndentPath + target->label().GetUserVisibleName(false) + " ->\n";
+      failure_path_str->insert(0, prepend_path);
+      return false;
+    }
+  }
+
+  return true;
+}
+
 }  // namespace
 
 Target::Target(const Settings* settings, const Label& label)
@@ -234,6 +289,8 @@
       return false;
     if (!CheckNoNestedStaticLibs(err))
       return false;
+    if (!CheckAssertNoDeps(err))
+      return false;
     CheckSourcesGenerated();
   }
 
@@ -604,6 +661,27 @@
   return true;
 }
 
+bool Target::CheckAssertNoDeps(Err* err) const {
+  if (assert_no_deps_.empty())
+    return true;
+
+  std::set<const Target*> visited;
+  std::string failure_path_str;
+  const LabelPattern* failure_pattern = nullptr;
+
+  if (!RecursiveCheckAssertNoDeps(this, false, assert_no_deps_, &visited,
+                                  &failure_path_str, &failure_pattern)) {
+    *err = Err(defined_from(), "assert_no_deps failed.",
+        label().GetUserVisibleName(false) +
+        " has an assert_no_deps entry:\n  " +
+        failure_pattern->Describe() +
+        "\nwhich fails for the dependency path:\n" +
+        failure_path_str);
+    return false;
+  }
+  return true;
+}
+
 void Target::CheckSourcesGenerated() const {
   // Checks that any inputs or sources to this target that are in the build
   // directory are generated by a target that this one transitively depends on
diff --git a/tools/gn/target.h b/tools/gn/target.h
index cbdab35..18e5fb8 100644
--- a/tools/gn/target.h
+++ b/tools/gn/target.h
@@ -16,6 +16,7 @@
 #include "tools/gn/config_values.h"
 #include "tools/gn/inherited_libraries.h"
 #include "tools/gn/item.h"
+#include "tools/gn/label_pattern.h"
 #include "tools/gn/label_ptr.h"
 #include "tools/gn/lib_file.h"
 #include "tools/gn/ordered_set.h"
@@ -209,6 +210,13 @@
     return recursive_hard_deps_;
   }
 
+  std::vector<LabelPattern>& assert_no_deps() {
+    return assert_no_deps_;
+  }
+  const std::vector<LabelPattern>& assert_no_deps() const {
+    return assert_no_deps_;
+  }
+
   // The toolchain is only known once this target is resolved (all if its
   // dependencies are known). They will be null until then. Generally, this can
   // only be used during target writing.
@@ -284,6 +292,7 @@
   bool CheckVisibility(Err* err) const;
   bool CheckTestonly(Err* err) const;
   bool CheckNoNestedStaticLibs(Err* err) const;
+  bool CheckAssertNoDeps(Err* err) const;
   void CheckSourcesGenerated() const;
   void CheckSourceGenerated(const SourceFile& source) const;
 
@@ -324,6 +333,8 @@
   // target is marked resolved. This will not include the current target.
   std::set<const Target*> recursive_hard_deps_;
 
+  std::vector<LabelPattern> assert_no_deps_;
+
   // Used for all binary targets. The precompiled header values in this struct
   // will be resolved to the ones to use for this target, if precompiled
   // headers are used.
diff --git a/tools/gn/target_generator.cc b/tools/gn/target_generator.cc
index 3734ea9..b1ed1ea 100644
--- a/tools/gn/target_generator.cc
+++ b/tools/gn/target_generator.cc
@@ -50,6 +50,9 @@
   if (!FillTestonly())
     return;
 
+  if (!FillAssertNoDeps())
+    return;
+
   if (!Visibility::FillItemVisibility(target_, scope_, err_))
     return;
 
@@ -266,6 +269,15 @@
   return true;
 }
 
+bool TargetGenerator::FillAssertNoDeps() {
+  const Value* value = scope_->GetValue(variables::kAssertNoDeps, true);
+  if (value) {
+    return ExtractListOfLabelPatterns(*value, scope_->GetSourceDir(),
+                                      &target_->assert_no_deps(), err_);
+  }
+  return true;
+}
+
 bool TargetGenerator::FillOutputs(bool allow_substitutions) {
   const Value* value = scope_->GetValue(variables::kOutputs, true);
   if (!value)
diff --git a/tools/gn/target_generator.h b/tools/gn/target_generator.h
index 1fd055b..4fa51f6 100644
--- a/tools/gn/target_generator.h
+++ b/tools/gn/target_generator.h
@@ -70,6 +70,7 @@
   bool FillData();
   bool FillDependencies();  // Includes data dependencies.
   bool FillTestonly();
+  bool FillAssertNoDeps();
 
   // Reads configs/deps from the given var name, and uses the given setting on
   // the target to save them.
diff --git a/tools/gn/target_unittest.cc b/tools/gn/target_unittest.cc
index c59f44c..c99ebe8 100644
--- a/tools/gn/target_unittest.cc
+++ b/tools/gn/target_unittest.cc
@@ -668,3 +668,58 @@
       "  source: //pcs2.cc",
       err.help_text());
 }
+
+TEST(Target, AssertNoDeps) {
+  TestWithScope setup;
+  Err err;
+
+  // A target.
+  TestTarget a(setup, "//a", Target::SHARED_LIBRARY);
+  ASSERT_TRUE(a.OnResolved(&err));
+
+  // B depends on A and has an assert_no_deps for a random dir.
+  TestTarget b(setup, "//b", Target::SHARED_LIBRARY);
+  b.private_deps().push_back(LabelTargetPair(&a));
+  b.assert_no_deps().push_back(LabelPattern(
+      LabelPattern::RECURSIVE_DIRECTORY, SourceDir("//disallowed/"),
+      std::string(), Label()));
+  ASSERT_TRUE(b.OnResolved(&err));
+
+  LabelPattern disallow_a(LabelPattern::RECURSIVE_DIRECTORY, SourceDir("//a/"),
+                          std::string(), Label());
+
+  // C depends on B and disallows depending on A. This should fail.
+  TestTarget c(setup, "//c", Target::EXECUTABLE);
+  c.private_deps().push_back(LabelTargetPair(&b));
+  c.assert_no_deps().push_back(disallow_a);
+  ASSERT_FALSE(c.OnResolved(&err));
+
+  // Validate the error message has the proper path.
+  EXPECT_EQ(
+      "//c:c has an assert_no_deps entry:\n"
+      "  //a/*\n"
+      "which fails for the dependency path:\n"
+      "  //c:c ->\n"
+      "  //b:b ->\n"
+      "  //a:a",
+      err.help_text());
+  err = Err();
+
+  // Add an intermediate executable with: exe -> b -> a
+  TestTarget exe(setup, "//exe", Target::EXECUTABLE);
+  exe.private_deps().push_back(LabelTargetPair(&b));
+  ASSERT_TRUE(exe.OnResolved(&err));
+
+  // D depends on the executable and disallows depending on A. Since there is
+  // an intermediate executable, this should be OK.
+  TestTarget d(setup, "//d", Target::EXECUTABLE);
+  d.private_deps().push_back(LabelTargetPair(&exe));
+  d.assert_no_deps().push_back(disallow_a);
+  ASSERT_TRUE(d.OnResolved(&err));
+
+  // A2 disallows depending on anything in its own directory, but the
+  // assertions should not match the target itself so this should be OK.
+  TestTarget a2(setup, "//a:a2", Target::EXECUTABLE);
+  a2.assert_no_deps().push_back(disallow_a);
+  ASSERT_TRUE(a2.OnResolved(&err));
+}
diff --git a/tools/gn/value_extractors.cc b/tools/gn/value_extractors.cc
index 23c5a27..ff009ce 100644
--- a/tools/gn/value_extractors.cc
+++ b/tools/gn/value_extractors.cc
@@ -144,6 +144,17 @@
   const Label& current_toolchain;
 };
 
+struct LabelPatternResolver {
+  LabelPatternResolver(const SourceDir& current_dir_in)
+      : current_dir(current_dir_in) {
+  }
+  bool operator()(const Value& v, LabelPattern* out, Err* err) const {
+    *out = LabelPattern::GetPattern(current_dir, v, err);
+    return !err->has_error();
+  }
+  const SourceDir& current_dir;
+};
+
 }  // namespace
 
 bool ExtractListOfStringValues(const Value& value,
@@ -236,3 +247,11 @@
   RelativeFileConverter converter(build_settings, current_dir);
   return converter(value, file, err);
 }
+
+bool ExtractListOfLabelPatterns(const Value& value,
+                                const SourceDir& current_dir,
+                                std::vector<LabelPattern>* patterns,
+                                Err* err) {
+  return ListValueExtractor(value, patterns, err,
+                            LabelPatternResolver(current_dir));
+}
diff --git a/tools/gn/value_extractors.h b/tools/gn/value_extractors.h
index 06d64ce..1e42650 100644
--- a/tools/gn/value_extractors.h
+++ b/tools/gn/value_extractors.h
@@ -15,6 +15,7 @@
 class BuildSettings;
 class Err;
 class Label;
+class LabelPattern;
 class SourceDir;
 class SourceFile;
 class Value;
@@ -80,4 +81,9 @@
                          SourceFile* file,
                          Err* err);
 
+bool ExtractListOfLabelPatterns(const Value& value,
+                                const SourceDir& current_dir,
+                                std::vector<LabelPattern>* patterns,
+                                Err* err);
+
 #endif  // TOOLS_GN_VALUE_EXTRACTORS_H_
diff --git a/tools/gn/variables.cc b/tools/gn/variables.cc
index a835b22..c88a182 100644
--- a/tools/gn/variables.cc
+++ b/tools/gn/variables.cc
@@ -391,6 +391,49 @@
     "\n"
     "  See also \"gn help action\" and \"gn help action_foreach\".\n";
 
+const char kAssertNoDeps[] = "assert_no_deps";
+const char kAssertNoDeps_HelpShort[] =
+    "assert_no_deps: [label pattern list] Ensure no deps on these targets.";
+const char kAssertNoDeps_Help[] =
+    "assert_no_deps: Ensure no deps on these targets.\n"
+    "\n"
+    "  A list of label patterns.\n"
+    "\n"
+    "  This list is a list of patterns that must not match any of the\n"
+    "  transitive dependencies of the target. These include all public,\n"
+    "  private, and data dependencies, and cross shared library boundaries.\n"
+    "  This allows you to express that undesirable code isn't accidentally\n"
+    "  added to downstream dependencies in a way that might otherwise be\n"
+    "  difficult to notice.\n"
+    "\n"
+    "  Checking does not cross executable boundaries. If a target depends on\n"
+    "  an executable, it's assumed that the executable is a tool that is\n"
+    "  producing part of the build rather than something that is linked and\n"
+    "  distributed. This allows assert_no_deps to express what is distributed\n"
+    "  in the final target rather than depend on the internal build steps\n"
+    "  (which may include non-distributable code).\n"
+    "\n"
+    "  See \"gn help label_pattern\" for the format of the entries in the\n"
+    "  list. These patterns allow blacklisting individual targets or whole\n"
+    "  directory hierarchies.\n"
+    "\n"
+    "  Sometimes it is desirable to enforce that many targets have no\n"
+    "  dependencies on a target or set of targets. One efficient way to\n"
+    "  express this is to create a group with the assert_no_deps rule on\n"
+    "  it, and make that group depend on all targets you want to apply that\n"
+    "  assertion to.\n"
+    "\n"
+    "Example\n"
+    "\n"
+    "  executable(\"doom_melon\") {\n"
+    "    deps = [ \"//foo:bar\" ]\n"
+    "    ...\n"
+    "    assert_no_deps = [\n"
+    "      \"//evil/*\",  # Don't link any code from the evil directory.\n"
+    "      \"//foo:test_support\",  # This target is also disallowed.\n"
+    "    ]\n"
+    "  }\n";
+
 const char kCflags[] = "cflags";
 const char kCflags_HelpShort[] =
     "cflags: [string list] Flags passed to all C compiler variants.";
@@ -1400,6 +1443,7 @@
     INSERT_VARIABLE(AllowCircularIncludesFrom)
     INSERT_VARIABLE(Args)
     INSERT_VARIABLE(Asmflags)
+    INSERT_VARIABLE(AssertNoDeps)
     INSERT_VARIABLE(Cflags)
     INSERT_VARIABLE(CflagsC)
     INSERT_VARIABLE(CflagsCC)
diff --git a/tools/gn/variables.h b/tools/gn/variables.h
index d02d2c6..ee68bd5 100644
--- a/tools/gn/variables.h
+++ b/tools/gn/variables.h
@@ -87,6 +87,10 @@
 extern const char kAsmflags_HelpShort[];
 extern const char* kAsmflags_Help;
 
+extern const char kAssertNoDeps[];
+extern const char kAssertNoDeps_HelpShort[];
+extern const char kAssertNoDeps_Help[];
+
 extern const char kCflags[];
 extern const char kCflags_HelpShort[];
 extern const char* kCflags_Help;