Add fine grained detail to QueryException and Lexer.

Allow QueryException to take in a Query code from failure detail and construct a failure detail with its inner constructed message.

RELNOTES: None.
PiperOrigin-RevId: 319832572
diff --git a/src/main/java/com/google/devtools/build/lib/query2/engine/BUILD b/src/main/java/com/google/devtools/build/lib/query2/engine/BUILD
index 5f75194..3693efb 100644
--- a/src/main/java/com/google/devtools/build/lib/query2/engine/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/query2/engine/BUILD
@@ -18,6 +18,7 @@
         "//src/main/java/com/google/devtools/build/lib/graph",
         "//src/main/java/com/google/devtools/build/lib/packages",
         "//src/main/java/com/google/devtools/build/lib/profiler",
+        "//src/main/protobuf:failure_details_java_proto",
         "//third_party:guava",
         "//third_party:jsr305",
     ],
diff --git a/src/main/java/com/google/devtools/build/lib/query2/engine/LetExpression.java b/src/main/java/com/google/devtools/build/lib/query2/engine/LetExpression.java
index 7709ce7..92567bb 100644
--- a/src/main/java/com/google/devtools/build/lib/query2/engine/LetExpression.java
+++ b/src/main/java/com/google/devtools/build/lib/query2/engine/LetExpression.java
@@ -16,6 +16,7 @@
 import com.google.common.base.Function;
 import com.google.devtools.build.lib.query2.engine.QueryEnvironment.QueryTaskFuture;
 import com.google.devtools.build.lib.query2.engine.QueryEnvironment.ThreadSafeMutableSet;
+import com.google.devtools.build.lib.server.FailureDetails.Query;
 import java.util.Collection;
 import java.util.regex.Pattern;
 
@@ -72,7 +73,10 @@
       final Callback<T> callback) {
     if (!NAME_PATTERN.matcher(varName).matches()) {
       return env.immediateFailedFuture(
-          new QueryException(this, "invalid variable name '" + varName + "' in let expression"));
+          new QueryException(
+              this,
+              "invalid variable name '" + varName + "' in let expression",
+              Query.Code.VARIABLE_NAME_INVALID));
     }
     QueryTaskFuture<ThreadSafeMutableSet<T>> varValueFuture =
         QueryUtil.evalAll(env, context, varExpr);
diff --git a/src/main/java/com/google/devtools/build/lib/query2/engine/Lexer.java b/src/main/java/com/google/devtools/build/lib/query2/engine/Lexer.java
index 60ae900..c25777c 100644
--- a/src/main/java/com/google/devtools/build/lib/query2/engine/Lexer.java
+++ b/src/main/java/com/google/devtools/build/lib/query2/engine/Lexer.java
@@ -13,6 +13,7 @@
 // limitations under the License.
 package com.google.devtools.build.lib.query2.engine;
 
+import com.google.devtools.build.lib.server.FailureDetails.Query;
 import java.util.ArrayList;
 import java.util.EnumSet;
 import java.util.HashMap;
@@ -158,7 +159,7 @@
           }
       }
     }
-    throw new QueryException("unclosed quotation");
+    throw new QueryException("unclosed quotation", Query.Code.UNCLOSED_QUOTATION_EXPRESSION_ERROR);
   }
 
   private TokenKind getTokenKindForWord(String word) {
diff --git a/src/main/java/com/google/devtools/build/lib/query2/engine/QueryException.java b/src/main/java/com/google/devtools/build/lib/query2/engine/QueryException.java
index 8a9a5df..3d92ba3 100644
--- a/src/main/java/com/google/devtools/build/lib/query2/engine/QueryException.java
+++ b/src/main/java/com/google/devtools/build/lib/query2/engine/QueryException.java
@@ -13,9 +13,14 @@
 // limitations under the License.
 package com.google.devtools.build.lib.query2.engine;
 
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
+import com.google.devtools.build.lib.server.FailureDetails.Query;
+import java.util.Optional;
+
 /**
  */
 public class QueryException extends Exception {
+  private final Optional<FailureDetail> failureDetail;
 
   /**
    * Returns a better error message for the query.
@@ -36,17 +41,34 @@
   public QueryException(QueryException e, QueryExpression toplevel) {
     super(describeFailedQuery(e, toplevel), e);
     this.expression = null;
+    this.failureDetail = Optional.empty();
   }
 
   public QueryException(QueryExpression expression, String message) {
     super(message);
     this.expression = expression;
+    this.failureDetail = Optional.empty();
+  }
+
+  public QueryException(QueryExpression expression, String message, Query.Code queryCode) {
+    super(message);
+    this.expression = expression;
+    this.failureDetail =
+        Optional.of(
+            FailureDetail.newBuilder()
+                .setMessage(message)
+                .setQuery(Query.newBuilder().setCode(queryCode).build())
+                .build());
   }
 
   public QueryException(String message) {
     this(null, message);
   }
 
+  public QueryException(String message, Query.Code queryCode) {
+    this(null, message, queryCode);
+  }
+
   /**
    * Returns the subexpression for which evaluation failed, or null if
    * the failure occurred during lexing/parsing.
@@ -55,4 +77,8 @@
     return expression;
   }
 
+  /** Returns an optional {@link FailureDetail} containing fine grained detail code. */
+  public Optional<FailureDetail> getFailureDetail() {
+    return failureDetail;
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/QueryCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/QueryCommand.java
index 6713171..f39c973 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/commands/QueryCommand.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/QueryCommand.java
@@ -39,6 +39,7 @@
 import com.google.devtools.build.lib.server.FailureDetails.Interrupted;
 import com.google.devtools.build.lib.server.FailureDetails.Query;
 import com.google.devtools.build.lib.server.FailureDetails.Query.Code;
+import com.google.devtools.build.lib.util.DetailedExitCode;
 import com.google.devtools.build.lib.util.Either;
 import com.google.devtools.build.lib.util.ExitCode;
 import com.google.devtools.build.lib.util.InterruptedFailureDetails;
@@ -80,7 +81,11 @@
     } catch (QueryException e) {
       String message = "Error while parsing '" + query + "': " + e.getMessage();
       env.getReporter().handle(Event.error(null, message));
-      return Either.ofLeft(BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR));
+      return e.getFailureDetail().isPresent()
+          ? Either.ofLeft(
+              BlazeCommandResult.detailedExitCode(
+                  DetailedExitCode.of(ExitCode.COMMAND_LINE_ERROR, e.getFailureDetail().get())))
+          : Either.ofLeft(BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR));
     }
 
     try {
@@ -117,64 +122,60 @@
 
     QueryEvalResult result;
     boolean catastrophe = true;
-    try (SilentCloseable closeable = Profiler.instance().profile("queryEnv.evaluateQuery")) {
-      result = queryEnv.evaluateQuery(expr, callback);
-      catastrophe = false;
-    } catch (QueryException e) {
-      catastrophe = false;
-      // Keep consistent with reportBuildFileError()
-      env.getReporter()
-          // TODO(bazel-team): this is a kludge to fix a bug observed in the wild. We should make
-          // sure no null error messages ever get in.
-          .handle(Event.error(e.getMessage() == null ? e.toString() : e.getMessage()));
-      return Either.ofLeft(BlazeCommandResult.exitCode(ExitCode.ANALYSIS_FAILURE));
-    } catch (InterruptedException e) {
-      catastrophe = false;
-      IOException ioException = callback.getIoException();
-      if (ioException == null || ioException instanceof ClosedByInterruptException) {
-        return reportAndCreateInterruptedResult(env);
-      } else {
-        env.getReporter().handle(Event.error("I/O error: " + e.getMessage()));
-        return Either.ofLeft(BlazeCommandResult.exitCode(ExitCode.LOCAL_ENVIRONMENTAL_ERROR));
-      }
-    } catch (IOException e) {
-      catastrophe = false;
-      env.getReporter().handle(Event.error("I/O error: " + e.getMessage()));
-      return Either.ofLeft(BlazeCommandResult.exitCode(ExitCode.LOCAL_ENVIRONMENTAL_ERROR));
-    } finally {
-      if (!catastrophe) {
-        try {
-          out.flush();
-        } catch (IOException e) {
-          return reportAndCreateFlushFailureResult(env, e);
+    try {
+      try (SilentCloseable closeable = Profiler.instance().profile("queryEnv.evaluateQuery")) {
+        result = queryEnv.evaluateQuery(expr, callback);
+        catastrophe = false;
+      } catch (QueryException e) {
+        catastrophe = false;
+        // Keep consistent with reportBuildFileError()
+        env.getReporter()
+            // TODO(bazel-team): this is a kludge to fix a bug observed in the wild. We should make
+            // sure no null error messages ever get in.
+            .handle(Event.error(e.getMessage() == null ? e.toString() : e.getMessage()));
+        return Either.ofLeft(BlazeCommandResult.exitCode(ExitCode.ANALYSIS_FAILURE));
+      } catch (InterruptedException e) {
+        catastrophe = false;
+        IOException ioException = callback.getIoException();
+        if (ioException == null || ioException instanceof ClosedByInterruptException) {
+          return reportAndCreateInterruptedResult(env);
+        } else {
+          env.getReporter().handle(Event.error("I/O error: " + e.getMessage()));
+          return Either.ofLeft(BlazeCommandResult.exitCode(ExitCode.LOCAL_ENVIRONMENTAL_ERROR));
         }
-      }
-    }
-    if (!streamResults) {
-      disableAnsiCharactersFiltering(env);
-      try (SilentCloseable closeable = Profiler.instance().profile("QueryOutputUtils.output")) {
-        Set<Target> targets =
-            ((AggregateAllOutputFormatterCallback<Target, ?>) callback).getResult();
-        QueryOutputUtils.output(
-            queryOptions,
-            result,
-            targets,
-            formatter,
-            out,
-            queryOptions.aspectDeps.createResolver(env.getPackageManager(), env.getReporter()),
-            env.getReporter());
-      } catch (ClosedByInterruptException | InterruptedException e) {
-        return reportAndCreateInterruptedResult(env);
       } catch (IOException e) {
+        catastrophe = false;
         env.getReporter().handle(Event.error("I/O error: " + e.getMessage()));
         return Either.ofLeft(BlazeCommandResult.exitCode(ExitCode.LOCAL_ENVIRONMENTAL_ERROR));
       } finally {
-        try {
+        if (!catastrophe) {
           out.flush();
-        } catch (IOException e) {
-          return reportAndCreateFlushFailureResult(env, e);
         }
       }
+      if (!streamResults) {
+        disableAnsiCharactersFiltering(env);
+        try (SilentCloseable closeable = Profiler.instance().profile("QueryOutputUtils.output")) {
+          Set<Target> targets =
+              ((AggregateAllOutputFormatterCallback<Target, ?>) callback).getResult();
+          QueryOutputUtils.output(
+              queryOptions,
+              result,
+              targets,
+              formatter,
+              out,
+              queryOptions.aspectDeps.createResolver(env.getPackageManager(), env.getReporter()),
+              env.getReporter());
+        } catch (ClosedByInterruptException | InterruptedException e) {
+          return reportAndCreateInterruptedResult(env);
+        } catch (IOException e) {
+          env.getReporter().handle(Event.error("I/O error: " + e.getMessage()));
+          return Either.ofLeft(BlazeCommandResult.exitCode(ExitCode.LOCAL_ENVIRONMENTAL_ERROR));
+        } finally {
+          out.flush();
+        }
+      }
+    } catch (IOException e) {
+      return reportAndCreateFlushFailureResult(env, e);
     }
 
     return Either.ofRight(result);
diff --git a/src/main/protobuf/failure_details.proto b/src/main/protobuf/failure_details.proto
index 656be6a..1b59140 100644
--- a/src/main/protobuf/failure_details.proto
+++ b/src/main/protobuf/failure_details.proto
@@ -596,6 +596,8 @@
     QUERY_STDOUT_FLUSH_FAILURE = 13 [(metadata) = { exit_code: 36 }];
     ANALYSIS_QUERY_PREREQ_UNMET = 14 [(metadata) = { exit_code: 2 }];
     QUERY_RESULTS_FLUSH_FAILURE = 15 [(metadata) = { exit_code: 36 }];
+    UNCLOSED_QUOTATION_EXPRESSION_ERROR = 16 [(metadata) = { exit_code: 2 }];
+    VARIABLE_NAME_INVALID = 17 [(metadata) = { exit_code: 7 }];
 
     reserved 7 to 12; // For internal use
   }