Internal build change.

PiperOrigin-RevId: 587885671
Change-Id: I426b7349412df400b0d2c149da5ff2dd47050e39
diff --git a/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/javac/plugins/BUILD b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/javac/plugins/BUILD
index ad488fe..be5c27f 100644
--- a/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/javac/plugins/BUILD
+++ b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/javac/plugins/BUILD
@@ -1,5 +1,5 @@
-load("@rules_java//java:defs.bzl", "java_library")
 load("//tools/build_rules:java_rules_skylark.bzl", "bootstrap_java_library")
+load("@rules_java//java:defs.bzl", "java_library")
 
 # Description:
 #   Plugins for the Java library builders, which are used by Bazel to
@@ -30,6 +30,7 @@
     javacopts = [
         "--add-exports=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED",
         "--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
+        "--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED",
         "--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED",
         "--add-exports=jdk.compiler/com.sun.tools.javac.resources=ALL-UNNAMED",
         "--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
diff --git a/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/javac/plugins/dependency/StrictJavaDepsPlugin.java b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/javac/plugins/dependency/StrictJavaDepsPlugin.java
index de1ad14..829c80e 100644
--- a/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/javac/plugins/dependency/StrictJavaDepsPlugin.java
+++ b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/javac/plugins/dependency/StrictJavaDepsPlugin.java
@@ -16,7 +16,12 @@
 
 import static com.google.common.collect.ImmutableSet.toImmutableSet;
 import static com.google.common.collect.Iterables.getOnlyElement;
+import static com.google.devtools.build.buildjar.javac.plugins.dependency.StrictJavaDepsPlugin.NonPlatformJar.Kind.FOR_JSPECIFY_FROM_PLATFORM;
+import static com.google.devtools.build.buildjar.javac.plugins.dependency.StrictJavaDepsPlugin.NonPlatformJar.Kind.IN_CLASSPATH;
+import static java.lang.Boolean.parseBoolean;
+import static javax.tools.StandardLocation.CLASS_PATH;
 
+import com.google.auto.value.AutoOneOf;
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableSet;
 import com.google.devtools.build.buildjar.JarOwner;
@@ -41,6 +46,7 @@
 import com.sun.tools.javac.util.Log;
 import com.sun.tools.javac.util.Log.WriterKind;
 import com.sun.tools.javac.util.Name;
+import com.sun.tools.javac.util.Names;
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.io.UncheckedIOException;
@@ -48,6 +54,7 @@
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Comparator;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -59,7 +66,9 @@
 import java.util.jar.Manifest;
 import javax.lang.model.element.AnnotationValue;
 import javax.lang.model.util.SimpleAnnotationValueVisitor8;
+import javax.tools.JavaFileManager;
 import javax.tools.JavaFileObject;
+import javax.tools.JavaFileObject.Kind;
 
 /**
  * A plugin for BlazeJavaCompiler that checks for types referenced directly in the source, but
@@ -133,9 +142,14 @@
             dependencyModule.getPlatformJars());
     checkingTreeScanner = context.get(CheckingTreeScanner.class);
     if (checkingTreeScanner == null) {
-      Set<Path> platformJars = dependencyModule.getPlatformJars();
       checkingTreeScanner =
-          new CheckingTreeScanner(dependencyModule, diagnostics, missingTargets, platformJars);
+          new CheckingTreeScanner(
+              dependencyModule,
+              diagnostics,
+              missingTargets,
+              dependencyModule.getPlatformJars(),
+              context.get(JavaFileManager.class),
+              Names.instance(context));
       context.put(CheckingTreeScanner.class, checkingTreeScanner);
     }
   }
@@ -235,24 +249,41 @@
 
     private final Set<JarOwner> seenTargets = new HashSet<>();
 
-    private final Set<Path> seenStrictDepsViolatingJars = new HashSet<>();
+    private final Set<NonPlatformJar> seenStrictDepsViolatingJars = new HashSet<>();
 
     /** The set of jars on the compilation bootclasspath. */
     private final Set<Path> platformJars;
 
+    private final JavaFileManager fileManager;
+
     /** The current source, for diagnostics. */
     private JavaFileObject source = null;
 
+    /** Cache of classpath (not platform) jars in which given symbols can be found. */
+    private final Map<ClassSymbol, Optional<Path>> classpathOnlyDepPaths = new HashMap<>();
+
+    private final Name jspecifyAnnotationsPackage;
+    private final Name jspecifyNullnessPackage;
+
+    /* TODO(b/297254214): Remove this flag after the depot is clean. */
+    private final boolean hidePlatformJspecify;
+
     public CheckingTreeScanner(
         DependencyModule dependencyModule,
         List<SjdDiagnostic> diagnostics,
         Set<JarOwner> missingTargets,
-        Set<Path> platformJars) {
+        Set<Path> platformJars,
+        JavaFileManager fileManager,
+        Names names) {
       this.directJars = dependencyModule.directJars();
       this.diagnostics = diagnostics;
       this.missingTargets = missingTargets;
       this.directDependenciesMap = dependencyModule.getExplicitDependenciesMap();
       this.platformJars = platformJars;
+      this.fileManager = fileManager;
+      jspecifyAnnotationsPackage = names.fromString("org.jspecify.annotations");
+      jspecifyNullnessPackage = names.fromString("org.jspecify.nullness");
+      hidePlatformJspecify = parseBoolean(System.getProperty("hidePlatformJspecify", "true"));
     }
 
     Set<ClassSymbol> getSeenClasses() {
@@ -264,12 +295,12 @@
       if (sym == null || sym.kind != Kinds.Kind.TYP) {
         return;
       }
-      Path jarPath = getJarPath(sym.enclClass(), platformJars);
+      NonPlatformJar jar = getNonPlatformJar(sym.enclClass(), platformJars);
 
       // If this type symbol comes from a class file loaded from a jar, check
       // whether that jar was a direct dependency and error out otherwise.
-      if (jarPath != null && seenClasses.add(sym.enclClass())) {
-        collectExplicitDependency(jarPath, node, sym);
+      if (jar != null && seenClasses.add(sym.enclClass())) {
+        collectExplicitDependency(jar, node, sym);
       }
     }
 
@@ -277,13 +308,13 @@
      * Marks the provided dependency as a direct/explicit dependency. Additionally, if
      * strict_java_deps is enabled, it emits a [strict] compiler warning/error.
      */
-    private void collectExplicitDependency(Path jarPath, JCTree node, Symbol sym) {
+    private void collectExplicitDependency(NonPlatformJar jar, JCTree node, Symbol sym) {
       // Does it make sense to emit a warning/error for this pair of (type, owner)?
       // We want to emit only one error/warning per owner.
-      if (!directJars.contains(jarPath) && seenStrictDepsViolatingJars.add(jarPath)) {
+      if (!directJars.contains(jar.pathOrEmpty()) && seenStrictDepsViolatingJars.add(jar)) {
         // IO cost here is fine because we only hit this path for an explicit dependency
         // _not_ in the direct jars, i.e. an error
-        JarOwner owner = readJarOwnerFromManifest(jarPath);
+        JarOwner owner = readJarOwnerFromManifest(jar);
         if (seenTargets.add(owner)) {
           // owner is of the form "//label/of:rule <Aspect name>" where <Aspect name> is
           // optional.
@@ -309,7 +340,7 @@
         }
       }
 
-      if (!directDependenciesMap.containsKey(jarPath)) {
+      if (!directDependenciesMap.containsKey(jar.pathOrEmpty())) {
         // Also update the dependency proto
         Dependency dep =
             Dependency.newBuilder()
@@ -317,14 +348,20 @@
                 // match the format in params files (which currently always use `/`, see
                 // bazelbuild/bazel#4108). JavaBuilder should always parse Path strings into
                 // java.nio.file.Paths before comparing them.
-                .setPath(jarPath.toString())
+                //
+                // An empty path is OK in the cases we produce it. See readJarOwnerFromManifest.
+                .setPath(jar.pathOrEmpty().toString())
                 .setKind(Dependency.Kind.EXPLICIT)
                 .build();
-        directDependenciesMap.put(jarPath, dep);
+        directDependenciesMap.put(jar.pathOrEmpty(), dep);
       }
     }
 
-    private static JarOwner readJarOwnerFromManifest(Path jarPath) {
+    private JarOwner readJarOwnerFromManifest(NonPlatformJar jar) {
+      if (jar.getKind() == FOR_JSPECIFY_FROM_PLATFORM) {
+        return JSPECIFY_JAR_OWNER;
+      }
+      Path jarPath = jar.inClasspath();
       try (JarFile jarFile = new JarFile(jarPath.toFile())) {
         Manifest manifest = jarFile.getManifest();
         if (manifest == null) {
@@ -423,6 +460,110 @@
       scan(tree.annotations);
       checkTypeLiteral(tree, tree.packge.package_info);
     }
+
+    /**
+     * Returns the name of the <i>classpath</i> jar from which the given class symbol was loaded
+     * (with an exception for the JSpecify annotations) or else {@code null}.
+     *
+     * <p>If the symbol came from the platform (i.e., system modules/bootclasspath), rather than
+     * from the classpath, this method <i>usually</i> returns {@code null}. The exception is for
+     * JSpecify-annotation symbols that are read from the platform: For such a symbol, this method
+     * still returns the first <i>classpath</i> jar that contains the symbol, or, if no classpath
+     * jar contains the symbol, it returns {@code FOR_JSPECIFY_FROM_PLATFORM}. (The calling code
+     * later converts that to {@code JSPECIFY_JAR_OWNER}, which will lead to a strict-deps error,
+     * since that jar clearly isn't a direct dependency.) In this way, we pretend that the
+     * JSpecify-annotation symbols <i>aren't</i> part of the platform. That's important because in
+     * fact they aren't part of it <i>at runtime</i> and so we want to force users of those classes
+     * to declare a dependency on them.
+     *
+     * <p>This behavior is mildly unfortunate in the unusual situation that a project normally reads
+     * a JSpecify-annotations class from an uber-jar, rather than from the normal JSpecify target.
+     * In that case, we claim that the class is being loaded from the normal target. That is not the
+     * target that the project's developers are likely to want. It's even possible that the class
+     * isn't present on the reduced classpath but <i>would</i> be present (via the uber-jar) if only
+     * we compiled with the full classpath. The full-classpath compilation would still produce a
+     * strict-deps error, but it would produce one that recommends the correct jar/dependency. But
+     * as this code is, we fail with a suggestion of the normal JSpecify target, and we may or may
+     * not fall back to the full classpath.
+     *
+     * <p>OK, arguably it's unfortunate that <i>ever</i> we suggest that the normal JSpecify target
+     * is on the classpath when it isn't really. However, the most common result of that is going to
+     * be that we produce a more convenient error message. That convenience helps to offset any
+     * confusion that we produce. Still, we won't introduce similar behavior for other classes
+     * lightly.
+     *
+     * @param platformJars jars on javac's bootclasspath
+     */
+    private NonPlatformJar getNonPlatformJar(ClassSymbol classSymbol, Set<Path> platformJars) {
+      if (classSymbol == null) {
+        return null;
+      }
+
+      // Ignore symbols that appear in the sourcepath:
+      if (haveSourceForSymbol(classSymbol)) {
+        return null;
+      }
+
+      JavaFileObject classfile = classSymbol.classfile;
+
+      Path path = ImplicitDependencyExtractor.getJarPath(classfile);
+      // Filter out classes from the system modules and bootclasspath
+      if (path == null || platformJars.contains(path)) {
+        // ...except the JSpecify annotations, which we treat specially.
+        if (hidePlatformJspecify
+            && (classSymbol.packge().fullname.equals(jspecifyAnnotationsPackage)
+                || classSymbol.packge().fullname.equals(jspecifyNullnessPackage))) {
+          Path classpathJar = findLookingOnlyInClasspath(classSymbol);
+          return classpathJar != null
+              ? NonPlatformJar.forClasspathJar(classpathJar)
+              : NonPlatformJar.FOR_JSPECIFY_FROM_PLATFORM;
+        }
+        return null;
+      }
+
+      return NonPlatformJar.forClasspathJar(path);
+    }
+
+    /**
+     * Returns the first jar file in the classpath (not system modules, not bootclasspath) that
+     * contains the given class or {@code null} if no such jar is available.
+     */
+    private Path findLookingOnlyInClasspath(ClassSymbol sym) {
+      /*
+       * computeIfAbsent doesn't cache null results, so we store Optional instances instead.
+       *
+       * In practice, that won't normally matter much: The only case in which our computation
+       * function runs once per usage of a JSpecify-annotation class is the failing-build case—that
+       * is, when the class is not on the classpath.
+       */
+      return classpathOnlyDepPaths
+          .computeIfAbsent(
+              sym,
+              (unused) -> {
+                try {
+                  for (JavaFileObject file :
+                      fileManager.list(
+                          CLASS_PATH,
+                          sym.packge().fullname.toString(),
+                          ImmutableSet.of(Kind.CLASS),
+                          false /* do not return classes in subpackages */)) {
+                    /*
+                     * The query above returns all classpath classes from the given package. We can
+                     * imagine situations in which only *some* JSpecify annotations are present in a
+                     * given classpath jar (an uber-jar with unused classes removed?), so we want to
+                     * make sure that we found the class we want.
+                     */
+                    if (file.isNameCompatible(sym.getSimpleName().toString(), Kind.CLASS)) {
+                      return Optional.of(ImplicitDependencyExtractor.getJarPath(file));
+                    }
+                  }
+                } catch (IOException e) {
+                  throw new UncheckedIOException(e);
+                }
+                return Optional.empty();
+              })
+          .orElse(null);
+    }
   }
 
   /**
@@ -507,37 +648,6 @@
     return target;
   }
 
-  /**
-   * Returns the name of the jar file from which the given class symbol was loaded, if available,
-   * and null otherwise. Implicitly filters out jars from the compilation bootclasspath.
-   *
-   * @param platformJars jars on javac's bootclasspath
-   */
-  public static Path getJarPath(ClassSymbol classSymbol, Set<Path> platformJars) {
-    if (classSymbol == null) {
-      return null;
-    }
-
-    // Ignore symbols that appear in the sourcepath:
-    if (haveSourceForSymbol(classSymbol)) {
-      return null;
-    }
-
-    JavaFileObject classfile = classSymbol.classfile;
-
-    Path path = ImplicitDependencyExtractor.getJarPath(classfile);
-    if (path == null) {
-      return null;
-    }
-
-    // Filter out classes on bootclasspath
-    if (platformJars.contains(path)) {
-      return null;
-    }
-
-    return path;
-  }
-
   /** Returns true if the given classSymbol corresponds to one of the sources being compiled. */
   private static boolean haveSourceForSymbol(ClassSymbol classSymbol) {
     if (classSymbol.sourcefile == null) {
@@ -559,4 +669,57 @@
   public boolean runOnAttributionErrors() {
     return true;
   }
+
+  /**
+   * Either a jar in the classpath or the well-known jar that contains the classes that are present
+   * in the platform at compile time but not runtime.
+   */
+  @AutoOneOf(NonPlatformJar.Kind.class)
+  abstract static class NonPlatformJar {
+    enum Kind {
+      IN_CLASSPATH,
+      FOR_JSPECIFY_FROM_PLATFORM,
+    }
+
+    abstract Kind getKind();
+
+    abstract Path inClasspath();
+
+    abstract Placeholder forJspecifyFromPlatform();
+
+    final Path pathOrEmpty() {
+      return getKind() == IN_CLASSPATH ? inClasspath() : EMPTY_PATH;
+    }
+
+    static NonPlatformJar forClasspathJar(Path s) {
+      return AutoOneOf_StrictJavaDepsPlugin_NonPlatformJar.inClasspath(s);
+    }
+
+    static final NonPlatformJar FOR_JSPECIFY_FROM_PLATFORM =
+        AutoOneOf_StrictJavaDepsPlugin_NonPlatformJar.forJspecifyFromPlatform(Placeholder.INSTANCE);
+  }
+
+  enum Placeholder {
+    INSTANCE
+  }
+
+  private static final Path EMPTY_PATH = Path.of("");
+
+  /**
+   * A special-purpose {@link JarOwner} instance that points to the main JSpecify target but is used
+   * when the JSpecify annotations are instead read from the platform.
+   *
+   * <p>We use this instance to force users to add the explicit JSpecify dependency—again, even
+   * though the annotations are present in the compile-time platform (i.e., bootclasspath or system
+   * modules). We require users to add the dependency because the annotations are <i>not</i> present
+   * in the <i>runtime</i> platform.
+   *
+   * <p>The {@link Path} argument that we pass to this instance doesn't matter because the build is
+   * usually going to fail. (Or, if a strict-deps enforcement is disabled, the user can't reasonably
+   * expect fully accurate dependency information, and our tools should be mostly resilient to an
+   * empty path.)
+   */
+  private static final JarOwner JSPECIFY_JAR_OWNER =
+      JarOwner.create(
+          EMPTY_PATH, "//third_party/java/jspecify_annotations", /* aspect= */ Optional.empty());
 }