Add `validations` dependency type to targets

This introduces a new dependency type called validations. Validations
are targets that must be built if the depending target is built, but
which do not affect the result of the current target (unlike deps) and
are not runtime-only dependencies (unlike data_deps).

This is primarily useful for declaring tasks like static analysis, style
checking, or other linters that should run alongside the build.

Bug: 478798763
Change-Id: I7dff3394aaae9f70a7ceb93ffd661803409c3d7c
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/20860
Reviewed-by: Takuto Ikuta <tikuta@google.com>
Commit-Queue: Neri Marschik <nerima@google.com>
diff --git a/docs/reference.md b/docs/reference.md
index 7e848c7..a2231e6 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -165,6 +165,7 @@
     *   [target_xcode_platform: [string] The desired platform for the build.](#var_target_xcode_platform)
     *   [testonly: [boolean] Declares a target must only be used for testing.](#var_testonly)
     *   [transparent: [bool] True if the bundle is transparent.](#var_transparent)
+    *   [validations: [label list] Validation dependencies.](#var_validations)
     *   [visibility: [label list] A list of labels that can depend on a target.](#var_visibility)
     *   [walk_keys: [string list] Key(s) for managing the metadata collection walk.](#var_walk_keys)
     *   [weak_frameworks: [name list] Name of frameworks that must be weak linked.](#var_weak_frameworks)
@@ -6966,6 +6967,31 @@
   depends on it (unless the "bundle_data" target sets "product_type" to the
   same value as the "create_bundle" target).
 ```
+### <a name="var_validations"></a>**validations**: Validation dependencies.&nbsp;[Back to Top](#gn-reference)
+
+```
+  A list of target labels.
+
+  "Validations" are a list of targets that should be built if the current
+  target is built, but which do not effect the result of the current target.
+  This is used to declare things like static analysis, style checking, or
+  other checks that should run in parallel with the build.
+```
+
+#### **Example**
+
+```
+  executable("my_program") {
+    sources = [ "my_program.cc" ]
+    validations = [ ":my_program_style_check" ]
+  }
+
+  action("my_program_style_check") {
+    script = "//tools/style_checker.py"
+    sources = [ "my_program.cc" ]
+    outputs = [ "$target_gen_dir/my_program_style_check.stamp" ]
+  }
+```
 ### <a name="var_visibility"></a>**visibility**: A list of labels that can depend on a target.&nbsp;[Back to Top](#gn-reference)
 
 ```
diff --git a/src/gn/builder.cc b/src/gn/builder.cc
index 3710886..84eff84 100644
--- a/src/gn/builder.cc
+++ b/src/gn/builder.cc
@@ -89,6 +89,19 @@
 
   record->set_item(std::move(item));
 
+  // Notify anyone waiting on this item's definition.
+  const BuilderRecordSet& waiting_deps = record->waiting_on_definition();
+  for (auto it = waiting_deps.begin(); it.valid(); ++it) {
+    BuilderRecord* waiting = *it;
+    if (waiting->OnDefinedDep(record)) {
+      if (!ResolveItem(waiting, &err)) {
+        g_scheduler->FailWithError(err);
+        return;
+      }
+    }
+  }
+  record->waiting_on_definition().clear();
+
   // Do target-specific dependency setup. This will also schedule dependency
   // loads for targets that are required.
   switch (type) {
@@ -257,7 +270,9 @@
       !AddDeps(record, target->all_dependent_configs(), err) ||
       !AddDeps(record, target->public_configs(), err) ||
       !AddGenDeps(record, target->gen_deps(), err) ||
-      !AddPoolDep(record, target, err) || !AddToolchainDep(record, target, err))
+      !AddPoolDep(record, target, err) ||
+      !AddToolchainDep(record, target, err) ||
+      !AddValidationDeps(record, target->validations(), err))
     return false;
 
   // All targets in the default toolchain get generated by default. We also
@@ -464,6 +479,19 @@
   return true;
 }
 
+bool Builder::AddValidationDeps(BuilderRecord* record,
+                                const LabelTargetVector& targets,
+                                Err* err) {
+  for (const auto& target : targets) {
+    BuilderRecord* dep_record = GetOrCreateRecordOfType(
+        target.label, target.origin, BuilderRecord::ITEM_TARGET, err);
+    if (!dep_record)
+      return false;
+    record->AddValidationDep(dep_record);
+  }
+  return true;
+}
+
 void Builder::RecursiveSetShouldGenerate(BuilderRecord* record, bool force) {
   if (!record->should_generate()) {
     // This function can encounter cycles because gen_deps aren't a DAG. Setting
@@ -501,6 +529,7 @@
     if (!ResolveDeps(&target->public_deps(), err) ||
         !ResolveDeps(&target->private_deps(), err) ||
         !ResolveDeps(&target->data_deps(), err) ||
+        !ResolveDeps(&target->validations(), err) ||
         !ResolveConfigs(&target->configs(), err) ||
         !ResolveConfigs(&target->all_dependent_configs(), err) ||
         !ResolveConfigs(&target->public_configs(), err) ||
diff --git a/src/gn/builder.h b/src/gn/builder.h
index 6776363..6578a89 100644
--- a/src/gn/builder.h
+++ b/src/gn/builder.h
@@ -103,6 +103,9 @@
                   Err* err);
   bool AddPoolDep(BuilderRecord* record, const Target* target, Err* err);
   bool AddToolchainDep(BuilderRecord* record, const Target* target, Err* err);
+  bool AddValidationDeps(BuilderRecord* record,
+                         const LabelTargetVector& targets,
+                         Err* err);
 
   // Given a target, sets the "should generate" bit and pushes it through the
   // dependency tree. Any time the bit it set, we ensure that the given item is
diff --git a/src/gn/builder_record.cc b/src/gn/builder_record.cc
index d1d0c53..f5af711 100644
--- a/src/gn/builder_record.cc
+++ b/src/gn/builder_record.cc
@@ -62,12 +62,32 @@
   return ITEM_UNKNOWN;
 }
 
+void BuilderRecord::AddDep(BuilderRecord* record) {
+  if (all_deps_.add(record) && !record->resolved()) {
+    unresolved_count_ += 1;
+    record->waiting_on_resolution_.add(this);
+  }
+}
+
 void BuilderRecord::AddGenDep(BuilderRecord* record) {
   // Records don't have to wait on resolution of their gen deps, since all they
   // need to do is propagate should_generate to them.
   all_deps_.insert(record);
 }
 
+void BuilderRecord::AddValidationDep(BuilderRecord* record) {
+  if (all_deps_.add(record) && !record->item()) {
+    unresolved_count_ += 1;
+    record->waiting_on_definition_.add(this);
+  }
+}
+
+bool BuilderRecord::OnDefinedDep(const BuilderRecord* dep) {
+  DCHECK(all_deps_.contains(const_cast<BuilderRecord*>(dep)));
+  DCHECK(unresolved_count_ > 0);
+  return --unresolved_count_ == 0;
+}
+
 bool BuilderRecord::OnResolvedDep(const BuilderRecord* dep) {
   DCHECK(all_deps_.contains(const_cast<BuilderRecord*>(dep)));
   DCHECK(unresolved_count_ > 0);
@@ -79,16 +99,11 @@
   std::vector<const BuilderRecord*> result;
   for (auto it = all_deps_.begin(); it.valid(); ++it) {
     BuilderRecord* dep = *it;
-    if (dep->waiting_on_resolution_.contains(const_cast<BuilderRecord*>(this)))
+    if (dep->waiting_on_resolution_.contains(
+            const_cast<BuilderRecord*>(this)) ||
+        dep->waiting_on_definition_.contains(const_cast<BuilderRecord*>(this)))
       result.push_back(dep);
   }
   std::sort(result.begin(), result.end(), LabelCompare);
   return result;
 }
-
-void BuilderRecord::AddDep(BuilderRecord* record) {
-  if (all_deps_.add(record) && !record->resolved()) {
-    unresolved_count_ += 1;
-    record->waiting_on_resolution_.add(this);
-  }
-}
diff --git a/src/gn/builder_record.h b/src/gn/builder_record.h
index c4dc867..06ff6c3 100644
--- a/src/gn/builder_record.h
+++ b/src/gn/builder_record.h
@@ -82,20 +82,34 @@
   // as a list sorted by label.
   std::vector<const BuilderRecord*> GetSortedUnresolvedDeps() const;
 
-  // Call this method to notify the record that its dependency |dep| was
-  // just resolved. This returns true to indicate that the current record
-  // should now be resolved.
-  bool OnResolvedDep(const BuilderRecord* dep);
+  // Records that are waiting on this one to be defined. This is used for
+  // "validations" dependencies which don't require the target to be fully
+  // resolved, only defined.
+  BuilderRecordSet& waiting_on_definition() { return waiting_on_definition_; }
+  const BuilderRecordSet& waiting_on_definition() const {
+    return waiting_on_definition_;
+  }
 
   // Records that are waiting on this one to be resolved. This is the other
-  // end of the "unresolved deps" arrow.
+  // end of the "unresolved deps" arrow for standard dependencies.
   BuilderRecordSet& waiting_on_resolution() { return waiting_on_resolution_; }
   const BuilderRecordSet& waiting_on_resolution() const {
     return waiting_on_resolution_;
   }
 
-  void AddGenDep(BuilderRecord* record);
   void AddDep(BuilderRecord* record);
+  void AddGenDep(BuilderRecord* record);
+  void AddValidationDep(BuilderRecord* record);
+
+  // Call this method to notify the record that its dependency |dep| was
+  // just defined. This returns true to indicate that the current record
+  // should now be resolved.
+  bool OnDefinedDep(const BuilderRecord* dep);
+
+  // Call this method to notify the record that its dependency |dep| was
+  // just resolved. This returns true to indicate that the current record
+  // should now be resolved.
+  bool OnResolvedDep(const BuilderRecord* dep);
 
   // Comparator function used to sort records from their label.
   static bool LabelCompare(const BuilderRecord* a, const BuilderRecord* b) {
@@ -113,6 +127,7 @@
   size_t unresolved_count_ = 0;
   BuilderRecordSet all_deps_;
   BuilderRecordSet waiting_on_resolution_;
+  BuilderRecordSet waiting_on_definition_;
 
   BuilderRecord(const BuilderRecord&) = delete;
   BuilderRecord& operator=(const BuilderRecord&) = delete;
diff --git a/src/gn/builder_unittest.cc b/src/gn/builder_unittest.cc
index accd714..da497c7 100644
--- a/src/gn/builder_unittest.cc
+++ b/src/gn/builder_unittest.cc
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 #include <algorithm>
+#include <memory>
 
 #include "gn/builder.h"
 #include "gn/config.h"
@@ -375,4 +376,59 @@
   EXPECT_TRUE(loader_->HasLoadedOne(SourceFile("//b/BUILD.gn")));
 }
 
+// Tests that "validations" dependencies behave correctly:
+// 1. They trigger the loading of the validated target's build file (simulating
+// cross-directory deps).
+// 2. The validator waits for the validation target to be DEFINED, not RESOLVED.
+// 3. This allows cycles (A validates B, B depends on A) to resolve without
+// error.
+TEST_F(BuilderTest, Validations) {
+  DefineToolchain();
+  SourceDir toolchain_dir = settings_.toolchain_label().dir();
+  std::string toolchain_name = settings_.toolchain_label().name();
+
+  Label a_label(SourceDir("//a/"), "a", toolchain_dir, toolchain_name);
+  Label b_label(SourceDir("//b/"), "b", toolchain_dir, toolchain_name);
+
+  // Define A with validatation B.
+  auto a = std::make_unique<Target>(&settings_, a_label);
+  Target* a_ptr = a.get();
+  a_ptr->set_output_type(Target::ACTION);
+  a_ptr->visibility().SetPublic();
+  a_ptr->validations().push_back(LabelTargetPair(b_label));
+  builder_.ItemDefined(std::move(a));
+
+  // Should have requested that B is loaded.
+  EXPECT_TRUE(loader_->HasLoadedOne(SourceFile("//b/BUILD.gn")));
+
+  // A should NOT be resolved yet (waiting for B definition).
+  BuilderRecord* a_record = builder_.GetRecord(a_label);
+  EXPECT_TRUE(a_record);
+  EXPECT_FALSE(a_record->resolved());
+
+  // Define B. B depends on A.
+  auto b = std::make_unique<Target>(&settings_, b_label);
+  Target* b_ptr = b.get();
+  b_ptr->set_output_type(Target::ACTION);
+  b_ptr->visibility().SetPublic();
+  b_ptr->private_deps().push_back(LabelTargetPair(a_label));
+  builder_.ItemDefined(std::move(b));
+
+  scheduler().Run();
+
+  // Now both should be resolved.
+  EXPECT_TRUE(a_record->resolved());
+  BuilderRecord* b_record = builder_.GetRecord(b_label);
+  EXPECT_TRUE(b_record->resolved());
+
+  // There should be no cycle.
+  Err err;
+  EXPECT_TRUE(builder_.CheckForBadItems(&err));
+  EXPECT_FALSE(err.has_error()) << "CheckForBadItems error: " << err.message();
+
+  // A should have B in its validations.
+  ASSERT_EQ(1u, a_ptr->validations().size());
+  EXPECT_EQ(b_ptr, a_ptr->validations()[0].ptr);
+}
+
 }  // namespace gn_builder_unittest
diff --git a/src/gn/ninja_action_target_writer.cc b/src/gn/ninja_action_target_writer.cc
index 0fe0c1b..3aa5686 100644
--- a/src/gn/ninja_action_target_writer.cc
+++ b/src/gn/ninja_action_target_writer.cc
@@ -108,7 +108,9 @@
       path_output_.WriteFiles(out_, order_only_deps);
     }
 
+    WriteValidations();
     out_ << std::endl;
+
     if (target_->action_values().has_depfile()) {
       WriteDepfile(SourceFile());
     }
@@ -235,6 +237,7 @@
       out_ << " ||";
       path_output_.WriteFiles(out_, order_only_deps);
     }
+    WriteValidations();
     out_ << std::endl;
 
     // Response files require a unique name be defined.
diff --git a/src/gn/ninja_action_target_writer_unittest.cc b/src/gn/ninja_action_target_writer_unittest.cc
index 8bcea93..fa2939e 100644
--- a/src/gn/ninja_action_target_writer_unittest.cc
+++ b/src/gn/ninja_action_target_writer_unittest.cc
@@ -653,3 +653,47 @@
 )";
   EXPECT_EQ(expected, out.str()) << expected << "--" << out.str();
 }
+
+// Tests that validation dependencies are correctly written for an action
+// target.
+TEST(NinjaActionTargetWriter, ActionWithValidations) {
+  Err err;
+  TestWithScope setup;
+
+  Target validation_target(setup.settings(), Label(SourceDir("//foo/"), "val"));
+  validation_target.set_output_type(Target::ACTION);
+  validation_target.visibility().SetPublic();
+  validation_target.action_values().set_script(SourceFile("//foo/script.py"));
+  validation_target.action_values().outputs() =
+      SubstitutionList::MakeForTest("//out/Debug/val.out");
+  validation_target.SetToolchain(setup.toolchain());
+  ASSERT_TRUE(validation_target.OnResolved(&err));
+
+  Target target(setup.settings(), Label(SourceDir("//foo/"), "bar"));
+  target.set_output_type(Target::ACTION);
+  target.action_values().set_script(SourceFile("//foo/script.py"));
+  target.action_values().outputs() =
+      SubstitutionList::MakeForTest("//out/Debug/foo.out");
+  target.validations().push_back(LabelTargetPair(&validation_target));
+  target.SetToolchain(setup.toolchain());
+  ASSERT_TRUE(target.OnResolved(&err)) << err.message();
+
+  setup.build_settings()->SetPythonPath(
+      base::FilePath(FILE_PATH_LITERAL("/usr/bin/python")));
+
+  std::ostringstream out;
+  NinjaActionTargetWriter writer(&target, out);
+  writer.Run();
+
+  const char expected[] =
+      "rule __foo_bar___rule\n"
+      "  command = /usr/bin/python ../../foo/script.py\n"
+      "  description = ACTION //foo:bar()\n"
+      "  restat = 1\n"
+      "\n"
+      "build foo.out: __foo_bar___rule | ../../foo/script.py |@ phony/foo/val\n"
+      "\n"
+      "build phony/foo/bar: phony foo.out |@ phony/foo/val\n";
+
+  EXPECT_EQ(expected, out.str());
+}
diff --git a/src/gn/ninja_binary_target_writer.cc b/src/gn/ninja_binary_target_writer.cc
index ac45e97..bf0da36 100644
--- a/src/gn/ninja_binary_target_writer.cc
+++ b/src/gn/ninja_binary_target_writer.cc
@@ -295,6 +295,7 @@
     out_ << " ||";
     path_output_.WriteFiles(out_, order_only_deps);
   }
+  WriteValidations();
   out_ << std::endl;
 
   if (!sources.empty() && can_write_source_info) {
diff --git a/src/gn/ninja_c_binary_target_writer.cc b/src/gn/ninja_c_binary_target_writer.cc
index c864bc0..04ea485 100644
--- a/src/gn/ninja_c_binary_target_writer.cc
+++ b/src/gn/ninja_c_binary_target_writer.cc
@@ -640,6 +640,8 @@
   // to listing them again.
   WriteOrderOnlyDependencies(classified_deps.non_linkable_deps);
 
+  WriteValidations();
+
   // End of the link "build" line.
   out_ << std::endl;
 
diff --git a/src/gn/ninja_copy_target_writer.cc b/src/gn/ninja_copy_target_writer.cc
index 47657c0..0952c6f 100644
--- a/src/gn/ninja_copy_target_writer.cc
+++ b/src/gn/ninja_copy_target_writer.cc
@@ -124,6 +124,7 @@
       path_output_.WriteFiles(out_, input_deps);
       path_output_.WriteFiles(out_, data_outs);
     }
+    WriteValidations();
     out_ << std::endl;
   }
 }
diff --git a/src/gn/ninja_copy_target_writer_unittest.cc b/src/gn/ninja_copy_target_writer_unittest.cc
index e094f80..da7da59 100644
--- a/src/gn/ninja_copy_target_writer_unittest.cc
+++ b/src/gn/ninja_copy_target_writer_unittest.cc
@@ -223,3 +223,38 @@
     EXPECT_EQ(ninja_outputs[2].value(), "phony/foo/bar");
   }
 }
+
+// Tests that validation dependencies are correctly written for a copy target.
+TEST(NinjaCopyTargetWriter, CopyWithValidations) {
+  Err err;
+  TestWithScope setup;
+
+  Target validation_target(setup.settings(), Label(SourceDir("//foo/"), "val"));
+  validation_target.set_output_type(Target::ACTION);
+  validation_target.visibility().SetPublic();
+  validation_target.action_values().set_script(SourceFile("//foo/script.py"));
+  validation_target.action_values().outputs() =
+      SubstitutionList::MakeForTest("//out/Debug/val.out");
+  validation_target.SetToolchain(setup.toolchain());
+  ASSERT_TRUE(validation_target.OnResolved(&err));
+
+  Target target(setup.settings(), Label(SourceDir("//foo/"), "bar"));
+  target.set_output_type(Target::COPY_FILES);
+  target.sources().push_back(SourceFile("//foo/input1.txt"));
+  target.action_values().outputs() =
+      SubstitutionList::MakeForTest("//out/Debug/{{source_name_part}}.out");
+  target.validations().push_back(LabelTargetPair(&validation_target));
+  target.SetToolchain(setup.toolchain());
+  ASSERT_TRUE(target.OnResolved(&err));
+
+  std::ostringstream out;
+  NinjaCopyTargetWriter writer(&target, out);
+  writer.Run();
+
+  const char expected_linux[] =
+      "build input1.out: copy ../../foo/input1.txt |@ phony/foo/val\n"
+      "\n"
+      "build phony/foo/bar: phony input1.out |@ phony/foo/val\n";
+  std::string out_str = out.str();
+  EXPECT_EQ(expected_linux, out_str);
+}
diff --git a/src/gn/ninja_target_writer.cc b/src/gn/ninja_target_writer.cc
index e5248a1..d6c69c7 100644
--- a/src/gn/ninja_target_writer.cc
+++ b/src/gn/ninja_target_writer.cc
@@ -627,5 +627,26 @@
     out_ << " ||";
     path_output_.WriteFiles(out_, order_only_deps);
   }
+  WriteValidations();
   out_ << std::endl;
 }
+
+void NinjaTargetWriter::WriteValidations() {
+  const LabelTargetVector& validations = target_->validations();
+  if (validations.empty())
+    return;
+
+  bool first = true;
+  for (const auto& pair : validations) {
+    // This check is needed because empty groups have no output.
+    if (!pair.ptr->has_dependency_output()) {
+      continue;
+    }
+    if (first) {
+      out_ << " |@";
+      first = false;
+    }
+    out_ << " ";
+    WriteOutput(pair.ptr->dependency_output());
+  }
+}
diff --git a/src/gn/ninja_target_writer.h b/src/gn/ninja_target_writer.h
index 68bc0a3..0936033 100644
--- a/src/gn/ninja_target_writer.h
+++ b/src/gn/ninja_target_writer.h
@@ -99,6 +99,9 @@
       const std::vector<OutputFile>& deps,
       const std::vector<OutputFile>& order_only_deps);
 
+  // Writes the validation dependencies to the output stream.
+  void WriteValidations();
+
   const Settings* settings_;  // Non-owning.
   const Target* target_;      // Non-owning.
   std::ostream& out_;
diff --git a/src/gn/ninja_target_writer_unittest.cc b/src/gn/ninja_target_writer_unittest.cc
index ae691d7..317ce2a 100644
--- a/src/gn/ninja_target_writer_unittest.cc
+++ b/src/gn/ninja_target_writer_unittest.cc
@@ -28,6 +28,12 @@
     return NinjaTargetWriter::WriteInputDepsStampOrPhonyAndGetDep(
         additional_hard_deps, num_stamp_uses);
   }
+
+  void WriteStampOrPhonyForTarget(
+      const std::vector<OutputFile>& deps,
+      const std::vector<OutputFile>& order_only_deps) {
+    NinjaTargetWriter::WriteStampOrPhonyForTarget(deps, order_only_deps);
+  }
 };
 
 }  // namespace
@@ -300,3 +306,87 @@
   EXPECT_EQ("phony/foo/setup", dep[0].value());
   EXPECT_EQ("", stream.str());
 }
+
+// Tests that validation dependencies are written to the generated Ninja file
+// with the "|@" syntax for a generic target.
+TEST(NinjaTargetWriter, WriteValidations) {
+  TestWithScope setup;
+  setup.build_settings()->set_no_stamp_files(false);
+  Err err;
+
+  Target validation_target(setup.settings(), Label(SourceDir("//foo/"), "val"));
+  validation_target.set_output_type(Target::ACTION);
+  validation_target.visibility().SetPublic();
+  validation_target.SetToolchain(setup.toolchain());
+  validation_target.action_values().set_script(SourceFile("//foo/script.py"));
+  ASSERT_TRUE(validation_target.OnResolved(&err));
+
+  Target target(setup.settings(), Label(SourceDir("//foo/"), "target"));
+  target.set_output_type(Target::GROUP);
+  target.visibility().SetPublic();
+  target.SetToolchain(setup.toolchain());
+  target.validations().push_back(LabelTargetPair(&validation_target));
+  ASSERT_TRUE(target.OnResolved(&err));
+
+  std::ostringstream stream;
+  TestingNinjaTargetWriter writer(&target, setup.toolchain(), stream);
+
+  std::vector<OutputFile> deps;
+  std::vector<OutputFile> order_only;
+  deps.push_back(OutputFile("obj/foo/target.stamp"));
+
+  writer.WriteStampOrPhonyForTarget(deps, order_only);
+
+  std::string out = stream.str();
+  EXPECT_EQ(
+      "build obj/foo/target.stamp: stamp obj/foo/target.stamp |@ "
+      "obj/foo/val.stamp\n",
+      out);
+}
+
+// Tests that if a validation target has no output (e.g., an empty group),
+// no validation dependency is written to the Ninja file.
+TEST(NinjaTargetWriter, ValidationsWithNoOutput) {
+  TestWithScope setup;
+  setup.build_settings()->set_no_stamp_files(true);
+  Err err;
+
+  // Validation target with no output (empty group, no stamp files).
+  Target validation_target(setup.settings(), Label(SourceDir("//foo/"), "val"));
+  validation_target.set_output_type(Target::GROUP);
+  validation_target.visibility().SetPublic();
+  validation_target.SetToolchain(setup.toolchain());
+  ASSERT_TRUE(validation_target.OnResolved(&err));
+  ASSERT_FALSE(validation_target.has_dependency_output());
+
+  // A dependency that HAS an output, so 'target' will have an output alias.
+  Target real_dep(setup.settings(), Label(SourceDir("//foo/"), "dep"));
+  real_dep.set_output_type(Target::ACTION);
+  real_dep.visibility().SetPublic();
+  real_dep.SetToolchain(setup.toolchain());
+  real_dep.action_values().set_script(SourceFile("//foo/script.py"));
+  ASSERT_TRUE(real_dep.OnResolved(&err));
+
+  Target target(setup.settings(), Label(SourceDir("//foo/"), "target"));
+  target.set_output_type(Target::GROUP);
+  target.visibility().SetPublic();
+  target.SetToolchain(setup.toolchain());
+  target.public_deps().push_back(LabelTargetPair(&real_dep));
+  target.validations().push_back(LabelTargetPair(&validation_target));
+  ASSERT_TRUE(target.OnResolved(&err));
+  ASSERT_TRUE(target.has_dependency_output());
+
+  std::ostringstream stream;
+  TestingNinjaTargetWriter writer(&target, setup.toolchain(), stream);
+
+  std::vector<OutputFile> deps;
+  std::vector<OutputFile> order_only;
+  deps.push_back(OutputFile("phony/foo/dep"));
+
+  writer.WriteStampOrPhonyForTarget(deps, order_only);
+
+  std::string out = stream.str();
+  // Should not contain validation separator since the validation target has no
+  // output.
+  EXPECT_EQ("build phony/foo/target: phony phony/foo/dep\n", out);
+}
diff --git a/src/gn/target.cc b/src/gn/target.cc
index 1ea1298..9cba6af 100644
--- a/src/gn/target.cc
+++ b/src/gn/target.cc
@@ -1158,6 +1158,10 @@
     if (!Visibility::CheckItemVisibility(this, pair.ptr, err))
       return false;
   }
+  for (const auto& pair : validations_) {
+    if (!Visibility::CheckItemVisibility(this, pair.ptr, err))
+      return false;
+  }
   return true;
 }
 
@@ -1195,6 +1199,14 @@
     }
   }
 
+  // Verify no validations have "testonly" set.
+  for (const auto& pair : validations_) {
+    if (pair.ptr->testonly()) {
+      *err = MakeTestOnlyError(this, pair.ptr);
+      return false;
+    }
+  }
+
   // Verify no configs have "testonly" set.
   for (ConfigValuesIterator iter(this); !iter.done(); iter.Next()) {
     if (const Config* config = iter.GetCurrentConfig()) {
diff --git a/src/gn/target.h b/src/gn/target.h
index c8c4933..ccd5372 100644
--- a/src/gn/target.h
+++ b/src/gn/target.h
@@ -261,6 +261,10 @@
   const LabelTargetVector& data_deps() const { return data_deps_; }
   LabelTargetVector& data_deps() { return data_deps_; }
 
+  // Validation dependencies.
+  const LabelTargetVector& validations() const { return validations_; }
+  LabelTargetVector& validations() { return validations_; }
+
   // gen_deps only propagate the "should_generate" flag. These dependencies can
   // have cycles so care should be taken if iterating over them recursively.
   const LabelTargetVector& gen_deps() const { return gen_deps_; }
@@ -522,6 +526,7 @@
   LabelTargetVector private_deps_;
   LabelTargetVector public_deps_;
   LabelTargetVector data_deps_;
+  LabelTargetVector validations_;
   LabelTargetVector gen_deps_;
 
   // See getters for more info.
diff --git a/src/gn/target_generator.cc b/src/gn/target_generator.cc
index 59bcf1a..7529b2e 100644
--- a/src/gn/target_generator.cc
+++ b/src/gn/target_generator.cc
@@ -260,6 +260,8 @@
     return false;
   if (!FillGenericDeps(variables::kDataDeps, &target_->data_deps()))
     return false;
+  if (!FillGenericDeps(variables::kValidations, &target_->validations()))
+    return false;
   if (!FillGenericDeps(variables::kGenDeps, &target_->gen_deps()))
     return false;
 
diff --git a/src/gn/variables.cc b/src/gn/variables.cc
index 4573279..5090e3a 100644
--- a/src/gn/variables.cc
+++ b/src/gn/variables.cc
@@ -2212,6 +2212,33 @@
   }
 )";
 
+const char kValidations[] = "validations";
+const char kValidations_HelpShort[] =
+    "validations: [label list] Validation dependencies.";
+const char kValidations_Help[] =
+    R"(validations: Validation dependencies.
+
+  A list of target labels.
+
+  "Validations" are a list of targets that should be built if the current
+  target is built, but which do not effect the result of the current target.
+  This is used to declare things like static analysis, style checking, or
+  other checks that should run in parallel with the build.
+
+Example
+
+  executable("my_program") {
+    sources = [ "my_program.cc" ]
+    validations = [ ":my_program_style_check" ]
+  }
+
+  action("my_program_style_check") {
+    script = "//tools/style_checker.py"
+    sources = [ "my_program.cc" ]
+    outputs = [ "$target_gen_dir/my_program_style_check.stamp" ]
+  }
+)";
+
 const char kVisibility[] = "visibility";
 const char kVisibility_HelpShort[] =
     "visibility: [label list] A list of labels that can depend on a target.";
@@ -2489,6 +2516,7 @@
     INSERT_VARIABLE(XcodeTestApplicationName)
     INSERT_VARIABLE(TargetXcodePlatform)
     INSERT_VARIABLE(Testonly)
+    INSERT_VARIABLE(Validations)
     INSERT_VARIABLE(Visibility)
     INSERT_VARIABLE(WalkKeys)
     INSERT_VARIABLE(WeakFrameworks)
diff --git a/src/gn/variables.h b/src/gn/variables.h
index 2d46db4..1d953af 100644
--- a/src/gn/variables.h
+++ b/src/gn/variables.h
@@ -338,6 +338,10 @@
 extern const char kTestonly_HelpShort[];
 extern const char kTestonly_Help[];
 
+extern const char kValidations[];
+extern const char kValidations_HelpShort[];
+extern const char kValidations_Help[];
+
 extern const char kVisibility[];
 extern const char kVisibility_HelpShort[];
 extern const char kVisibility_Help[];