Check included and subninja Ninja files against passed Artifacts paths

In the future ninja_graph rule, paths to all the included and subninja files should be passed in the srcs attribute.

Closes #10619.

PiperOrigin-RevId: 290733133
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/parser/NinjaParser.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/parser/NinjaParser.java
index d1b2980..38894e7 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/parser/NinjaParser.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/parser/NinjaParser.java
@@ -11,7 +11,6 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-//
 
 package com.google.devtools.build.lib.bazel.rules.ninja.parser;
 
@@ -35,10 +34,13 @@
 public class NinjaParser implements DeclarationConsumer {
   private final NinjaPipeline pipeline;
   private final NinjaFileParseResult parseResult;
+  private final String ninjaFileName;
 
-  public NinjaParser(NinjaPipeline pipeline, NinjaFileParseResult parseResult) {
+  public NinjaParser(
+      NinjaPipeline pipeline, NinjaFileParseResult parseResult, String ninjaFileName) {
     this.pipeline = pipeline;
     this.parseResult = parseResult;
+    this.ninjaFileName = ninjaFileName;
   }
 
   @Override
@@ -82,13 +84,15 @@
       case INCLUDE:
         NinjaVariableValue includeStatement = parser.parseIncludeStatement();
         NinjaPromise<NinjaFileParseResult> includeFuture =
-            pipeline.createChildFileParsingPromise(includeStatement, declarationStart);
+            pipeline.createChildFileParsingPromise(
+                includeStatement, declarationStart, ninjaFileName);
         parseResult.addIncludeScope(declarationStart, includeFuture);
         break;
       case SUBNINJA:
         NinjaVariableValue subNinjaStatement = parser.parseSubNinjaStatement();
         NinjaPromise<NinjaFileParseResult> subNinjaFuture =
-            pipeline.createChildFileParsingPromise(subNinjaStatement, declarationStart);
+            pipeline.createChildFileParsingPromise(
+                subNinjaStatement, declarationStart, ninjaFileName);
         parseResult.addSubNinjaScope(declarationStart, subNinjaFuture);
         break;
       case BUILD:
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/pipeline/NinjaPipeline.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/pipeline/NinjaPipeline.java
index 9a02523..d23a4cc 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/pipeline/NinjaPipeline.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/pipeline/NinjaPipeline.java
@@ -11,7 +11,6 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-//
 
 package com.google.devtools.build.lib.bazel.rules.ninja.pipeline;
 
@@ -41,6 +40,7 @@
 import java.io.IOException;
 import java.nio.channels.ReadableByteChannel;
 import java.util.ArrayDeque;
+import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 
@@ -53,14 +53,24 @@
 public class NinjaPipeline {
   private final Path basePath;
   private final ListeningExecutorService service;
+  private final Collection<Path> includedOrSubninjaFiles;
+  private final String ownerTargetName;
 
   /**
    * @param basePath base path for resolving include and subninja paths.
    * @param service service to use for scheduling tasks in parallel.
+   * @param includedOrSubninjaFiles Ninja files expected in include/subninja statements
+   * @param ownerTargetName name of the owner ninja_graph target
    */
-  public NinjaPipeline(Path basePath, ListeningExecutorService service) {
+  public NinjaPipeline(
+      Path basePath,
+      ListeningExecutorService service,
+      Collection<Path> includedOrSubninjaFiles,
+      String ownerTargetName) {
     this.basePath = basePath;
     this.service = service;
+    this.includedOrSubninjaFiles = includedOrSubninjaFiles;
+    this.ownerTargetName = ownerTargetName;
   }
 
   /**
@@ -125,11 +135,12 @@
    * parsing result in the parent file {@link NinjaFileParseResult} structure.
    */
   public NinjaPromise<NinjaFileParseResult> createChildFileParsingPromise(
-      NinjaVariableValue value, Integer offset) throws IOException {
+      NinjaVariableValue value, Integer offset, String parentNinjaFileName)
+      throws GenericParsingException, IOException {
     if (value.isPlainText()) {
       // If the value of the path is already known, we can immediately schedule parsing
       // of the child Ninja file.
-      Path path = basePath.getRelative(value.getRawText());
+      Path path = getChildNinjaPath(value.getRawText(), parentNinjaFileName);
       ListenableFuture<NinjaFileParseResult> parsingFuture = scheduleParsing(path);
       return (scope) ->
           waitForFutureAndGetWithCheckedException(
@@ -142,13 +153,25 @@
         if (expandedValue.isEmpty()) {
           throw new GenericParsingException("Expected non-empty path.");
         }
-        Path path = basePath.getRelative(expandedValue);
+        Path path = getChildNinjaPath(expandedValue, parentNinjaFileName);
         return waitForFutureAndGetWithCheckedException(
             scheduleParsing(path), GenericParsingException.class, IOException.class);
       };
     }
   }
 
+  private Path getChildNinjaPath(String rawText, String parentNinjaFileName)
+      throws GenericParsingException {
+    Path childPath = basePath.getRelative(rawText);
+    if (!this.includedOrSubninjaFiles.contains(childPath)) {
+      throw new GenericParsingException(
+          String.format(
+              "Ninja file requested from '%s' " + "not declared in 'srcs' attribute of '%s'.",
+              parentNinjaFileName, this.ownerTargetName));
+    }
+    return childPath;
+  }
+
   /**
    * Actually schedules the parsing of the Ninja file and returns {@link
    * ListenableFuture<NinjaFileParseResult>} for obtaining the result.
@@ -165,7 +188,7 @@
                 () -> {
                   NinjaFileParseResult parseResult = new NinjaFileParseResult();
                   pieces.add(parseResult);
-                  return new NinjaParser(NinjaPipeline.this, parseResult);
+                  return new NinjaParser(NinjaPipeline.this, parseResult, path.getBaseName());
                 },
                 service,
                 NinjaSeparatorFinder.INSTANCE);
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/NinjaPipelineTest.java b/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/NinjaPipelineTest.java
index 0e6bce1..897a0ea 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/NinjaPipelineTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/NinjaPipelineTest.java
@@ -11,16 +11,18 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-//
 
 package com.google.devtools.build.lib.bazel.rules.ninja;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows;
 import static junit.framework.TestCase.fail;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.google.devtools.build.lib.bazel.rules.ninja.file.GenericParsingException;
 import com.google.devtools.build.lib.bazel.rules.ninja.parser.NinjaTarget;
 import com.google.devtools.build.lib.bazel.rules.ninja.pipeline.NinjaPipeline;
 import com.google.devtools.build.lib.concurrent.ExecutorUtil;
@@ -107,7 +109,9 @@
             "  command = c $in $out",
             "build t1: r1 in1 in2",
             "build t2: r1 in3");
-    NinjaPipeline pipeline = new NinjaPipeline(vfsPath.getParentDirectory(), tester.getService());
+    NinjaPipeline pipeline =
+        new NinjaPipeline(
+            vfsPath.getParentDirectory(), tester.getService(), ImmutableList.of(), "ninja_target");
     List<NinjaTarget> targets = pipeline.pipeline(vfsPath).getSecond();
     checkTargets(targets);
   }
@@ -117,8 +121,13 @@
     Path vfsPath =
         tester.writeTmpFile(
             "test.ninja", "rule r1", "  command = c $in $out", "include child.ninja");
-    tester.writeTmpFile("child.ninja", "build t1: r1 in1 in2", "build t2: r1 in3");
-    NinjaPipeline pipeline = new NinjaPipeline(vfsPath.getParentDirectory(), tester.getService());
+    Path childFile = tester.writeTmpFile("child.ninja", "build t1: r1 in1 in2", "build t2: r1 in3");
+    NinjaPipeline pipeline =
+        new NinjaPipeline(
+            vfsPath.getParentDirectory(),
+            tester.getService(),
+            ImmutableList.of(childFile),
+            "ninja_target");
     List<NinjaTarget> targets = pipeline.pipeline(vfsPath).getSecond();
     checkTargets(targets);
   }
@@ -132,8 +141,13 @@
             "rule r1",
             "  command = c $in $out",
             "include ${subfile}.ninja");
-    tester.writeTmpFile("child.ninja", "build t1: r1 in1 in2", "build t2: r1 in3");
-    NinjaPipeline pipeline = new NinjaPipeline(vfsPath.getParentDirectory(), tester.getService());
+    Path childFile = tester.writeTmpFile("child.ninja", "build t1: r1 in1 in2", "build t2: r1 in3");
+    NinjaPipeline pipeline =
+        new NinjaPipeline(
+            vfsPath.getParentDirectory(),
+            tester.getService(),
+            ImmutableList.of(childFile),
+            "ninja_target");
     List<NinjaTarget> targets = pipeline.pipeline(vfsPath).getSecond();
     checkTargets(targets);
   }
@@ -149,10 +163,19 @@
             "  command = c $in $out",
             "include ${subfile}.ninja",
             "build t1: r1 ${top_scope_var} in2");
-    tester.writeTmpFile(
-        "child.ninja", "top_scope_var=in1", "var_for_sub=in3", "subninja ${subninja_file}.ninja");
-    tester.writeTmpFile("sub.ninja", "build t2: r1 ${var_for_sub}");
-    NinjaPipeline pipeline = new NinjaPipeline(vfsPath.getParentDirectory(), tester.getService());
+    Path childFile =
+        tester.writeTmpFile(
+            "child.ninja",
+            "top_scope_var=in1",
+            "var_for_sub=in3",
+            "subninja ${subninja_file}.ninja");
+    Path subFile = tester.writeTmpFile("sub.ninja", "build t2: r1 ${var_for_sub}");
+    NinjaPipeline pipeline =
+        new NinjaPipeline(
+            vfsPath.getParentDirectory(),
+            tester.getService(),
+            ImmutableList.of(childFile, subFile),
+            "ninja_target");
     List<NinjaTarget> targets = pipeline.pipeline(vfsPath).getSecond();
     checkTargets(targets);
   }
@@ -160,11 +183,33 @@
   @Test
   public void testEmptyFile() throws Exception {
     Path vfsPath = tester.writeTmpFile("test.ninja");
-    NinjaPipeline pipeline = new NinjaPipeline(vfsPath.getParentDirectory(), tester.getService());
+    NinjaPipeline pipeline =
+        new NinjaPipeline(
+            vfsPath.getParentDirectory(), tester.getService(), ImmutableList.of(), "ninja_target");
     List<NinjaTarget> targets = pipeline.pipeline(vfsPath).getSecond();
     assertThat(targets).isEmpty();
   }
 
+  @Test
+  public void testIncludedNinjaFileIsNotDeclared() throws Exception {
+    Path vfsPath = tester.writeTmpFile("test.ninja", "include subfile.ninja");
+    GenericParsingException exception =
+        assertThrows(
+            GenericParsingException.class,
+            () ->
+                new NinjaPipeline(
+                        vfsPath.getParentDirectory(),
+                        tester.getService(),
+                        ImmutableList.of(),
+                        "ninja_target")
+                    .pipeline(vfsPath));
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo(
+            "Ninja file requested from 'test.ninja' "
+                + "not declared in 'srcs' attribute of 'ninja_target'.");
+  }
+
   private static void checkTargets(List<NinjaTarget> targets) {
     assertThat(targets).hasSize(2);
     for (NinjaTarget target : targets) {