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.