Enable Ninja parser to parse validation inputs.

RELNOTES: None
PiperOrigin-RevId: 320089902
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/lexer/NinjaLexer.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/lexer/NinjaLexer.java
index fe57c2b..fb77ba0 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/lexer/NinjaLexer.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/lexer/NinjaLexer.java
@@ -115,6 +115,9 @@
           if (step.tryReadDoublePipe()) {
             return push(NinjaToken.PIPE2);
           }
+          if (step.tryReadPipeAt()) {
+            return push(NinjaToken.PIPE_AT);
+          }
           return push(NinjaToken.PIPE);
         case '$':
           if (step.trySkipEscapedNewline()) {
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/lexer/NinjaLexerStep.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/lexer/NinjaLexerStep.java
index 025a517..a697448 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/lexer/NinjaLexerStep.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/lexer/NinjaLexerStep.java
@@ -246,6 +246,15 @@
     return false;
   }
 
+  public boolean tryReadPipeAt() {
+    Preconditions.checkState('|' == fragment.byteAt(position));
+    if (checkForward(1, '@')) {
+      end = position + 2;
+      return true;
+    }
+    return false;
+  }
+
   public void readText() {
     int i = position;
     for (; i < fragment.length(); i++) {
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/lexer/NinjaToken.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/lexer/NinjaToken.java
index d068595..b812bef 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/lexer/NinjaToken.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/lexer/NinjaToken.java
@@ -36,6 +36,7 @@
   EQUALS("="),
   PIPE("|"),
   PIPE2("||"),
+  PIPE_AT("|@"),
 
   INDENT("indent"),
   NEWLINE("newline"),
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 a1349ed..c23bed9 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
@@ -135,6 +135,7 @@
       case NEWLINE:
       case PIPE:
       case PIPE2:
+      case PIPE_AT:
       case TEXT:
       case VARIABLE:
         throw new UnsupportedOperationException(token.name() + UNSUPPORTED_TOKEN_MESSAGE);
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/parser/NinjaParserStep.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/parser/NinjaParserStep.java
index e7ad10e..3913748 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/parser/NinjaParserStep.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/parser/NinjaParserStep.java
@@ -233,11 +233,13 @@
   }
 
   private enum NinjaTargetParsingPart {
+
     OUTPUTS(OutputKind.EXPLICIT, true),
     IMPLICIT_OUTPUTS(OutputKind.IMPLICIT, true),
     INPUTS(InputKind.EXPLICIT, false),
     IMPLICIT_INPUTS(InputKind.IMPLICIT, false),
     ORDER_ONLY_INPUTS(InputKind.ORDER_ONLY, false),
+    VALIDATION_INPUTS(InputKind.VALIDATION, false),
     RULE_NAME(null, false),
     VARIABLES(null, false);
 
@@ -265,24 +267,42 @@
   private static final ImmutableSortedMap<
           NinjaTargetParsingPart, ImmutableSortedMap<NinjaToken, NinjaTargetParsingPart>>
       TARGET_PARTS_TRANSITIONS_MAP =
-          ImmutableSortedMap.of(
-              NinjaTargetParsingPart.OUTPUTS,
+          ImmutableSortedMap
+              .<NinjaTargetParsingPart, ImmutableSortedMap<NinjaToken, NinjaTargetParsingPart>>
+                  naturalOrder()
+              .put(
+                  NinjaTargetParsingPart.OUTPUTS,
                   ImmutableSortedMap.of(
                       NinjaToken.PIPE, NinjaTargetParsingPart.IMPLICIT_OUTPUTS,
-                      NinjaToken.COLON, NinjaTargetParsingPart.RULE_NAME),
-              NinjaTargetParsingPart.IMPLICIT_OUTPUTS,
-                  ImmutableSortedMap.of(NinjaToken.COLON, NinjaTargetParsingPart.RULE_NAME),
-              NinjaTargetParsingPart.INPUTS,
+                      NinjaToken.COLON, NinjaTargetParsingPart.RULE_NAME))
+              .put(
+                  NinjaTargetParsingPart.IMPLICIT_OUTPUTS,
+                  ImmutableSortedMap.of(NinjaToken.COLON, NinjaTargetParsingPart.RULE_NAME))
+              // Because there is no specific token separating the rule name from the inputs
+              // (besides a space), there is no entry for transitioning to INPUTS, and transitioning
+              // is instead handled in parseTargetDependenciesPart().
+              .put(
+                  NinjaTargetParsingPart.INPUTS,
                   ImmutableSortedMap.of(
                       NinjaToken.PIPE, NinjaTargetParsingPart.IMPLICIT_INPUTS,
                       NinjaToken.PIPE2, NinjaTargetParsingPart.ORDER_ONLY_INPUTS,
-                      NinjaToken.NEWLINE, NinjaTargetParsingPart.VARIABLES),
-              NinjaTargetParsingPart.IMPLICIT_INPUTS,
+                      NinjaToken.PIPE_AT, NinjaTargetParsingPart.VALIDATION_INPUTS,
+                      NinjaToken.NEWLINE, NinjaTargetParsingPart.VARIABLES))
+              .put(
+                  NinjaTargetParsingPart.IMPLICIT_INPUTS,
                   ImmutableSortedMap.of(
                       NinjaToken.PIPE2, NinjaTargetParsingPart.ORDER_ONLY_INPUTS,
-                      NinjaToken.NEWLINE, NinjaTargetParsingPart.VARIABLES),
-              NinjaTargetParsingPart.ORDER_ONLY_INPUTS,
-                  ImmutableSortedMap.of(NinjaToken.NEWLINE, NinjaTargetParsingPart.VARIABLES));
+                      NinjaToken.PIPE_AT, NinjaTargetParsingPart.VALIDATION_INPUTS,
+                      NinjaToken.NEWLINE, NinjaTargetParsingPart.VARIABLES))
+              .put(
+                  NinjaTargetParsingPart.ORDER_ONLY_INPUTS,
+                  ImmutableSortedMap.of(
+                      NinjaToken.PIPE_AT, NinjaTargetParsingPart.VALIDATION_INPUTS,
+                      NinjaToken.NEWLINE, NinjaTargetParsingPart.VARIABLES))
+              .put(
+                  NinjaTargetParsingPart.VALIDATION_INPUTS,
+                  ImmutableSortedMap.of(NinjaToken.NEWLINE, NinjaTargetParsingPart.VARIABLES))
+              .build();
 
   /**
    * Parses Ninja target using {@link NinjaScope} of the file, where it is defined, to expand
@@ -362,29 +382,36 @@
    */
   private Map<InputOutputKind, List<NinjaVariableValue>> parseTargetDependenciesPart(
       NinjaTarget.Builder builder) throws GenericParsingException {
+
     Map<InputOutputKind, List<NinjaVariableValue>> pathValuesMap = Maps.newHashMap();
     boolean ruleNameParsed = false;
     NinjaTargetParsingPart parsingPart = NinjaTargetParsingPart.OUTPUTS;
+
     while (lexer.hasNextToken() && !NinjaTargetParsingPart.VARIABLES.equals(parsingPart)) {
+
       if (NinjaTargetParsingPart.RULE_NAME.equals(parsingPart)) {
         ruleNameParsed = true;
         builder.setRuleName(asString(parseExpected(NinjaToken.IDENTIFIER)));
         parsingPart = NinjaTargetParsingPart.INPUTS;
         continue;
       }
+
       List<NinjaVariableValue> paths = parsePaths();
       if (paths.isEmpty() && !NinjaTargetParsingPart.INPUTS.equals(parsingPart)) {
         throw new GenericParsingException("Expected paths sequence");
       }
+
       if (!paths.isEmpty()) {
         pathValuesMap.put(Preconditions.checkNotNull(parsingPart.getInputOutputKind()), paths);
       }
+
       if (!lexer.hasNextToken()) {
         if (parsingPart.isTransitionRequired()) {
           throw new GenericParsingException("Unexpected end of target");
         }
         break;
       }
+
       NinjaToken lexicalSeparator = lexer.nextToken();
       parsingPart =
           Preconditions.checkNotNull(TARGET_PARTS_TRANSITIONS_MAP.get(parsingPart))
@@ -394,11 +421,13 @@
         throw new GenericParsingException("Unexpected token: " + lexicalSeparator);
       }
     }
+
     if (!ruleNameParsed) {
       throw new GenericParsingException("Expected rule name");
     }
     Preconditions.checkState(
         !lexer.hasNextToken() || NinjaTargetParsingPart.VARIABLES.equals(parsingPart));
+
     return pathValuesMap;
   }
 
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 b8a3a27..c667954 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
@@ -165,7 +165,8 @@
   public enum InputKind implements InputOutputKind {
     EXPLICIT,
     IMPLICIT,
-    ORDER_ONLY
+    ORDER_ONLY,
+    VALIDATION,
   }
 
   /** Enum with possible kinds of outputs. */
@@ -242,6 +243,10 @@
     return inputs.get(InputKind.ORDER_ONLY);
   }
 
+  public Collection<PathFragment> getValidationInputs() {
+    return inputs.get(InputKind.VALIDATION);
+  }
+
   public long getOffset() {
     return offset;
   }
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 ab3d232..2ec7c33 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
@@ -327,7 +327,7 @@
     assertThat(target.getExplicitInputs()).containsExactly(PathFragment.create("input"));
 
     NinjaTarget target1 =
-        createParser("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 |@ vi1 vi2")
             .parseNinjaTarget(scope, LINE_NUM_AFTER_RULE_DEFS);
     assertThat(target1.getRuleName()).isEqualTo("command");
     assertThat(target1.getOutputs())
@@ -340,6 +340,8 @@
         .containsExactly(PathFragment.create("ii1"), PathFragment.create("ii2"));
     assertThat(target1.getOrderOnlyInputs())
         .containsExactly(PathFragment.create("ooi1"), PathFragment.create("ooi2"));
+    assertThat(target1.getValidationInputs())
+        .containsExactly(PathFragment.create("vi1"), PathFragment.create("vi2"));
 
     NinjaTarget target2 =
         createParser("build output: phony").parseNinjaTarget(scope, LINE_NUM_AFTER_RULE_DEFS);
@@ -362,6 +364,7 @@
     testNinjaTargetParsingError("build xxx || yyy: command", "Unexpected token: PIPE2");
     testNinjaTargetParsingError("build xxx: command :", "Unexpected token: COLON");
     testNinjaTargetParsingError("build xxx: command | || a", "Expected paths sequence");
+    testNinjaTargetParsingError("build xxx: command | |@ a", "Expected paths sequence");
   }
 
   @Test