From 81950b77cf273764142094fc01d606d8be9981ce Mon Sep 17 00:00:00 2001 From: Werner Dietl Date: Fri, 2 Oct 2026 07:22:31 -0400 Subject: [PATCH] AnnotationFileParser: accept a stub record whose library type is a class A stub file written for a newer JDK declares a record where an older JDK has a class of the same name (JDK 21's Collectors.CollectorImpl, a class in JDK 8 to 17). processTypeDecl's CLASS case cast every nested declaration to ClassOrInterfaceDeclaration, so the RecordDeclaration threw a ClassCastException. It was reported as a stub parse problem and everything after the record in the enclosing class was lost; the binary-stub differential check showed this as a mismatch for Collectors.toList and Collectors.joining on JDK 8, 11, and 17 with the JDK 21 annotated JDK. Cast to TypeDeclaration instead, skip a component with no field in the class, and store a record stub only for a record element, as the binary stub files do. The jtreg test fails without the change. Co-Authored-By: Claude Sonnet 5.5 --- .../jtreg/stubs/recordstubclass/Outer.astub | 13 ++++++++++ .../jtreg/stubs/recordstubclass/Outer.java | 12 +++++++++ checker/jtreg/stubs/recordstubclass/Use.java | 18 +++++++++++++ checker/jtreg/stubs/recordstubclass/Use.out | 4 +++ docs/CHANGELOG.md | 4 +++ .../framework/stub/AnnotationFileParser.java | 26 +++++++++++++------ 6 files changed, 69 insertions(+), 8 deletions(-) create mode 100644 checker/jtreg/stubs/recordstubclass/Outer.astub create mode 100644 checker/jtreg/stubs/recordstubclass/Outer.java create mode 100644 checker/jtreg/stubs/recordstubclass/Use.java create mode 100644 checker/jtreg/stubs/recordstubclass/Use.out diff --git a/checker/jtreg/stubs/recordstubclass/Outer.astub b/checker/jtreg/stubs/recordstubclass/Outer.astub new file mode 100644 index 00000000000..2c3d00f3580 --- /dev/null +++ b/checker/jtreg/stubs/recordstubclass/Outer.astub @@ -0,0 +1,13 @@ +package recordstubclass; + +import org.checkerframework.checker.nullness.qual.Nullable; + +public class Outer { + // The class is a record here but a plain class in the library, as when the annotated JDK + // (written for a newer JDK) declares a record that the JDK running the checker has as a class. + // Component "missing" has no field in the library. + public record Inner(Object f, Object missing) {} + + // Declared after the record: it was lost when the record aborted the processing of Outer. + public @Nullable Object get(); +} diff --git a/checker/jtreg/stubs/recordstubclass/Outer.java b/checker/jtreg/stubs/recordstubclass/Outer.java new file mode 100644 index 00000000000..040c8025a7d --- /dev/null +++ b/checker/jtreg/stubs/recordstubclass/Outer.java @@ -0,0 +1,12 @@ +package recordstubclass; + +/** A library class whose nested class is a record in the annotated stub (see Outer.astub). */ +public class Outer { + public static class Inner { + public final Object f = new Object(); + } + + public Object get() { + return new Object(); + } +} diff --git a/checker/jtreg/stubs/recordstubclass/Use.java b/checker/jtreg/stubs/recordstubclass/Use.java new file mode 100644 index 00000000000..d3795fbcbf7 --- /dev/null +++ b/checker/jtreg/stubs/recordstubclass/Use.java @@ -0,0 +1,18 @@ +/* + * @test + * @summary A record in a stub file whose library type is a class must not stop the processing of + * the members that follow it. + * + * @compile Outer.java + * @compile/fail/ref=Use.out -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext -Astubs=Outer.astub Use.java + */ + +package recordstubclass; + +import org.checkerframework.checker.nullness.qual.NonNull; + +public class Use { + void g(Outer o) { + @NonNull Object x = o.get(); + } +} diff --git a/checker/jtreg/stubs/recordstubclass/Use.out b/checker/jtreg/stubs/recordstubclass/Use.out new file mode 100644 index 00000000000..9d7b3acd176 --- /dev/null +++ b/checker/jtreg/stubs/recordstubclass/Use.out @@ -0,0 +1,4 @@ +- compiler.warn.proc.messager: Outer.astub:(line 9,col 35): field missing not found in type recordstubclass.Outer.Inner +Use.java:16:34: compiler.err.proc.messager: (assignment.type.incompatible) +1 error +1 warning diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index a16482ab6b0..77b6506443b 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -153,6 +153,10 @@ Command-line option additions and changes: - Invalid option arguments (e.g. malformed regex) now produce compiler errors instead of stack traces. +A stub file that declares a record where the running JDK has a class (for example +the JDK 21 annotated JDK on JDK 8 to 17) no longer loses the annotations of the +members that follow the record. + ### Implementation details Performance optimizations: diff --git a/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileParser.java b/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileParser.java index 274e442387c..b90e508c148 100644 --- a/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileParser.java +++ b/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileParser.java @@ -1194,14 +1194,21 @@ private String javaParserNodeToStringTruncated(Node n, int length) { Map byName = ArrayMap.newArrayMapOrLinkedHashMap(recordMembers.size()); for (Parameter recordMember : recordMembers) { - RecordComponentStub stub = - processRecordField( - recordMember, - findFieldElement( - typeElt, recordMember.getNameAsString(), recordMember)); - byName.put(recordMember.getNameAsString(), stub); + VariableElement componentElt = + findFieldElement(typeElt, recordMember.getNameAsString(), recordMember); + // Null if the type is a class without a field for the component; see + // findFieldElement, which already reported it. + if (componentElt != null) { + byName.put( + recordMember.getNameAsString(), + processRecordField(recordMember, componentElt)); + } + } + // A record in the stub file can be a class in the running JDK; only a record element + // has components, and the binary stub files likewise store no record for a class. + if (ElementUtils.isRecordElement(typeElt)) { + putMergeRecords(recordDecl.getFullyQualifiedName().get(), new RecordStub(byName)); } - putMergeRecords(recordDecl.getFullyQualifiedName().get(), new RecordStub(byName)); } Pair>, Map>>> members = @@ -1236,8 +1243,11 @@ private String javaParserNodeToStringTruncated(Node n, int length) { break; case CLASS: case INTERFACE: + // The declaration is a record when the stub file was written for a newer JDK + // than the one that is running: a class that became a record keeps its name, + // and its element is still a class here. // Not processing an ajava file, so ignore the return value. - processTypeDecl((ClassOrInterfaceDeclaration) decl, innerName, null); + processTypeDecl((TypeDeclaration) decl, innerName, null); break; case ENUM: // Not processing an ajava file, so ignore the return value.