Throw a parsing exception when ninja targets reference undefined rules

RELNOTES: None.
PiperOrigin-RevId: 309066431
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/parser/NinjaTarget.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/parser/NinjaTarget.java
index 6522d17..06b52b1 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/parser/NinjaTarget.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/parser/NinjaTarget.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSortedMap;
 import com.google.common.collect.Interner;
+import com.google.devtools.build.lib.bazel.rules.ninja.file.GenericParsingException;
 import com.google.devtools.build.lib.collect.ImmutableSortedKeyListMultimap;
 import com.google.devtools.build.lib.util.Pair;
 import com.google.devtools.build.lib.vfs.PathFragment;
@@ -80,7 +81,7 @@
       return this;
     }
 
-    public NinjaTarget build() {
+    public NinjaTarget build() throws GenericParsingException {
       Preconditions.checkNotNull(ruleName);
       String internedName = nameInterner.intern(ruleName);
       ImmutableSortedMap<String, String> variables = variablesBuilder.build();
@@ -91,8 +92,8 @@
       } else {
         NinjaRule ninjaRule = scope.findRule(offset, ruleName);
         if (ninjaRule == null) {
-          // TODO(cparsons): This indicates no rule was found, so throw a parse exception instead.
-          ruleVariables = ImmutableSortedMap.of();
+          throw new GenericParsingException(
+              String.format("could not resolve rule '%s'", internedName));
         } else {
           ruleVariables =
               reduceRuleVariables(scope, offset, ninjaRule.getVariables(), variables, nameInterner);
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/NinjaParserStepTest.java b/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/NinjaParserStepTest.java
index b53e313..8058ec9 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/NinjaParserStepTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/NinjaParserStepTest.java
@@ -46,6 +46,8 @@
 /** Tests for {@link NinjaParserStep}. */
 @RunWith(JUnit4.class)
 public class NinjaParserStepTest {
+  private static final int LINE_NUM_AFTER_RULE_DEFS = 5;
+
   @Test
   public void testSimpleVariable() throws Exception {
     doTestSimpleVariable("a=b", "a", "b");
@@ -313,15 +315,20 @@
 
   @Test
   public void testNinjaTargets() throws Exception {
+    NinjaScope scope = scopeWithStubRule("command");
+
     // Additionally test the situation when the target does not have the variables section and
     // we get more line separators in the end.
-    NinjaTarget target = parseNinjaTarget("build output: command input\n\n");
+    NinjaTarget target =
+        createParser("build output: command input\n\n")
+            .parseNinjaTarget(scope, LINE_NUM_AFTER_RULE_DEFS);
     assertThat(target.getRuleName()).isEqualTo("command");
     assertThat(target.getOutputs()).containsExactly(PathFragment.create("output"));
     assertThat(target.getUsualInputs()).containsExactly(PathFragment.create("input"));
 
     NinjaTarget target1 =
-        parseNinjaTarget("build o1 o2 | io1 io2: command i1 i2 | ii1 ii2 || ooi1 ooi2");
+        createParser("build o1 o2 | io1 io2: command i1 i2 | ii1 ii2 || ooi1 ooi2")
+            .parseNinjaTarget(scope, LINE_NUM_AFTER_RULE_DEFS);
     assertThat(target1.getRuleName()).isEqualTo("command");
     assertThat(target1.getOutputs())
         .containsExactly(PathFragment.create("o1"), PathFragment.create("o2"));
@@ -334,11 +341,14 @@
     assertThat(target1.getOrderOnlyInputs())
         .containsExactly(PathFragment.create("ooi1"), PathFragment.create("ooi2"));
 
-    NinjaTarget target2 = parseNinjaTarget("build output: phony");
+    NinjaTarget target2 =
+        createParser("build output: phony").parseNinjaTarget(scope, LINE_NUM_AFTER_RULE_DEFS);
     assertThat(target2.getRuleName()).isEqualTo("phony");
     assertThat(target2.getOutputs()).containsExactly(PathFragment.create("output"));
 
-    NinjaTarget target3 = parseNinjaTarget("build output: command $\n || order-only-input");
+    NinjaTarget target3 =
+        createParser("build output: command $\n || order-only-input")
+            .parseNinjaTarget(scope, LINE_NUM_AFTER_RULE_DEFS);
     assertThat(target3.getRuleName()).isEqualTo("command");
     assertThat(target3.getOutputs()).containsExactly(PathFragment.create("output"));
     assertThat(target3.getOrderOnlyInputs())
@@ -376,7 +386,7 @@
         createParser(
                 "build $output : testRule $input $dir/abcde\n"
                     + "  dir = def$input\n  empty = '$dir'")
-            .parseNinjaTarget(scope, 5);
+            .parseNinjaTarget(scope, LINE_NUM_AFTER_RULE_DEFS);
     assertThat(target.getRuleName()).isEqualTo("testRule");
     assertThat(target.getOutputs()).containsExactly(PathFragment.create("out123"));
     assertThat(target.getUsualInputs())
@@ -400,7 +410,12 @@
 
   @Test
   public void testNinjaTargetsPathWithEscapedSpace() throws Exception {
-    NinjaTarget target = parseNinjaTarget("build output : command input$ with$ space other");
+    NinjaScope scope = scopeWithStubRule("command");
+
+    NinjaTarget target =
+        createParser("build output : command input$ with$ space other")
+            .parseNinjaTarget(scope, LINE_NUM_AFTER_RULE_DEFS);
+
     assertThat(target.getRuleName()).isEqualTo("command");
     assertThat(target.getOutputs()).containsExactly(PathFragment.create("output"));
     assertThat(target.getUsualInputs())
@@ -409,9 +424,12 @@
 
   @Test
   public void testNinjaTargetsPathWithEscapedNewline() throws Exception {
+    NinjaScope scope = scopeWithStubRule("command");
+
     NinjaTarget target =
-        parseNinjaTarget(
-            "build $\n" + "  output : $\n" + "  command input$\n" + "  with$\n" + "  newline");
+        createParser(
+                "build $\n" + "  output : $\n" + "  command input$\n" + "  with$\n" + "  newline")
+            .parseNinjaTarget(scope, LINE_NUM_AFTER_RULE_DEFS);
     assertThat(target.getRuleName()).isEqualTo("command");
     assertThat(target.getOutputs()).containsExactly(PathFragment.create("output"));
     assertThat(target.getUsualInputs())
@@ -423,12 +441,36 @@
 
   @Test
   public void testNinjaTargetWithScope() throws Exception {
-    NinjaTarget target = parseNinjaTarget("build output : command input\n  pool = abc\n");
+    NinjaScope scope = scopeWithStubRule("command");
+
+    NinjaTarget target =
+        createParser("build output : command input\n  pool = abc\n")
+            .parseNinjaTarget(scope, LINE_NUM_AFTER_RULE_DEFS);
+
     assertThat(target.getRuleName()).isEqualTo("command");
     assertThat(target.getOutputs()).containsExactly(PathFragment.create("output"));
     assertThat(target.getUsualInputs()).containsExactly(PathFragment.create("input"));
   }
 
+  @Test
+  public void testUndefinedRuleForTarget() throws Exception {
+    GenericParsingException exception =
+        assertThrows(
+            GenericParsingException.class,
+            () -> parseNinjaTarget("build output : myUndefinedRule input\n  pool = abc\n"));
+
+    assertThat(exception).hasMessageThat().contains("could not resolve rule 'myUndefinedRule'");
+  }
+
+  private static NinjaScope scopeWithStubRule(String ruleName) throws Exception {
+    NinjaScope scope = new NinjaScope();
+
+    NinjaParserStep parser = createParser("rule " + ruleName + " \n");
+    NinjaRule ninjaRule = parser.parseNinjaRule();
+    scope.setRules(ImmutableSortedMap.of(ruleName, ImmutableList.of(Pair.of(0L, ninjaRule))));
+    return scope;
+  }
+
   private static void testNinjaTargetParsingError(String text, String error) {
     GenericParsingException exception =
         assertThrows(GenericParsingException.class, () -> parseNinjaTarget(text));
diff --git a/src/test/java/com/google/devtools/build/lib/blackbox/tests/NinjaBlackBoxTest.java b/src/test/java/com/google/devtools/build/lib/blackbox/tests/NinjaBlackBoxTest.java
index 32d5680..c427461 100644
--- a/src/test/java/com/google/devtools/build/lib/blackbox/tests/NinjaBlackBoxTest.java
+++ b/src/test/java/com/google/devtools/build/lib/blackbox/tests/NinjaBlackBoxTest.java
@@ -393,7 +393,7 @@
             "build group1: phony a b c",
             "build group2: phony d e",
             "build inputs_alias: phony group1 group2",
-            "build hello.txt: echo inputs_alias",
+            "build hello.txt: cat inputs_alias",
             "build alias: phony hello.txt");
 
     context()