diff --git a/build.gradle b/build.gradle index c024660a620..129af081881 100644 --- a/build.gradle +++ b/build.gradle @@ -405,6 +405,8 @@ List getJavaFilesToFormat(projectName) { && !details.path.contains("calledmethods-delomboked") && !details.path.contains("returnsreceiverdelomboked") && !details.path.contains("build") + && (isJava16 || !details.path.contains("nullness-records")) + && (isJava16 || !details.path.contains("stubparser-records")) && details.name.endsWith('.java')) { javaFiles.add(details.file) } diff --git a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationVisitor.java b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationVisitor.java index 0e0c536a757..eb4f7075b53 100644 --- a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationVisitor.java @@ -379,6 +379,14 @@ protected void checkFieldsInitialized( return; } + // Canonical record constructors do not generate visible assignments in the source, + // but by definition they assign to all the record's fields so we don't need to + // check for uninitialized fields in them: + if (node.getKind() == Tree.Kind.METHOD + && TreeUtils.isCanonicalRecordConstructor((MethodTree) node)) { + return; + } + Pair, List> uninitializedFields = atypeFactory.getUninitializedFields( store, getCurrentPath(), staticFields, receiverAnnotations); diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessRecordsTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessRecordsTest.java new file mode 100644 index 00000000000..ff76024efdf --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessRecordsTest.java @@ -0,0 +1,34 @@ +package org.checkerframework.checker.test.junit; + +import java.io.File; +import java.util.List; +import org.checkerframework.checker.nullness.NullnessChecker; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +/** JUnit tests for the Nullness checker with records (JDK16+ only). */ +public class NullnessRecordsTest extends CheckerFrameworkPerDirectoryTest { + + /** + * Create a NullnessRecordsTest. + * + * @param testFiles the files containing test code, which will be type-checked + */ + public NullnessRecordsTest(List testFiles) { + super( + testFiles, + NullnessChecker.class, + "nullness-records", + "-AcheckPurityAnnotations", + "-Anomsgtext", + "-Xlint:deprecation"); + } + + @Parameters + public static String[] getTestDirs() { + // Check for JDK 16+ without using a library: + if (System.getProperty("java.version").matches("^(1[6-9]|[2-9][0-9])\\..*")) + return new String[] {"nullness-records"}; + else return new String[] {}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/StubparserRecordTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/StubparserRecordTest.java new file mode 100644 index 00000000000..c5e854499bf --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/StubparserRecordTest.java @@ -0,0 +1,33 @@ +package org.checkerframework.checker.test.junit; + +import java.io.File; +import java.util.List; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized; + +/** Tests for stub parsing with records. */ +public class StubparserRecordTest extends CheckerFrameworkPerDirectoryTest { + + /** + * Create a StubparserRecordTest. + * + * @param testFiles the files containing test code, which will be type-checked + */ + public StubparserRecordTest(List testFiles) { + super( + testFiles, + org.checkerframework.checker.nullness.NullnessChecker.class, + "stubparser-records", + "-Anomsgtext", + "-Astubs=tests/stubparser-records", + "-AstubWarnIfNotFound"); + } + + @Parameterized.Parameters + public static String[] getTestDirs() { + // Check for JDK 16+ without using a library: + if (System.getProperty("java.version").matches("^(1[6-9]|[2-9][0-9])\\..*")) + return new String[] {"stubparser-records"}; + else return new String[] {}; + } +} diff --git a/checker/tests/nullness-records/BasicRecord.java b/checker/tests/nullness-records/BasicRecord.java new file mode 100644 index 00000000000..daafd6ee2f9 --- /dev/null +++ b/checker/tests/nullness-records/BasicRecord.java @@ -0,0 +1,14 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +// @below-java16-jdk-skip-test +public record BasicRecord(String str) { + + public static BasicRecord makeNonNull(String s) { + return new BasicRecord(s); + } + + public static BasicRecord makeNull(@Nullable String s) { + // :: error: argument + return new BasicRecord(s); + } +} diff --git a/checker/tests/nullness-records/BasicRecordCanon.java b/checker/tests/nullness-records/BasicRecordCanon.java new file mode 100644 index 00000000000..11fd58b41ce --- /dev/null +++ b/checker/tests/nullness-records/BasicRecordCanon.java @@ -0,0 +1,16 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +// @below-java16-jdk-skip-test +public record BasicRecordCanon(String str) { + + public static BasicRecordCanon makeNonNull(String s) { + return new BasicRecordCanon(s); + } + + public static BasicRecordCanon makeNull(@Nullable String s) { + // :: error: argument + return new BasicRecordCanon(s); + } + + public BasicRecordCanon {} +} diff --git a/checker/tests/nullness-records/BasicRecordNullable.java b/checker/tests/nullness-records/BasicRecordNullable.java new file mode 100644 index 00000000000..abb2228a827 --- /dev/null +++ b/checker/tests/nullness-records/BasicRecordNullable.java @@ -0,0 +1,31 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +// @below-java16-jdk-skip-test +public record BasicRecordNullable(@Nullable String str) { + + public static BasicRecordNullable makeNonNull(String s) { + return new BasicRecordNullable(s); + } + + public static BasicRecordNullable makeNull(@Nullable String s) { + return new BasicRecordNullable(s); + } + + public @Nullable String getStringFromField() { + return str; + } + + public @Nullable String getStringFromMethod() { + return str(); + } + + public String getStringFromFieldErr() { + // :: error: return + return str; + } + + public String getStringFromMethodErr() { + // :: error: return + return str(); + } +} diff --git a/checker/tests/nullness-records/DefaultQualRecord.java b/checker/tests/nullness-records/DefaultQualRecord.java new file mode 100644 index 00000000000..c27a9749eca --- /dev/null +++ b/checker/tests/nullness-records/DefaultQualRecord.java @@ -0,0 +1,62 @@ +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.framework.qual.DefaultQualifier; + +class StandardQualClass { + // :: error: assignment + public static String s = null; + // :: error: initialization.static.field.uninitialized + public static String u; +} + +@DefaultQualifier(Nullable.class) +class DefaultQualClass { + public static String s = null; + public static String u; +} + +interface StandardQualInterface { + // :: error: assignment + public static String s = null; +} + +@DefaultQualifier(Nullable.class) +interface DefaultQualInterface { + public static String s = null; +} + +enum StandardQualEnum { + DUMMY; + // :: error: assignment + public static String s = null; + // :: error: initialization.static.field.uninitialized + public static String u; +} + +@DefaultQualifier(Nullable.class) +enum DefaultQualEnum { + DUMMY; + public static String s = null; + public static String u; +} + +record StandardQualRecord(String m) { + // :: error: assignment + public static String s = null; + // :: error: initialization.static.field.uninitialized + public static String u; + + StandardQualRecord { + // :: error: assignment + m = null; + } +} + +@DefaultQualifier(Nullable.class) +record DefaultQualRecord(String m) { + public static String s = null; + public static String u; + + DefaultQualRecord { + m = null; + } +} diff --git a/checker/tests/nullness-records/GenericPair.java b/checker/tests/nullness-records/GenericPair.java new file mode 100644 index 00000000000..e2955305222 --- /dev/null +++ b/checker/tests/nullness-records/GenericPair.java @@ -0,0 +1,11 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +// @below-java16-jdk-skip-test +public record GenericPair(K key, V value) { + + public static void foo() { + GenericPair p = new GenericPair<>("k", null); + // :: error: (dereference.of.nullable) + p.value().toString(); + } +} diff --git a/checker/tests/nullness-records/LocalRecords.java b/checker/tests/nullness-records/LocalRecords.java new file mode 100644 index 00000000000..94755ce77d9 --- /dev/null +++ b/checker/tests/nullness-records/LocalRecords.java @@ -0,0 +1,12 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +// @below-java16-jdk-skip-test +public class LocalRecords { + public static void foo() { + record L(String key, @Nullable Integer value) {} + L a = new L("one", 1); + L b = new L("i", null); + // :: error: (argument) + L c = new L(null, 6); + } +} diff --git a/checker/tests/nullness-records/NestedRecordTest.java b/checker/tests/nullness-records/NestedRecordTest.java new file mode 100644 index 00000000000..2b75825564d --- /dev/null +++ b/checker/tests/nullness-records/NestedRecordTest.java @@ -0,0 +1,98 @@ +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +// @below-java16-jdk-skip-test + +public class NestedRecordTest { + + static @NonNull String nn = "foo"; + static @Nullable String nble = null; + static @NonNull String nn2 = "foo"; + static @Nullable String nble2 = null; + + public static class Nested { + public record NPerson(String familyName, @Nullable String maidenName) {} + + void nclient() { + Nested.NPerson np1 = new Nested.NPerson(nn, nn); + Nested.NPerson np2 = new Nested.NPerson(nn, nble); + // :: error: argument + Nested.NPerson np3 = new Nested.NPerson(nble, nn); + // :: error: argument + Nested.NPerson np4 = new Nested.NPerson(nble, nble); + Inner.IPerson ip1 = new Inner.IPerson(nn, nn); + Inner.IPerson ip2 = new Inner.IPerson(nn, nble); + // :: error: argument + Inner.IPerson ip3 = new Inner.IPerson(nble, nn); + // :: error: argument + Inner.IPerson ip4 = new Inner.IPerson(nble, nble); + + nn2 = np2.familyName(); + nble2 = np2.familyName(); + // :: error: assignment + nn2 = np2.maidenName(); + nble2 = np2.maidenName(); + nn2 = ip2.familyName(); + nble2 = ip2.familyName(); + // :: error: assignment + nn2 = ip2.maidenName(); + nble2 = ip2.maidenName(); + } + } + + public class Inner { + public record IPerson(String familyName, @Nullable String maidenName) {} + + void iclient() { + Nested.NPerson np1 = new Nested.NPerson(nn, nn); + Nested.NPerson np2 = new Nested.NPerson(nn, nble); + // :: error: argument + Nested.NPerson np3 = new Nested.NPerson(nble, nn); + // :: error: argument + Nested.NPerson np4 = new Nested.NPerson(nble, nble); + Inner.IPerson ip1 = new Inner.IPerson(nn, nn); + Inner.IPerson ip2 = new Inner.IPerson(nn, nble); + // :: error: argument + Inner.IPerson ip3 = new Inner.IPerson(nble, nn); + // :: error: argument + Inner.IPerson ip4 = new Inner.IPerson(nble, nble); + + nn2 = np2.familyName(); + nble2 = np2.familyName(); + // :: error: assignment + nn2 = np2.maidenName(); + nble2 = np2.maidenName(); + nn2 = ip2.familyName(); + nble2 = ip2.familyName(); + // :: error: assignment + nn2 = ip2.maidenName(); + nble2 = ip2.maidenName(); + } + } + + void client() { + Nested.NPerson np1 = new Nested.NPerson(nn, nn); + Nested.NPerson np2 = new Nested.NPerson(nn, nble); + // :: error: argument + Nested.NPerson np3 = new Nested.NPerson(nble, nn); + // :: error: argument + Nested.NPerson np4 = new Nested.NPerson(nble, nble); + Inner.IPerson ip1 = new Inner.IPerson(nn, nn); + Inner.IPerson ip2 = new Inner.IPerson(nn, nble); + // :: error: argument + Inner.IPerson ip3 = new Inner.IPerson(nble, nn); + // :: error: argument + Inner.IPerson ip4 = new Inner.IPerson(nble, nble); + + nn2 = np2.familyName(); + nble2 = np2.familyName(); + // :: error: assignment + nn2 = np2.maidenName(); + nble2 = np2.maidenName(); + nn2 = ip2.familyName(); + nble2 = ip2.familyName(); + // :: error: assignment + nn2 = ip2.maidenName(); + nble2 = ip2.maidenName(); + } +} diff --git a/checker/tests/nullness-records/NormalizingRecord.java b/checker/tests/nullness-records/NormalizingRecord.java new file mode 100644 index 00000000000..df711e0c797 --- /dev/null +++ b/checker/tests/nullness-records/NormalizingRecord.java @@ -0,0 +1,60 @@ +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +// @below-java16-jdk-skip-test + +public class NormalizingRecord {} + +// TODO: Nest the rest of the file within NormalizingRecord when that doesn't crash the Checker +// Framework. + +record NormalizingRecord1(@Nullable String s) { + NormalizingRecord1(String s) { + if (s.equals("")) { + this.s = null; + } else { + this.s = s; + } + } +} + +record NormalizingRecord2(String s) { + NormalizingRecord2(@Nullable String s) { + if (s == null) { + s = ""; + } + this.s = s; + } +} + +record NormalizingRecordIllegalConstructor1(String s) { + NormalizingRecordIllegalConstructor1(@Nullable String s) { + // :: error: (assignment) + this.s = s; + } +} + +record NormalizingRecordIllegalConstructor2(@Nullable String s) { + NormalizingRecordIllegalConstructor2(String s) { + if (s.equals("")) { + // The formal parametr type is @NonNull, so this assignment to it is illegal. + // :: error: (assignment) + s = null; + } + this.s = s; + } +} + +class Client { + + // :: error: (argument) + NormalizingRecord1 nr1_1 = new NormalizingRecord1(null); + NormalizingRecord1 nr1_2 = new NormalizingRecord1(""); + NormalizingRecord1 nr1_3 = new NormalizingRecord1("hello"); + @Nullable String nble = nr1_2.s(); + + NormalizingRecord2 nr2_1 = new NormalizingRecord2(null); + NormalizingRecord2 nr2_2 = new NormalizingRecord2(""); + NormalizingRecord2 nr2_3 = new NormalizingRecord2("hello"); + @NonNull String nn = nr2_1.s(); +} diff --git a/checker/tests/stubparser-records/PairRecord.astub b/checker/tests/stubparser-records/PairRecord.astub new file mode 100644 index 00000000000..0c9b1883593 --- /dev/null +++ b/checker/tests/stubparser-records/PairRecord.astub @@ -0,0 +1,5 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +record PairRecord(String key, @Nullable Object value) { + PairRecord(@Nullable String val); +} diff --git a/checker/tests/stubparser-records/PairRecord.java b/checker/tests/stubparser-records/PairRecord.java new file mode 100644 index 00000000000..eb7fbf66386 --- /dev/null +++ b/checker/tests/stubparser-records/PairRecord.java @@ -0,0 +1,6 @@ +record PairRecord(String key, Object value) { + + PairRecord(String val) { + this("", val); + } +} diff --git a/checker/tests/stubparser-records/RecordStubbed.astub b/checker/tests/stubparser-records/RecordStubbed.astub new file mode 100644 index 00000000000..ca7fbbd9681 --- /dev/null +++ b/checker/tests/stubparser-records/RecordStubbed.astub @@ -0,0 +1,14 @@ +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * nxx is nullable in the record but non-null in constructor and accessor via stubs + * nsxx is nullable in the stubs but non-null in constructor and accessor via stubs + * xnn is de-facto non-null in record but nullable in constructor and accessor via stubs + */ +public record RecordStubbed(String nxx, @Nullable String nsxx, Integer xnn) { + RecordStubbed(@NonNull String nxx, @NonNull String nsxx, @Nullable Integer xnn); + @NonNull String nxx(); + @NonNull String nsxx(); + @Nullable Integer xnn(); +} diff --git a/checker/tests/stubparser-records/RecordStubbed.java b/checker/tests/stubparser-records/RecordStubbed.java new file mode 100644 index 00000000000..fc48281915e --- /dev/null +++ b/checker/tests/stubparser-records/RecordStubbed.java @@ -0,0 +1,12 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * nxx is nullable in the record but non-null in constructor and accessor via stubs nsxx is nullable + * in the stubs but non-null in constructor and accessor via stubs xnn is de-facto non-null in + * record but nullable in constructor and accessor via stubs + */ +public record RecordStubbed(@Nullable String nxx, String nsxx, Integer xnn) { + RecordStubbed(Integer a, String b, String c) { + this(c, b, a); + } +} diff --git a/checker/tests/stubparser-records/RecordUsage.java b/checker/tests/stubparser-records/RecordUsage.java new file mode 100644 index 00000000000..adbafe2e00b --- /dev/null +++ b/checker/tests/stubparser-records/RecordUsage.java @@ -0,0 +1,24 @@ +import org.checkerframework.checker.nullness.qual.NonNull; + +class PairUsage { + public void makePairs() { + PairRecord a = new PairRecord("key", "value"); + PairRecord b = new PairRecord(null); + // :: error: (assignment) + @NonNull Object o = a.value(); + PairRecord p = new PairRecord("key", null); + } + + public void makeStubbed() { + RecordStubbed r = new RecordStubbed("a", "b", 7); + RecordStubbed r1 = new RecordStubbed("a", "b", null); + // :: error: (argument) + RecordStubbed r2 = new RecordStubbed((String) null, "b", null); + // :: error: (argument) + RecordStubbed r3 = new RecordStubbed("a", null, null); + @NonNull Object o = r.nxx(); + @NonNull Object o2 = r.nsxx(); + // :: error: (assignment) + @NonNull Object o3 = r.xnn(); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java index 7c68eea4100..48e40a424d2 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java @@ -3520,9 +3520,14 @@ public Node visitVariable(VariableTree tree, Void p) { // see JLS 14.4 - boolean isField = - getCurrentPath().getParentPath() != null - && getCurrentPath().getParentPath().getLeaf().getKind() == Tree.Kind.CLASS; + boolean isField = false; + if (getCurrentPath().getParentPath() != null) { + Tree.Kind kind = TreeUtils.getKindRecordAsClass(getCurrentPath().getParentPath().getLeaf()); + // CLASS includes records. + if (kind == Tree.Kind.CLASS || kind == Tree.Kind.INTERFACE || kind == Tree.Kind.ENUM) { + isField = true; + } + } Node node = null; ClassTree enclosingClass = TreePathUtil.enclosingClass(getCurrentPath()); diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index ac908c67e05..e9de36d60cd 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,4 +1,15 @@ -Version 3.17.0 (August 2, 2021) +Version 3.18.0 (September 1, 2021) +------------------------------- + +**User-visible changes:** + +Java records are type-checked. Thanks to Neil Brown. + +**Implementation details:** + +**Closed issues:** + +Version 3.17.0 (August 3, 2021) ------------------------------- **User-visible changes:** diff --git a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java index 1a5bf30ec99..3d2a4ddf4a8 100644 --- a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java +++ b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java @@ -386,8 +386,11 @@ public void defaultJointAction( for (Tree expected : expectedTreesVisitor.getTrees()) { if (!treePairs.containsKey(expected)) { throw new BugInCF( - "Javac tree not matched to JavaParser node: %s, in file: %s", - expected, root.getSourceFile().getName()); + "Javac tree not matched to JavaParser node: %s [%s @ %d], in file: %s", + expected, + expected.getClass(), + positions.getStartPosition(root, expected), + root.getSourceFile().getName()); } } } catch (IOException e) { @@ -1493,6 +1496,11 @@ private void warnAboutTypeAnnotationsTooEarly(Tree node, ModifiersTree modifiers // appears to be before "final". return; default: + if (TreeUtils.isAutoGeneratedRecordMember(node)) { + // Annotations can appear on record fields before the class body, so don't issue + // a warning about those. + return; + } // Nothing to do } } diff --git a/framework/src/main/java/org/checkerframework/common/util/debug/SignaturePrinter.java b/framework/src/main/java/org/checkerframework/common/util/debug/SignaturePrinter.java index 2fd31b1ebc0..5ec2629166d 100644 --- a/framework/src/main/java/org/checkerframework/common/util/debug/SignaturePrinter.java +++ b/framework/src/main/java/org/checkerframework/common/util/debug/SignaturePrinter.java @@ -243,6 +243,9 @@ private String typeIdentifier(TypeElement e) { case ENUM: return "enum"; default: + if (e.getKind().name().equals("RECORD")) { + return "record"; + } throw new IllegalArgumentException("Not a type element: " + e.getKind()); } } diff --git a/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java b/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java index ed1cd4efc15..323393d494a 100644 --- a/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java +++ b/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java @@ -10,6 +10,7 @@ import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.ast.body.Parameter; import com.github.javaparser.ast.body.ReceiverParameter; +import com.github.javaparser.ast.body.RecordDeclaration; import com.github.javaparser.ast.body.TypeDeclaration; import com.github.javaparser.ast.body.VariableDeclarator; import com.github.javaparser.ast.expr.AnnotationExpr; @@ -398,6 +399,11 @@ public void processClass(ClassTree javacTree, EnumDeclaration javaParserNode) { addClass(javacTree); } + @Override + public void processClass(ClassTree javacTree, RecordDeclaration javaParserNode) { + addClass(javacTree); + } + @Override public void processNewClass(NewClassTree javacTree, ObjectCreationExpr javaParserNode) { if (javacTree.getClassBody() != null) { diff --git a/framework/src/main/java/org/checkerframework/framework/ajava/DefaultJointVisitor.java b/framework/src/main/java/org/checkerframework/framework/ajava/DefaultJointVisitor.java index d66af21c14d..33885307846 100644 --- a/framework/src/main/java/org/checkerframework/framework/ajava/DefaultJointVisitor.java +++ b/framework/src/main/java/org/checkerframework/framework/ajava/DefaultJointVisitor.java @@ -7,12 +7,14 @@ import com.github.javaparser.ast.body.AnnotationDeclaration; import com.github.javaparser.ast.body.AnnotationMemberDeclaration; import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.body.CompactConstructorDeclaration; import com.github.javaparser.ast.body.ConstructorDeclaration; import com.github.javaparser.ast.body.EnumConstantDeclaration; import com.github.javaparser.ast.body.EnumDeclaration; import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.ast.body.Parameter; import com.github.javaparser.ast.body.ReceiverParameter; +import com.github.javaparser.ast.body.RecordDeclaration; import com.github.javaparser.ast.body.VariableDeclarator; import com.github.javaparser.ast.expr.ArrayAccessExpr; import com.github.javaparser.ast.expr.AssignExpr; @@ -183,6 +185,9 @@ public void processClass(ClassTree javacTree, ClassOrInterfaceDeclaration javaPa @Override public void processClass(ClassTree javacTree, EnumDeclaration javaParserNode) {} + @Override + public void processClass(ClassTree javacTree, RecordDeclaration javaParserNode) {} + @Override public void processCompilationUnit( CompilationUnitTree javacTree, CompilationUnit javaParserNode) {} @@ -288,6 +293,9 @@ public void processMethod(MethodTree javacTree, MethodDeclaration javaParserNode @Override public void processMethod(MethodTree javacTree, ConstructorDeclaration javaParserNode) {} + @Override + public void processMethod(MethodTree javacTree, CompactConstructorDeclaration javaParserNode) {} + @Override public void processMethod(MethodTree javacTree, AnnotationMemberDeclaration javaParserNode) {} diff --git a/framework/src/main/java/org/checkerframework/framework/ajava/DoubleJavaParserVisitor.java b/framework/src/main/java/org/checkerframework/framework/ajava/DoubleJavaParserVisitor.java index e2492e61c81..bbdf54d41fd 100644 --- a/framework/src/main/java/org/checkerframework/framework/ajava/DoubleJavaParserVisitor.java +++ b/framework/src/main/java/org/checkerframework/framework/ajava/DoubleJavaParserVisitor.java @@ -9,6 +9,7 @@ import com.github.javaparser.ast.body.AnnotationDeclaration; import com.github.javaparser.ast.body.AnnotationMemberDeclaration; import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.body.CompactConstructorDeclaration; import com.github.javaparser.ast.body.ConstructorDeclaration; import com.github.javaparser.ast.body.EnumConstantDeclaration; import com.github.javaparser.ast.body.EnumDeclaration; @@ -17,6 +18,7 @@ import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.ast.body.Parameter; import com.github.javaparser.ast.body.ReceiverParameter; +import com.github.javaparser.ast.body.RecordDeclaration; import com.github.javaparser.ast.body.VariableDeclarator; import com.github.javaparser.ast.comments.BlockComment; import com.github.javaparser.ast.comments.JavadocComment; @@ -77,6 +79,7 @@ import com.github.javaparser.ast.stmt.IfStmt; import com.github.javaparser.ast.stmt.LabeledStmt; import com.github.javaparser.ast.stmt.LocalClassDeclarationStmt; +import com.github.javaparser.ast.stmt.LocalRecordDeclarationStmt; import com.github.javaparser.ast.stmt.ReturnStmt; import com.github.javaparser.ast.stmt.SwitchEntry; import com.github.javaparser.ast.stmt.SwitchStmt; @@ -312,6 +315,18 @@ public void visit(final ConstructorDeclaration node1, final Node other) { visitLists(node1.getTypeParameters(), node2.getTypeParameters()); } + @Override + public void visit(CompactConstructorDeclaration node1, Node other) { + CompactConstructorDeclaration node2 = (CompactConstructorDeclaration) other; + defaultAction(node1, node2); + node1.getBody().accept(this, node2.getBody()); + visitLists(node1.getModifiers(), node2.getModifiers()); + node1.getName().accept(this, node2.getName()); + + visitLists(node1.getThrownExceptions(), node2.getThrownExceptions()); + visitLists(node1.getTypeParameters(), node2.getTypeParameters()); + } + @Override public void visit(final ContinueStmt node1, final Node other) { ContinueStmt node2 = (ContinueStmt) other; @@ -604,6 +619,21 @@ public void visit(final UnionType node1, final Node other) { visitLists(node1.getElements(), node2.getElements()); } + @Override + public void visit(RecordDeclaration node1, Node other) { + RecordDeclaration node2 = (RecordDeclaration) other; + defaultAction(node1, node2); + visitLists(node1.getImplementedTypes(), node2.getImplementedTypes()); + visitLists(node1.getTypeParameters(), node2.getTypeParameters()); + visitLists(node1.getParameters(), node2.getParameters()); + visitLists(node1.getMembers(), node2.getMembers()); + visitLists(node1.getModifiers(), node2.getModifiers()); + node1.getName().accept(this, node2.getName()); + if (node1.getReceiverParameter().isPresent() && node2.getReceiverParameter().isPresent()) { + node1.getReceiverParameter().get().accept(this, node2.getReceiverParameter().get()); + } + } + @Override public void visit(final ReturnStmt node1, final Node other) { ReturnStmt node2 = (ReturnStmt) other; @@ -686,6 +716,13 @@ public void visit(final LocalClassDeclarationStmt node1, final Node other) { node1.getClassDeclaration().accept(this, node2.getClassDeclaration()); } + @Override + public void visit(LocalRecordDeclarationStmt node1, final Node other) { + LocalRecordDeclarationStmt node2 = (LocalRecordDeclarationStmt) other; + defaultAction(node1, node2); + node1.getRecordDeclaration().accept(this, node2.getRecordDeclaration()); + } + @Override public void visit(final TypeParameter node1, final Node other) { TypeParameter node2 = (TypeParameter) other; diff --git a/framework/src/main/java/org/checkerframework/framework/ajava/ExpectedTreesVisitor.java b/framework/src/main/java/org/checkerframework/framework/ajava/ExpectedTreesVisitor.java index 59299570fa1..e1ef34aa7c5 100644 --- a/framework/src/main/java/org/checkerframework/framework/ajava/ExpectedTreesVisitor.java +++ b/framework/src/main/java/org/checkerframework/framework/ajava/ExpectedTreesVisitor.java @@ -29,6 +29,7 @@ import java.util.List; import java.util.Set; import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.javacutil.TreeUtils; /** * After this visitor visits a tree, {@link #getTrees} returns all the trees that should match with @@ -107,6 +108,43 @@ public Void visitClass(ClassTree tree, Void p) { trees.remove(constructor.getIdentifier()); } } + // RECORD was added in Java 14, so use string comparison to be JDK 8,11 compatible: + } else if (tree.getKind().name().equals("RECORD")) { + // A record like: + // record MyRec(String myField) {} + // will be expanded by javac to: + // class MyRec { + // MyRec(String myField) { + // super(); + // } + // private final String myField; + // } + // So the constructor and the field declarations have no matching trees in the JavaParser + // node, and we must remove those trees (and their subtrees) from the `trees` field. + TreeScannerWithDefaults removeAllVisitor = + new TreeScannerWithDefaults() { + @Override + public void defaultAction(Tree node) { + trees.remove(node); + } + }; + for (Tree member : tree.getMembers()) { + visit(member, p); + if (TreeUtils.isAutoGeneratedRecordMember(member)) { + member.accept(removeAllVisitor, null); + } else { + // If the user declares a canonical constructor, javac will automatically fill in the + // parameters. These trees also don't have a match: + if (member.getKind() == Tree.Kind.METHOD) { + MethodTree methodTree = (MethodTree) member; + if (TreeUtils.isCanonicalRecordConstructor(methodTree)) { + for (VariableTree canonicalParameter : methodTree.getParameters()) { + canonicalParameter.accept(removeAllVisitor, null); + } + } + } + } + } } else { visit(tree.getMembers(), p); } diff --git a/framework/src/main/java/org/checkerframework/framework/ajava/JointJavacJavaParserVisitor.java b/framework/src/main/java/org/checkerframework/framework/ajava/JointJavacJavaParserVisitor.java index 483e5e317a9..6e7040b8a60 100644 --- a/framework/src/main/java/org/checkerframework/framework/ajava/JointJavacJavaParserVisitor.java +++ b/framework/src/main/java/org/checkerframework/framework/ajava/JointJavacJavaParserVisitor.java @@ -8,6 +8,7 @@ import com.github.javaparser.ast.body.AnnotationMemberDeclaration; import com.github.javaparser.ast.body.BodyDeclaration; import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.body.CompactConstructorDeclaration; import com.github.javaparser.ast.body.ConstructorDeclaration; import com.github.javaparser.ast.body.EnumConstantDeclaration; import com.github.javaparser.ast.body.EnumDeclaration; @@ -15,6 +16,7 @@ import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.ast.body.Parameter; import com.github.javaparser.ast.body.ReceiverParameter; +import com.github.javaparser.ast.body.RecordDeclaration; import com.github.javaparser.ast.body.VariableDeclarator; import com.github.javaparser.ast.expr.ArrayAccessExpr; import com.github.javaparser.ast.expr.AssignExpr; @@ -63,6 +65,7 @@ import com.github.javaparser.ast.stmt.IfStmt; import com.github.javaparser.ast.stmt.LabeledStmt; import com.github.javaparser.ast.stmt.LocalClassDeclarationStmt; +import com.github.javaparser.ast.stmt.LocalRecordDeclarationStmt; import com.github.javaparser.ast.stmt.ReturnStmt; import com.github.javaparser.ast.stmt.Statement; import com.github.javaparser.ast.stmt.SwitchEntry; @@ -79,7 +82,10 @@ import com.github.javaparser.ast.type.UnionType; import com.github.javaparser.ast.type.VoidType; import com.github.javaparser.ast.type.WildcardType; +import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; import com.google.common.collect.Iterators; +import com.google.common.collect.Lists; import com.google.common.collect.PeekingIterator; import com.sun.source.tree.AnnotatedTypeTree; import com.sun.source.tree.AnnotationTree; @@ -149,6 +155,7 @@ import java.util.List; import java.util.Optional; import org.checkerframework.javacutil.BugInCF; +import org.checkerframework.javacutil.TreeUtils; /** * A visitor that processes javac trees and JavaParser nodes simultaneously, matching corresponding @@ -437,12 +444,29 @@ public Void visitClass(ClassTree javacTree, Node javaParserNode) { } visitClassMembers(javacTree.getMembers(), node.getMembers()); + } else if (javaParserNode instanceof RecordDeclaration) { + RecordDeclaration node = (RecordDeclaration) javaParserNode; + processClass(javacTree, node); + visitLists(javacTree.getTypeParameters(), node.getTypeParameters()); + visitLists(javacTree.getImplementsClause(), node.getImplementedTypes()); + List membersWithoutAutoGenerated = + Lists.newArrayList( + Iterables.filter( + javacTree.getMembers(), + (Predicate) + (Tree m) -> { + // Filter out all auto-generated items: + return !TreeUtils.isAutoGeneratedRecordMember(m); + })); + visitClassMembers(membersWithoutAutoGenerated, node.getMembers()); } else if (javaParserNode instanceof AnnotationDeclaration) { AnnotationDeclaration node = (AnnotationDeclaration) javaParserNode; processClass(javacTree, node); visitClassMembers(javacTree.getMembers(), node.getMembers()); } else if (javaParserNode instanceof LocalClassDeclarationStmt) { javacTree.accept(this, ((LocalClassDeclarationStmt) javaParserNode).getClassDeclaration()); + } else if (javaParserNode instanceof LocalRecordDeclarationStmt) { + javacTree.accept(this, ((LocalRecordDeclarationStmt) javaParserNode).getRecordDeclaration()); } else if (javaParserNode instanceof EnumDeclaration) { EnumDeclaration node = (EnumDeclaration) javaParserNode; processClass(javacTree, node); @@ -897,6 +921,9 @@ public Void visitMethod(MethodTree javacTree, Node javaParserNode) { visitMethodForMethodDeclaration(javacTree, (MethodDeclaration) javaParserNode); } else if (javaParserNode instanceof ConstructorDeclaration) { visitMethodForConstructorDeclaration(javacTree, (ConstructorDeclaration) javaParserNode); + } else if (javaParserNode instanceof CompactConstructorDeclaration) { + visitMethodForConstructorDeclaration( + javacTree, (CompactConstructorDeclaration) javaParserNode); } else if (javaParserNode instanceof AnnotationMemberDeclaration) { visitMethodForAnnotationMemberDeclaration( javacTree, (AnnotationMemberDeclaration) javaParserNode); @@ -921,7 +948,9 @@ private void visitMethodForMethodDeclaration( // modifiers. This is a problem because a ModifiersTree has separate accessors to // annotations and other modifiers, so the order doesn't match. It might be that for // JavaParser, the annotations and other modifiers are also accessed separately. - javacTree.getReturnType().accept(this, javaParserNode.getType()); + if (javacTree.getReturnType() != null) { + javacTree.getReturnType().accept(this, javaParserNode.getType()); + } // Unlike other javac constructs, the javac list is non-null even if no type parameters are // present. visitLists(javacTree.getTypeParameters(), javaParserNode.getTypeParameters()); @@ -955,6 +984,21 @@ private void visitMethodForConstructorDeclaration( javacTree.getBody().accept(this, javaParserNode.getBody()); } + /** + * Visits a method declaration in the case where the matched JavaParser node was a {@code + * CompactConstructorDeclaration}. + * + * @param javacTree method declaration to visit + * @param javaParserNode corresponding JavaParser constructor declaration + */ + private void visitMethodForConstructorDeclaration( + MethodTree javacTree, CompactConstructorDeclaration javaParserNode) { + processMethod(javacTree, javaParserNode); + visitLists(javacTree.getTypeParameters(), javaParserNode.getTypeParameters()); + visitLists(javacTree.getThrows(), javaParserNode.getThrownExceptions()); + javacTree.getBody().accept(this, javaParserNode.getBody()); + } + /** * Visits a method declaration in the case where the matched JavaParser node was a {@code * AnnotationMemberDeclaration}. @@ -1541,6 +1585,14 @@ public abstract void processArrayAccess( public abstract void processClass( ClassTree javacTree, ClassOrInterfaceDeclaration javaParserNode); + /** + * Process a {@code ClassTree} representing a record declaration. + * + * @param javacTree tree to process + * @param javaParserNode corresponding JavaParser node + */ + public abstract void processClass(ClassTree javacTree, RecordDeclaration javaParserNode); + /** * Process a {@code ClassTree} representing an enum declaration. * @@ -1838,6 +1890,15 @@ public abstract void processMemberSelect( */ public abstract void processMethod(MethodTree javacTree, ConstructorDeclaration javaParserNode); + /** + * Process a {@code MethodTree} representing a compact constructor declaration. + * + * @param javacTree tree to process + * @param javaParserNode corresponding JavaParser node + */ + public abstract void processMethod( + MethodTree javacTree, CompactConstructorDeclaration javaParserNode); + /** * Process a {@code MethodTree} representing a value field for an annotation. * diff --git a/framework/src/main/java/org/checkerframework/framework/ajava/JointVisitorWithDefaultAction.java b/framework/src/main/java/org/checkerframework/framework/ajava/JointVisitorWithDefaultAction.java index 6a4deacd2a4..7d6f03661ea 100644 --- a/framework/src/main/java/org/checkerframework/framework/ajava/JointVisitorWithDefaultAction.java +++ b/framework/src/main/java/org/checkerframework/framework/ajava/JointVisitorWithDefaultAction.java @@ -7,12 +7,14 @@ import com.github.javaparser.ast.body.AnnotationDeclaration; import com.github.javaparser.ast.body.AnnotationMemberDeclaration; import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.body.CompactConstructorDeclaration; import com.github.javaparser.ast.body.ConstructorDeclaration; import com.github.javaparser.ast.body.EnumConstantDeclaration; import com.github.javaparser.ast.body.EnumDeclaration; import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.ast.body.Parameter; import com.github.javaparser.ast.body.ReceiverParameter; +import com.github.javaparser.ast.body.RecordDeclaration; import com.github.javaparser.ast.body.VariableDeclarator; import com.github.javaparser.ast.expr.ArrayAccessExpr; import com.github.javaparser.ast.expr.AssignExpr; @@ -225,6 +227,11 @@ public void processClass(ClassTree javacTree, EnumDeclaration javaParserNode) { defaultJointAction(javacTree, javaParserNode); } + @Override + public void processClass(ClassTree javacTree, RecordDeclaration javaParserNode) { + defaultJointAction(javacTree, javaParserNode); + } + @Override public void processCompilationUnit( CompilationUnitTree javacTree, CompilationUnit javaParserNode) { @@ -395,6 +402,11 @@ public void processMethod(MethodTree javacTree, ConstructorDeclaration javaParse defaultJointAction(javacTree, javaParserNode); } + @Override + public void processMethod(MethodTree javacTree, CompactConstructorDeclaration javaParserNode) { + defaultJointAction(javacTree, javaParserNode); + } + @Override public void processMethod(MethodTree javacTree, AnnotationMemberDeclaration javaParserNode) { defaultJointAction(javacTree, javaParserNode); diff --git a/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileElementTypes.java b/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileElementTypes.java index 026ce413038..818aafb00a2 100644 --- a/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileElementTypes.java +++ b/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileElementTypes.java @@ -33,6 +33,7 @@ import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Types; import javax.tools.Diagnostic.Kind; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.signature.qual.CanonicalNameOrEmpty; @@ -40,6 +41,7 @@ import org.checkerframework.framework.qual.StubFiles; import org.checkerframework.framework.source.SourceChecker; import org.checkerframework.framework.stub.AnnotationFileParser.AnnotationFileAnnotations; +import org.checkerframework.framework.stub.AnnotationFileParser.RecordComponentStub; import org.checkerframework.framework.stub.AnnotationFileUtil.AnnotationFileType; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; @@ -392,6 +394,73 @@ public Set getDeclAnnotations(Element elt) { return Collections.emptySet(); } + /** + * Adds annotations from stub files for the corresponding record components (if the given + * constructor/method is the canonical constructor or a record accessor). Such transfer is + * automatically done by javac usually, but not from stubs. + * + * @param types a Types instance used for checking type equivalence + * @param elt a member. This method does nothing if it's not a method or constructor. + * @param memberType the type corresponding to the element elt; side-effected by this method + */ + public void injectRecordComponentType( + Types types, Element elt, AnnotatedExecutableType memberType) { + if (parsing) { + throw new BugInCF("parsing while calling injectRecordComponentType"); + } + + if (elt.getKind() == ElementKind.METHOD) { + if (((ExecutableElement) elt).getParameters().isEmpty()) { + String recordName = ElementUtils.getQualifiedName(elt.getEnclosingElement()); + AnnotationFileParser.RecordStub recordComponentType = + annotationFileAnnos.records.get(recordName); + if (recordComponentType != null) { + // If the record component has an annotation in the stub, the component annotation + // replaces any from the same hierarchy on the accessor method, unless there is an + // accessor in the stubs file (which may or may not have an annotation in the same + // hierarchy; + // the user may want to specify the annotation or deliberately not annotate the accessor). + // We thus only replace the method annotation with the component annotation + // if there is no accessor in the stubs file: + RecordComponentStub recordComponentStub = + recordComponentType.componentsByName.get(elt.getSimpleName().toString()); + if (recordComponentStub != null && !recordComponentStub.hasAccessorInStubs()) + replaceAnnotations(memberType.getReturnType(), recordComponentStub.type); + } + } + } else if (elt.getKind() == ElementKind.CONSTRUCTOR) { + if (AnnotationFileUtil.isCanonicalConstructor((ExecutableElement) elt, types)) { + TypeElement enclosing = (TypeElement) elt.getEnclosingElement(); + AnnotationFileParser.RecordStub recordComponentType = + annotationFileAnnos.records.get(enclosing.getQualifiedName().toString()); + if (recordComponentType != null) { + List componentsInCanonicalConstructor = + recordComponentType.getComponentsInCanonicalConstructor(); + if (componentsInCanonicalConstructor != null) { + for (int i = 0; i < componentsInCanonicalConstructor.size(); i++) { + replaceAnnotations( + memberType.getParameterTypes().get(i), componentsInCanonicalConstructor.get(i)); + } + } + } + } + } + } + + /** + * Replace annotations on destType with those from srcType, first removing any annotations on + * destType that are in the same hierarchy as any on srcType. + * + * @param destType the type whose annotations to remove/replace + * @param srcType the type whose annotations are copied to {@code destType} + */ + private void replaceAnnotations(AnnotatedTypeMirror destType, AnnotatedTypeMirror srcType) { + for (AnnotationMirror annotation : srcType.getAnnotations()) { + destType.removeAnnotationInHierarchy(annotation); + } + destType.addAnnotations(srcType.getAnnotations()); + } + /** * Returns the method type of the most specific fake override for the given element, when used as * a member of the given type. 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 80e702fafc5..108fea21d4f 100644 --- a/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileParser.java +++ b/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileParser.java @@ -22,6 +22,7 @@ import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.ast.body.Parameter; import com.github.javaparser.ast.body.ReceiverParameter; +import com.github.javaparser.ast.body.RecordDeclaration; import com.github.javaparser.ast.body.TypeDeclaration; import com.github.javaparser.ast.body.VariableDeclarator; import com.github.javaparser.ast.expr.AnnotationExpr; @@ -43,6 +44,7 @@ import com.github.javaparser.ast.expr.StringLiteralExpr; import com.github.javaparser.ast.expr.UnaryExpr; import com.github.javaparser.ast.nodeTypes.NodeWithRange; +import com.github.javaparser.ast.nodeTypes.NodeWithTypeParameters; import com.github.javaparser.ast.nodeTypes.modifiers.NodeWithAccessModifiers; import com.github.javaparser.ast.type.ClassOrInterfaceType; import com.github.javaparser.ast.type.PrimitiveType; @@ -84,6 +86,7 @@ import javax.lang.model.util.Types; import javax.tools.Diagnostic; import org.checkerframework.checker.formatter.qual.FormatMethod; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.signature.qual.CanonicalName; import org.checkerframework.checker.signature.qual.DotSeparatedIdentifiers; @@ -107,6 +110,7 @@ import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.Pair; import org.checkerframework.javacutil.TreeUtils; +import org.plumelib.util.CollectionsPlume; // From an implementation perspective, this class represents a single annotation file (stub file or // ajava file), notably its annotated types and its declaration annotations. @@ -285,6 +289,80 @@ public static class AnnotationFileAnnotations { */ public final Map>> fakeOverrides = new HashMap<>(1); + + /** Maps fully qualified record name to information in the stub file. */ + public final Map records = new HashMap<>(); + } + + /** Information about a record from a stub file. */ + public static class RecordStub { + /** + * A map from name to record component. The iteration order is the order that they are declared + * in the record header. + */ + public final LinkedHashMap componentsByName; + /** + * If the canonical constructor is given in the stubs, the annotated types (in component + * declaration order) for the constructor. Null if not present in the stubs. + */ + public @MonotonicNonNull List componentsInCanonicalConstructor; + + /** + * Creates a new RecordStub. + * + * @param componentsByName a map from name to record component. The insertion/iteration order is + * the order that they are declared in the record header. + */ + public RecordStub(LinkedHashMap componentsByName) { + this.componentsByName = componentsByName; + } + + /** + * Returns the annotated types for the parameters to the canonical constructor. This is either + * from explicit annotations on the constructor in the stubs, otherwise it's taken from the + * annotations on the record components in the stubs. + * + * @return the annotated types for the parameters to the canonical constructor + */ + public List getComponentsInCanonicalConstructor() { + if (componentsInCanonicalConstructor != null) { + return componentsInCanonicalConstructor; + } else { + return CollectionsPlume.mapList(c -> c.type, componentsByName.values()); + } + } + } + + /** + * Information about a record component: its type, and whether there was an accessor in the stubs + * for that component. That is, for a component "foo" was there a method named exactly "foo()" in + * the stubs. If so, annotations on that accessor will take precedence over annotations that would + * otherwise be copied from the component in the stubs to the acessor. + */ + public static class RecordComponentStub { + /** The type of the record component. */ + public final AnnotatedTypeMirror type; + + /** Whether this component has an accessor of exactly the same name in the stubs file. */ + private boolean hasAccessorInStubs = false; + + /** + * Creates a new RecordComponentStub with the given type. + * + * @param type the type of the record component + */ + public RecordComponentStub(AnnotatedTypeMirror type) { + this.type = type; + } + + /** + * Returns whether there is an accessor in a stub file. + * + * @return true if some stub file contains an accessor for this record component + */ + public boolean hasAccessorInStubs() { + return hasAccessorInStubs; + } } /** @@ -829,7 +907,10 @@ private List processTypeDecl( + "..."); return null; } - typeDeclTypeParameters = processType((ClassOrInterfaceDeclaration) typeDecl, typeElt); + typeDeclTypeParameters = processType(typeDecl, typeElt); + typeParameters.addAll(typeDeclTypeParameters); + } else if (typeDecl instanceof RecordDeclaration) { + typeDeclTypeParameters = processType(typeDecl, typeElt); typeParameters.addAll(typeDeclTypeParameters); } // else it's an EmptyTypeDeclaration. TODO: An EmptyTypeDeclaration can have // annotations, right? @@ -840,6 +921,20 @@ private List processTypeDecl( return typeDeclTypeParameters; } + if (typeDecl instanceof RecordDeclaration) { + NodeList recordMembers = ((RecordDeclaration) typeDecl).getParameters(); + LinkedHashMap byName = new LinkedHashMap<>(); + for (Parameter recordMember : recordMembers) { + RecordComponentStub stub = + processRecordField( + recordMember, + findFieldElement(typeElt, recordMember.getNameAsString(), recordMember)); + byName.put(recordMember.getNameAsString(), stub); + } + annotationFileAnnos.records.put( + typeDecl.getFullyQualifiedName().get(), new RecordStub(byName)); + } + Pair>, Map>>> members = getMembers(typeDecl, typeElt, typeDecl); for (Map.Entry> entry : members.first.entrySet()) { @@ -850,7 +945,20 @@ private List processTypeDecl( processField((FieldDeclaration) decl, (VariableElement) elt); break; case ENUM_CONSTANT: - processEnumConstant((EnumConstantDeclaration) decl, (VariableElement) elt); + // Enum constants can occur as fields in stubs files when their + // type has an annotation on it, e.g. see DeviceTypeTest which ends up with + // the TRACKER enum constant annotated with DefaultType: + if (decl instanceof FieldDeclaration) { + processField((FieldDeclaration) decl, (VariableElement) elt); + } else if (decl instanceof EnumConstantDeclaration) { + processEnumConstant((EnumConstantDeclaration) decl, (VariableElement) elt); + } else { + throw new Error( + "Unexpected decl type " + + decl.getClass() + + " for ENUM_CONSTANT kind, original: " + + decl); + } break; case CONSTRUCTOR: case METHOD: @@ -912,15 +1020,19 @@ private boolean hasNoAnnotationFileParserWarning(Iterable aexprs * @param elt the type's element * @return the type's type parameter declarations */ - private List processType( - ClassOrInterfaceDeclaration decl, TypeElement elt) { + private List processType(TypeDeclaration decl, TypeElement elt) { recordDeclAnnotation(elt, decl.getAnnotations(), decl); AnnotatedDeclaredType type = atypeFactory.fromElement(elt); annotate(type, decl.getAnnotations(), decl); final List typeArguments = type.getTypeArguments(); - final List typeParameters = decl.getTypeParameters(); + final List typeParameters; + if (decl instanceof NodeWithTypeParameters) { + typeParameters = ((NodeWithTypeParameters) decl).getTypeParameters(); + } else { + typeParameters = Collections.emptyList(); + } // It can be the case that args=[] and params=null, so don't crash in that case. // if ((typeParameters == null) != (typeArguments == null)) { @@ -951,7 +1063,9 @@ private List processType( } annotateTypeParameters(decl, elt, typeArguments, typeParameters); - annotateSupertypes(decl, type); + if (decl instanceof ClassOrInterfaceDeclaration) { + annotateSupertypes((ClassOrInterfaceDeclaration) decl, type); + } putMerge(annotationFileAnnos.atypes, elt, type); List typeVariables = new ArrayList<>(type.getTypeArguments().size()); for (AnnotatedTypeMirror typeV : type.getTypeArguments()) { @@ -1070,17 +1184,45 @@ private List processCallableDeclaration( // Return type, from declaration annotations on the method or constructor if (decl.isMethodDeclaration()) { + MethodDeclaration methodDeclaration = (MethodDeclaration) decl; + if (methodDeclaration.getParameters().isEmpty()) { + String qualRecordName = ElementUtils.getQualifiedName(elt.getEnclosingElement()); + RecordStub recordStub = annotationFileAnnos.records.get(qualRecordName); + if (recordStub != null) { + RecordComponentStub recordComponentStub = + recordStub.componentsByName.get(methodDeclaration.getNameAsString()); + if (recordComponentStub != null) { + recordComponentStub.hasAccessorInStubs = true; + } + } + } + try { annotate( - methodType.getReturnType(), - ((MethodDeclaration) decl).getType(), - decl.getAnnotations(), - decl); + methodType.getReturnType(), methodDeclaration.getType(), decl.getAnnotations(), decl); } catch (ErrorTypeKindException e) { // Do nothing, per https://github.com/typetools/checker-framework/issues/244 . } } else { assert decl.isConstructorDeclaration(); + if (AnnotationFileUtil.isCanonicalConstructor(elt, atypeFactory.types)) { + // If this is the (user-written) canonical constructor, record that the component + // annotations should not be automatically transferred: + String qualRecordName = ElementUtils.getQualifiedName(elt.getEnclosingElement()); + if (annotationFileAnnos.records.containsKey(qualRecordName)) { + ArrayList annotatedParameters = new ArrayList<>(); + List parameters = elt.getParameters(); + for (int i = 0; i < parameters.size(); i++) { + VariableElement parameter = parameters.get(i); + AnnotatedTypeMirror atm = + AnnotatedTypeMirror.createType(parameter.asType(), atypeFactory, false); + annotate(atm, decl.getParameter(i).getAnnotations(), decl.getParameter(i)); + annotatedParameters.add(atm); + } + annotationFileAnnos.records.get(qualRecordName).componentsInCanonicalConstructor = + annotatedParameters; + } + } annotate(methodType.getReturnType(), decl.getAnnotations(), decl); } @@ -1410,6 +1552,25 @@ private void processField(FieldDeclaration decl, VariableElement elt) { putMerge(annotationFileAnnos.atypes, elt, fieldType); } + /** + * Processes a parameter in a record header (i.e., a record component). + * + * @param decl the parameter in the record header + * @param elt the corresponding variable declaration element + * @return a representation of the record component in the stub file + */ + private RecordComponentStub processRecordField(Parameter decl, VariableElement elt) { + markAsFromStubFile(elt); + recordDeclAnnotation(elt, decl.getAnnotations(), decl); + // AnnotationFileParser parses all annotations in type annotation position as type annotations. + recordDeclAnnotation(elt, decl.getType().getAnnotations(), decl); + AnnotatedTypeMirror fieldType = atypeFactory.fromElement(elt); + + annotate(fieldType, decl.getType(), decl.getAnnotations(), decl); + putMerge(annotationFileAnnos.atypes, elt, fieldType); + return new RecordComponentStub(fieldType); + } + /** * Adds the annotations present on the declaration of an enum constant to the ATM of that * constant. diff --git a/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileUtil.java b/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileUtil.java index 27ecc5d79fb..aca48ac583d 100644 --- a/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileUtil.java +++ b/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileUtil.java @@ -26,12 +26,15 @@ import java.util.jar.JarEntry; import java.util.jar.JarFile; import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; +import javax.lang.model.util.Types; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.signature.qual.FullyQualifiedName; import org.checkerframework.javacutil.BugInCF; +import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.Pair; /** Utility class for annotation files (stub files and ajava files). */ @@ -473,4 +476,34 @@ public int compare(File o1, File o2) { } } } + + /** + * Returns true if the given {@link ExecutableElement} is the canonical constructor of a record + * (i.e., the parameter types of the constructor correspond to the parameter types of the record + * components, ignoring annotations). + * + * @param elt the constructor/method to check + * @param types the Types instance to use for comparing types + * @return true if elt is the canonical constructor of the record containing it + */ + public static boolean isCanonicalConstructor(ExecutableElement elt, Types types) { + if (elt.getKind() == ElementKind.CONSTRUCTOR) { + Element enclosing = elt.getEnclosingElement(); + // Can't use RECORD enum constant as it's not available before JDK 16: + if (enclosing.getKind().name().equals("RECORD")) { + List recordComponents = + ElementUtils.getRecordComponents((TypeElement) enclosing); + if (recordComponents.size() == elt.getParameters().size()) { + for (int i = 0; i < recordComponents.size(); i++) { + if (!types.isSameType( + recordComponents.get(i).asType(), elt.getParameters().get(i).asType())) { + return false; + } + } + return true; + } + } + } + return false; + } } diff --git a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java index 02af5cd0064..4471cee719e 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java @@ -2235,6 +2235,7 @@ public ParameterizedExecutableType methodFromUse( getAnnotatedType(methodElt); // get unsubstituted type AnnotatedExecutableType memberTypeWithOverrides = applyFakeOverrides(receiverType, methodElt, memberTypeWithoutOverrides); + memberTypeWithOverrides = applyRecordTypesToAccessors(methodElt, memberTypeWithOverrides); methodFromUsePreSubstitution(tree, memberTypeWithOverrides); AnnotatedExecutableType methodType = @@ -2356,6 +2357,27 @@ private AnnotatedExecutableType applyFakeOverrides( return methodType; } + /** + * Given a method, checks if there is: a record component with the same name AND the record + * component has an annotation AND the method has no-arguments. If so, replaces the annotations on + * the method return type with those from the record type in the same hierarchy. + * + * @param member a method or constructor + * @param memberType the type of the method/constructor; side-effected by this method + * @return {@code memberType} with annotations replaced if applicable + */ + private AnnotatedExecutableType applyRecordTypesToAccessors( + ExecutableElement member, AnnotatedExecutableType memberType) { + if (memberType.getKind() != TypeKind.EXECUTABLE) { + throw new BugInCF( + "member %s has type %s of kind %s", member, memberType, memberType.getKind()); + } + + stubTypes.injectRecordComponentType(types, member, memberType); + + return memberType; + } + /** * A callback method for the AnnotatedTypeFactory subtypes to customize the handling of the * declared method type before type variable substitution. @@ -2551,6 +2573,8 @@ public ParameterizedExecutableType constructorFromUse(NewClassTree tree) { con = (AnnotatedExecutableType) typeVarSubstitutor.substitute(typeParamToTypeArg, con); } + stubTypes.injectRecordComponentType(types, ctor, con); + return new ParameterizedExecutableType(con, typeargs); } @@ -3435,8 +3459,8 @@ public final Tree declarationFromElement(Element elt) { Tree fromElt; // Prevent calling declarationFor on elements we know we don't have the tree for. - switch (elt.getKind()) { - case CLASS: + switch (ElementUtils.getKindRecordAsClass(elt)) { + case CLASS: // Including RECORD case ENUM: case INTERFACE: case ANNOTATION_TYPE: diff --git a/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java index 8eee820d32a..0228fa14460 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java @@ -1320,7 +1320,7 @@ protected void performFlowAnalysis(ClassTree classTree) { members.sort(sortVariablesFirst); } for (Tree m : members) { - switch (m.getKind()) { + switch (TreeUtils.getKindRecordAsClass(m)) { case METHOD: MethodTree mt = (MethodTree) m; @@ -1368,7 +1368,7 @@ protected void performFlowAnalysis(ClassTree classTree) { } fieldValues.add(new FieldInitialValue<>(fieldExpr, declaredValue, null)); break; - case CLASS: + case CLASS: // Including RECORD case ANNOTATION_TYPE: case INTERFACE: case ENUM: diff --git a/framework/src/main/java/org/checkerframework/framework/type/TypeFromExpressionVisitor.java b/framework/src/main/java/org/checkerframework/framework/type/TypeFromExpressionVisitor.java index 116a4e92428..da20d5e0cee 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/TypeFromExpressionVisitor.java +++ b/framework/src/main/java/org/checkerframework/framework/type/TypeFromExpressionVisitor.java @@ -35,6 +35,7 @@ import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedWildcardType; import org.checkerframework.framework.util.AnnotatedTypes; import org.checkerframework.javacutil.BugInCF; +import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.Pair; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; @@ -191,7 +192,7 @@ public AnnotatedTypeMirror visitMemberSelect(MemberSelectTree node, AnnotatedTyp // the type of a class literal is the type of the "class" element. return f.getAnnotatedType(elt); } - switch (elt.getKind()) { + switch (ElementUtils.getKindRecordAsClass(elt)) { case METHOD: case PACKAGE: // "java.lang" in new java.lang.Short("2") case CLASS: // o instanceof MyClass.InnerClass diff --git a/framework/src/main/java/org/checkerframework/framework/util/JavaParserUtil.java b/framework/src/main/java/org/checkerframework/framework/util/JavaParserUtil.java index 571c682c56e..49033bb616c 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/JavaParserUtil.java +++ b/framework/src/main/java/org/checkerframework/framework/util/JavaParserUtil.java @@ -32,9 +32,9 @@ public class JavaParserUtil { /** * The Language Level to use when parsing if a specific level isn't applied. This should be the - * highest version of Java that the Checker Framework can process. Currently, Java 11. + * highest version of Java that the Checker Framework can process. Currently, Java 16. */ - public static LanguageLevel DEFAULT_LANGUAGE_LEVEL = LanguageLevel.JAVA_11; + public static LanguageLevel DEFAULT_LANGUAGE_LEVEL = LanguageLevel.JAVA_16; /// /// Replacements for StaticJavaParser diff --git a/framework/src/main/java/org/checkerframework/framework/util/VoidVisitorWithDefaultAction.java b/framework/src/main/java/org/checkerframework/framework/util/VoidVisitorWithDefaultAction.java index bbde91e77ce..683b2706c30 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/VoidVisitorWithDefaultAction.java +++ b/framework/src/main/java/org/checkerframework/framework/util/VoidVisitorWithDefaultAction.java @@ -10,6 +10,7 @@ import com.github.javaparser.ast.body.AnnotationDeclaration; import com.github.javaparser.ast.body.AnnotationMemberDeclaration; import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.body.CompactConstructorDeclaration; import com.github.javaparser.ast.body.ConstructorDeclaration; import com.github.javaparser.ast.body.EnumConstantDeclaration; import com.github.javaparser.ast.body.EnumDeclaration; @@ -18,6 +19,7 @@ import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.ast.body.Parameter; import com.github.javaparser.ast.body.ReceiverParameter; +import com.github.javaparser.ast.body.RecordDeclaration; import com.github.javaparser.ast.body.VariableDeclarator; import com.github.javaparser.ast.comments.BlockComment; import com.github.javaparser.ast.comments.JavadocComment; @@ -78,6 +80,7 @@ import com.github.javaparser.ast.stmt.IfStmt; import com.github.javaparser.ast.stmt.LabeledStmt; import com.github.javaparser.ast.stmt.LocalClassDeclarationStmt; +import com.github.javaparser.ast.stmt.LocalRecordDeclarationStmt; import com.github.javaparser.ast.stmt.ReturnStmt; import com.github.javaparser.ast.stmt.SwitchEntry; import com.github.javaparser.ast.stmt.SwitchStmt; @@ -690,4 +693,22 @@ public void visit(YieldStmt n, Void p) { super.visit(n, p); defaultAction(n); } + + @Override + public void visit(RecordDeclaration n, Void p) { + super.visit(n, p); + defaultAction(n); + } + + @Override + public void visit(LocalRecordDeclarationStmt n, Void p) { + super.visit(n, p); + defaultAction(n); + } + + @Override + public void visit(CompactConstructorDeclaration n, Void p) { + super.visit(n, p); + defaultAction(n); + } } diff --git a/framework/src/main/java/org/checkerframework/framework/util/defaults/QualifierDefaults.java b/framework/src/main/java/org/checkerframework/framework/util/defaults/QualifierDefaults.java index 55146e941d1..a3bbc093ea0 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/defaults/QualifierDefaults.java +++ b/framework/src/main/java/org/checkerframework/framework/util/defaults/QualifierDefaults.java @@ -428,7 +428,7 @@ private Element nearestEnclosingExceptLocal(Tree tree) { Tree prev = null; for (Tree t : path) { - switch (t.getKind()) { + switch (TreeUtils.getKindRecordAsClass(t)) { case VARIABLE: VariableTree vtree = (VariableTree) t; ExpressionTree vtreeInit = vtree.getInitializer(); @@ -452,7 +452,7 @@ private Element nearestEnclosingExceptLocal(Tree tree) { return TreeUtils.elementFromDeclaration((VariableTree) t); case METHOD: return TreeUtils.elementFromDeclaration((MethodTree) t); - case CLASS: + case CLASS: // Including RECORD case ENUM: case INTERFACE: case ANNOTATION_TYPE: diff --git a/framework/tests/annotationclassloader/Makefile b/framework/tests/annotationclassloader/Makefile index 1b84c404286..010c8fb2163 100644 --- a/framework/tests/annotationclassloader/Makefile +++ b/framework/tests/annotationclassloader/Makefile @@ -61,7 +61,7 @@ load-from-dir-test: -Anomsgtext \ -ApermitMissingJdk \ LoaderTest.java > Out.txt 2>&1 || true - diff -u Expected.txt Out.txt -I 'Note' + diff -u Expected.txt Out.txt rm -f Out.txt # loads from framework.jar diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/ElementUtils.java b/javacutil/src/main/java/org/checkerframework/javacutil/ElementUtils.java index 5258c72954e..701c97e9554 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/ElementUtils.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/ElementUtils.java @@ -8,6 +8,7 @@ import com.sun.tools.javac.model.JavacTypes; import com.sun.tools.javac.processing.JavacProcessingEnvironment; import com.sun.tools.javac.util.Context; +import java.lang.reflect.InvocationTargetException; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; @@ -36,6 +37,7 @@ import javax.lang.model.util.Elements; import javax.lang.model.util.Types; import javax.tools.JavaFileObject; +import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.signature.qual.BinaryName; import org.checkerframework.checker.signature.qual.CanonicalName; @@ -911,4 +913,42 @@ public static Set getOverriddenMethods( public static boolean inSameClass(Element e1, Element e2) { return e1.getEnclosingElement().equals(e2.getEnclosingElement()); } + + /** + * Calls getKind() on the given Element, but returns CLASS if the ElementKind is RECORD. This is + * needed because the Checker Framework runs on JDKs before the RECORD item was added, so RECORD + * can't be used in case statements, and usually we want to treat them the same as classes. + * + * @param elt the element to get the kind for + * @return the kind of the element, but CLASS if the kind was RECORD + */ + public static ElementKind getKindRecordAsClass(Element elt) { + ElementKind kind = elt.getKind(); + if (kind.name().equals("RECORD")) { + kind = ElementKind.CLASS; + } + return kind; + } + + /** + * Calls getRecordComponents on the given TypeElement. Uses reflection because this method is not + * available before JDK 16. On earlier JDKs, which don't support records anyway, an exception is + * thrown. + * + * @param element the type element to call getRecordComponents on + * @return the return value of calling getRecordComponents, or empty list if the method is not + * available + */ + @SuppressWarnings({"unchecked", "nullness"}) // because of cast from reflection + public static List getRecordComponents(TypeElement element) { + try { + return (@NonNull List) + TypeElement.class.getMethod("getRecordComponents").invoke(element); + } catch (NoSuchMethodException + | IllegalAccessException + | IllegalArgumentException + | InvocationTargetException e) { + throw new Error("Cannot access TypeElement.getRecordComponents", e); + } + } } diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java index 334711a9755..1a3582aca40 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java @@ -99,6 +99,12 @@ private TreeUtils() { /** Unique IDs for trees. */ public static final UniqueIdMap treeUids = new UniqueIdMap<>(); + /** The value of Flags.GENERATED_MEMBER which does not exist in Java 9 or 11. */ + private static final long Flags_GENERATED_MEMBER = 16777216; + + /** The value of Flags.RECORD which does not exist in Java 9 or 11. */ + private static final long Flags_RECORD = 2305843009213693952L; + /** * Checks if the provided method is a constructor method or no. * @@ -1205,6 +1211,43 @@ public static boolean isAnonymousConstructor(final MethodTree method) { return typeElement.getNestingKind() == NestingKind.ANONYMOUS; } + /** + * Returns true if the given {@link MethodTree} is a canonical constructor (the constructor for a + * record where the parameters are implicitly declared and implicitly assigned to the record's + * fields). This may be an explicitly declared canonical constructor or an implicitly generated + * one. + * + * @param method a method tree that may be a canonical constructor + * @return true if the given method is a canonical constructor + */ + public static boolean isCanonicalRecordConstructor(final MethodTree method) { + @Nullable Element e = elementFromTree(method); + if (!(e instanceof Symbol)) { + return false; + } + + return (((Symbol) e).flags() & Flags_RECORD) != 0; + } + + /** + * Returns true if the given {@link Tree} is part of a record that has been automatically + * generated by the compiler. This can be a field that is derived from the record's header field + * list, or an automatically generated canonical constructor. + * + * @param member the {@link Tree} for a member of a record + * @return true if the given path is generated by the compiler + */ + public static boolean isAutoGeneratedRecordMember(final Tree member) { + Element e = elementFromTree(member); + if (!(e instanceof Symbol)) { + return false; + } + + // Generated constructors seem to get GENERATEDCONSTR even though the documentation + // seems to imply they would get GENERATED_MEMBER like the fields do: + return (((Symbol) e).flags() & (Flags_GENERATED_MEMBER | Flags.GENERATEDCONSTR)) != 0; + } + /** * Converts the given AnnotationTrees to AnnotationMirrors. * @@ -1697,4 +1740,21 @@ private static boolean isVarArgs(ExecutableElement method, List