[ 
https://issues.apache.org/jira/browse/GROOVY-12207?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18099907#comment-18099907
 ] 

ASF GitHub Bot commented on GROOVY-12207:
-----------------------------------------

Copilot commented on code in PR #2745:
URL: https://github.com/apache/groovy/pull/2745#discussion_r3670595002


##########
src/main/java/org/codehaus/groovy/control/ClassNodeResolver.java:
##########
@@ -160,6 +174,76 @@ public ClassNode getFromClassCache(final String name) {
         return cached;
     }
 
+    /**
+     * Resolves a package name to a {@link PackageNode} carrying the 
annotations found on the
+     * package's compiled {@code package-info.class}, if any (GROOVY-12207). 
This makes
+     * package-level annotations of precompiled dependencies (e.g. JSpecify's 
{@code @NullMarked})
+     * visible to type checkers and AST transforms.
+     * <p>
+     * The {@code package-info.class} is located on the compilation unit's 
class path and decompiled
+     * on demand using the same ASM infrastructure as ordinary classes; 
results are cached per
+     * resolver, including a negative cache for packages that have no 
(annotation-bearing)
+     * package-info. Returns {@code null} if the package has no such metadata.
+     *
+     * @param packageName the fully qualified package name (no trailing dot), 
e.g. {@code "foo.bar"}
+     * @param compilationUnit the current {@link CompilationUnit}
+     * @return a {@link PackageNode} with the package's annotations, or {@code 
null} if none
+     */
+    public synchronized PackageNode resolvePackage(final String packageName, 
final CompilationUnit compilationUnit) {
+        if (packageName == null || packageName.isEmpty() || compilationUnit == 
null) {
+            return null;
+        }
+        PackageNode cached = cachedPackages.get(packageName);
+        if (cached != null) {
+            return cached == NO_PACKAGE ? null : cached;
+        }
+        PackageNode result = findPackageInfo(packageName, compilationUnit);
+        cachedPackages.put(packageName, result == null ? NO_PACKAGE : result);
+        return result;
+    }
+
+    /**
+     * Loads and decompiles {@code <packageName>/package-info.class} from the 
compilation unit's
+     * class loader, copying its (class-level) annotations onto a fresh {@link 
PackageNode}. The
+     * annotations of a {@code package-info} type are stored as ordinary class 
annotations in the
+     * bytecode, so the existing decompiler pipeline reads them without 
special handling.
+     *
+     * @return a populated {@link PackageNode}, or {@code null} if there is no 
package-info, it has
+     *         no annotations, or it could not be read
+     */
+    private PackageNode findPackageInfo(final String packageName, final 
CompilationUnit compilationUnit) {
+        GroovyClassLoader loader = compilationUnit.getClassLoader();
+        if (loader == null) {
+            return null;
+        }
+        String fileName = packageName.replace('.', '/') + 
"/package-info.class";
+        URL resource = loader.getResource(fileName);
+        if (resource == null) {
+            return null;
+        }
+        try {
+            DecompiledClassNode packageInfo = new DecompiledClassNode(
+                    AsmDecompiler.parseClass(resource), new 
AsmReferenceResolver(this, compilationUnit));
+            if (!packageInfo.getName().equals(packageName + ".package-info")) {
+                // this may happen under Windows/macOS because getResource is 
case-insensitive there!
+                return null;
+            }
+            List<AnnotationNode> annotations = packageInfo.getAnnotations();
+            if (annotations == null || annotations.isEmpty()) {
+                return null;
+            }
+            PackageNode packageNode = new PackageNode(packageName + ".");

Review Comment:
   `resolvePackage` documents `packageName` as having no trailing dot, but 
`findPackageInfo` constructs `PackageNode` with `packageName + "."`. This makes 
`PackageNode.getName()` inconsistent with the rest of the AST (and would render 
as `package foo.bar.` via `PackageNode#getText`). Create the `PackageNode` with 
the plain package name instead.



##########
src/main/java/org/codehaus/groovy/ast/decompiled/DecompiledClassNode.java:
##########
@@ -164,6 +165,23 @@ public long getCompilationTimeStamp() {
      * @throws org.codehaus.groovy.GroovyBugError if the class cannot be 
resolved at runtime
      * @see AsmReferenceResolver#resolveJvmClass(String)
      */
+    /**
+     * Returns the {@link PackageNode} for this decompiled class, carrying any 
annotations declared
+     * on the package's compiled {@code package-info.class} (GROOVY-12207). 
Unlike a source-based
+     * {@link ClassNode}, a decompiled class has no owning module, so the 
package (and its
+     * annotations) is resolved on demand from the class path via the 
reference resolver's

Review Comment:
   There are now two consecutive Javadoc blocks before `getPackage()`. The 
first block (about resolving the runtime JVM `Class`) is no longer attached to 
`getTypeClass()` and can trigger dangling-Javadoc warnings and misleading 
generated docs. Removing that stray Javadoc block (or relocating it directly 
above `getTypeClass()`) will keep Javadoc output clean.





> Consult package-info.class of dependencies during resolution
> ------------------------------------------------------------
>
>                 Key: GROOVY-12207
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12207
>             Project: Groovy
>          Issue Type: Improvement
>            Reporter: Paul King
>            Priority: Major
>
> Groovy's resolver does not consult {{package-info.class}} for the packages of 
> precompiled dependencies, so package-level annotations of libraries are 
> invisible to the compiler and to type-checking extensions.
> JSpecify's {{@NullMarked}} is commonly applied at package level (and may also 
> be applied at module level), meaning an entire library's nullness defaults 
> are typically expressed in files Groovy never reads. Without this, 
> per-element type annotations (see the companion type-annotation ingestion 
> issue) tell only half the story: a {{@NullMarked}} package with unannotated 
> method signatures means "everything here is non-null", and Groovy would 
> wrongly treat it as unspecified.
> Proposed scope:
> # On-demand loading of {{package-info.class}} for the package of a resolved 
> class, with caching (including negative caching for packages without one), 
> reusing the existing decompiled-class infrastructure.
> # Expose the package annotations via a suitable API (e.g. on {{PackageNode}} 
> or a resolver service) so checkers and AST transforms can query them for 
> dependency packages the way they already can for source packages.
> # Extend {{NullChecker}} to know about this new annotation information.
> # Consider {{module-info}} annotations ({{@NullMarked}} at module level) as a 
> stretch goal.
> Behaviour-neutral on its own: this only makes existing metadata reachable. 
> Consumers include the incubating {{groovy.typecheckers.NullChecker}} (which 
> already recognizes {{@NullMarked}}/{{@NullUnmarked}} by name but can 
> currently only see them on classes in the compilation unit).



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to