Add validation support to gn analyze/desc/path/refs

Given target `foo` having validation `clippy`

desc output:
```
Target //:foo
type: group
toolchain: //toolchain:toolchain

...

Direct dependencies (try also "--all", "--tree", or even "--all --tree")
  //:foo.dep

validations
  //:clippy
```

path output:
```
//:foo --[validation]--> //:clippy
```

refs output:
```
//:foo
```

Bug: 478798763
Change-Id: Ica196b758411cf502e0fdf0c5a8566d5615f9711
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/20900
Reviewed-by: Takuto Ikuta <tikuta@google.com>
Commit-Queue: Neri Marschik <nerima@google.com>
diff --git a/build/gen.py b/build/gen.py
index e5e2d7e..29085c9 100755
--- a/build/gen.py
+++ b/build/gen.py
@@ -827,6 +827,7 @@
         'src/gn/compile_commands_writer_unittest.cc',
         'src/gn/config_unittest.cc',
         'src/gn/config_values_extractors_unittest.cc',
+        'src/gn/desc_builder_unittest.cc',
         'src/gn/escape_unittest.cc',
         'src/gn/exec_process_unittest.cc',
         'src/gn/filesystem_utils_unittest.cc',
diff --git a/docs/reference.md b/docs/reference.md
index a2231e6..6d951a5 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -569,6 +569,7 @@
   script
   sources
   testonly
+  validations
   visibility
   walk_keys
   weak_frameworks
@@ -1331,7 +1332,7 @@
       Restricts output to targets which refer to input files by a specific
       relation. Defaults to any relation. Can be provided multiple times to
       include multiple relations.
-
+    
 ```
 
 #### **Examples (target input)**
@@ -3465,7 +3466,7 @@
   hash = string_hash(long_string)
 
   `string_hash` returns a string that contains a hash of the argument.  The hash
-  is computed by first calculating a SHA256 hash of the argument, and then
+  is computed by first calculating the SHA256 hash of the argument, and then
   returning the first 8 characters of the lowercase-ASCII, hexadecimal encoding
   of the SHA256 hash.
 
@@ -8499,3 +8500,4 @@
     *   -v: Verbose logging.
     *   --version: Prints the GN version number and exits.
 ```
+
diff --git a/src/gn/analyzer.cc b/src/gn/analyzer.cc
index a4ee31f..5c1d5e0 100644
--- a/src/gn/analyzer.cc
+++ b/src/gn/analyzer.cc
@@ -249,6 +249,9 @@
            item->AsTarget()->GetDeps(Target::DEPS_ALL))
         dep_map_.insert(std::make_pair(dep_target_pair.ptr, item));
 
+      for (const auto& validation_target_pair : item->AsTarget()->validations())
+        dep_map_.insert(std::make_pair(validation_target_pair.ptr, item));
+
       for (const auto& dep_config_pair : item->AsTarget()->configs())
         dep_map_.insert(std::make_pair(dep_config_pair.ptr, item));
 
@@ -405,6 +408,8 @@
     } else {
       for (const auto& pair : target->GetDeps(Target::DEPS_ALL))
         FilterTarget(pair.ptr, seen, filtered);
+      for (const auto& pair : target->validations())
+        FilterTarget(pair.ptr, seen, filtered);
     }
   }
 }
diff --git a/src/gn/analyzer_unittest.cc b/src/gn/analyzer_unittest.cc
index 5952562..c5aa8d4 100644
--- a/src/gn/analyzer_unittest.cc
+++ b/src/gn/analyzer_unittest.cc
@@ -754,4 +754,47 @@
       "}");
 }
 
+// Tests that a target is marked as affected if its validations are modified.
+TEST_F(AnalyzerTest, TargetRefersToValidations) {
+  std::unique_ptr<Target> t = MakeTarget("//dir", "target_name");
+  Target* t_raw = t.get();
+  std::unique_ptr<Target> v = MakeTarget("//dir", "validation_name");
+  Target* v_raw = v.get();
+  v_raw->set_output_type(Target::ACTION);
+  v_raw->action_values().set_script(SourceFile("//dir/other.py"));
+
+  t_raw->validations().push_back(LabelTargetPair(v.get()));
+
+  builder_.ItemDefined(std::move(t));
+  builder_.ItemDefined(std::move(v));
+
+  // Initially no dependency.
+  RunAnalyzerTest(
+      R"({
+       "files": [ "//dir/script.py" ],
+       "additional_compile_targets": [ "//dir:target_name" ],
+       "test_targets": []
+       })",
+      "{"
+      R"("compile_targets":[],)"
+      R"/("status":"No dependency",)/"
+      R"("test_targets":[])"
+      "}");
+
+  // Now change validation target to use the script.
+  v_raw->action_values().set_script(SourceFile("//dir/script.py"));
+
+  RunAnalyzerTest(
+      R"({
+       "files": [ "//dir/script.py" ],
+       "additional_compile_targets": [ "//dir:target_name" ],
+       "test_targets": []
+       })",
+      "{"
+      R"("compile_targets":["//dir:target_name"],)"
+      R"/("status":"Found dependency",)/"
+      R"("test_targets":[])"
+      "}");
+}
+
 }  // namespace gn_analyzer_unittest
diff --git a/src/gn/command_desc.cc b/src/gn/command_desc.cc
index 491afa3..2e8f18a 100644
--- a/src/gn/command_desc.cc
+++ b/src/gn/command_desc.cc
@@ -266,6 +266,7 @@
 std::map<std::string, DescHandlerFunc> GetHandlers() {
   return {{"type", LabelHandler},
           {"toolchain", LabelHandler},
+          {variables::kValidations, DefaultHandler},
           {variables::kVisibility, VisibilityHandler},
           {variables::kMetadata, MetadataHandler},
           {variables::kTestonly, DefaultHandler},
@@ -397,6 +398,7 @@
   HandleProperty(variables::kPrecompiledHeader, handler_map, v, dict);
   HandleProperty(variables::kPrecompiledSource, handler_map, v, dict);
   HandleProperty(variables::kDeps, handler_map, v, dict);
+  HandleProperty(variables::kValidations, handler_map, v, dict);
   HandleProperty(variables::kLibs, handler_map, v, dict);
   HandleProperty(variables::kLibDirs, handler_map, v, dict);
   HandleProperty(variables::kDataKeys, handler_map, v, dict);
@@ -529,6 +531,7 @@
   script
   sources
   testonly
+  validations
   visibility
   walk_keys
   weak_frameworks
diff --git a/src/gn/command_path.cc b/src/gn/command_path.cc
index 4ee1345..e3efdd5 100644
--- a/src/gn/command_path.cc
+++ b/src/gn/command_path.cc
@@ -16,7 +16,7 @@
 
 namespace {
 
-enum class DepType { NONE, PUBLIC, PRIVATE, DATA };
+enum class DepType { NONE, PUBLIC, PRIVATE, DATA, VALIDATION };
 
 // The dependency paths are stored in a vector. Assuming the chain:
 //    A --[public]--> B --[private]--> C
@@ -30,6 +30,7 @@
 // How to search.
 enum class PrivateDeps { INCLUDE, EXCLUDE };
 enum class DataDeps { INCLUDE, EXCLUDE };
+enum class ValidationDeps { INCLUDE, EXCLUDE };
 enum class PrintWhat { ONE, ALL };
 
 struct Options {
@@ -74,6 +75,8 @@
         result = DepType::PRIVATE;
     } else if (path[i].second == DepType::DATA) {
       result = DepType::DATA;
+    } else if (path[i].second == DepType::VALIDATION) {
+      result = DepType::VALIDATION;
     }
   }
   return result;
@@ -87,7 +90,8 @@
       return "private";
     case DepType::DATA:
       return "data";
-      break;
+    case DepType::VALIDATION:
+      return "validation";
     case DepType::NONE:
     default:
       return "";
@@ -170,6 +174,7 @@
                         const Target* to,
                         PrivateDeps private_deps,
                         DataDeps data_deps,
+                        ValidationDeps validation_deps,
                         PrintWhat print_what,
                         Stats* stats) {
   // Seed the initial stack with just the "from" target.
@@ -244,6 +249,14 @@
         work_queue.back().push_back(TargetDep(pair.ptr, DepType::DATA));
       }
     }
+
+    if (validation_deps == ValidationDeps::INCLUDE) {
+      // Add validations.
+      for (const auto& pair : current_target->validations()) {
+        work_queue.push_back(current_path);
+        work_queue.back().push_back(TargetDep(pair.ptr, DepType::VALIDATION));
+      }
+    }
   }
 }
 
@@ -252,15 +265,15 @@
               const Options& options,
               Stats* stats) {
   BreadthFirstSearch(from, to, PrivateDeps::EXCLUDE, DataDeps::EXCLUDE,
-                     options.print_what, stats);
+                     ValidationDeps::EXCLUDE, options.print_what, stats);
   if (!options.public_only) {
-    // Check private deps.
+    // Check private deps and validations.
     BreadthFirstSearch(from, to, PrivateDeps::INCLUDE, DataDeps::EXCLUDE,
-                       options.print_what, stats);
+                       ValidationDeps::INCLUDE, options.print_what, stats);
     if (options.with_data) {
       // Check data deps.
       BreadthFirstSearch(from, to, PrivateDeps::INCLUDE, DataDeps::INCLUDE,
-                         options.print_what, stats);
+                         ValidationDeps::INCLUDE, options.print_what, stats);
     }
   }
 }
diff --git a/src/gn/command_refs.cc b/src/gn/command_refs.cc
index b5ad605..0e7770b 100644
--- a/src/gn/command_refs.cc
+++ b/src/gn/command_refs.cc
@@ -38,6 +38,8 @@
   for (auto* target : setup->builder().GetAllResolvedTargets()) {
     for (const auto& dep_pair : target->GetDeps(Target::DEPS_ALL))
       dep_map->insert(std::make_pair(dep_pair.ptr, target));
+    for (const auto& validation_pair : target->validations())
+      dep_map->insert(std::make_pair(validation_pair.ptr, target));
   }
 }
 
diff --git a/src/gn/desc_builder.cc b/src/gn/desc_builder.cc
index 1b80302..8c6d897 100644
--- a/src/gn/desc_builder.cc
+++ b/src/gn/desc_builder.cc
@@ -575,6 +575,10 @@
     if (what(variables::kGenDeps) && !target_->gen_deps().empty())
       res->SetWithoutPathExpansion(variables::kGenDeps, RenderGenDeps());
 
+    if (what(variables::kValidations) && !target_->validations().empty())
+      res->SetWithoutPathExpansion(variables::kValidations,
+                                   RenderValidations());
+
     // Runtime deps are special, print only when explicitly asked for and not in
     // overview mode.
     if (what_.find("runtime_deps") != what_.end())
@@ -743,6 +747,20 @@
     return res;
   }
 
+  ValuePtr RenderValidations() {
+    auto res = std::make_unique<base::ListValue>();
+    Label default_tc = target_->settings()->default_toolchain_label();
+    const auto& validation_pairs = target_->validations();
+    std::vector<std::string> validations;
+    validations.reserve(validation_pairs.size());
+    for (const auto& pair : validation_pairs)
+      validations.push_back(pair.label.GetUserVisibleName(default_tc));
+    std::sort(validations.begin(), validations.end());
+    for (const auto& dep : validations)
+      res->AppendString(dep);
+    return res;
+  }
+
   ValuePtr RenderRuntimeDeps() {
     auto res = std::make_unique<base::ListValue>();
 
diff --git a/src/gn/desc_builder_unittest.cc b/src/gn/desc_builder_unittest.cc
new file mode 100644
index 0000000..23f1772
--- /dev/null
+++ b/src/gn/desc_builder_unittest.cc
@@ -0,0 +1,38 @@
+// Copyright (c) 2026 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 "gn/desc_builder.h"
+
+#include "gn/test_with_scope.h"
+#include "util/test/test.h"
+
+TEST(DescBuilder, TargetWithValidations) {
+  TestWithScope setup;
+  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"));
+  validation_target.action_values().outputs() =
+      SubstitutionList::MakeForTest("//out/Debug/val.out");
+  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::unique_ptr<base::DictionaryValue> desc =
+      DescBuilder::DescriptionForTarget(&target, "", false, false, false);
+
+  base::Value* validations = desc->FindKey("validations");
+  ASSERT_TRUE(validations);
+  ASSERT_TRUE(validations->is_list());
+  ASSERT_EQ(1u, validations->GetList().size());
+  EXPECT_EQ("//foo:val()", validations->GetList()[0].GetString());
+}