diff --git a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightClassUtil.java b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightClassUtil.java index cda667df3a14..e1f12f6777cf 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightClassUtil.java +++ b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightClassUtil.java @@ -404,11 +404,14 @@ public class HighlightClassUtil { static HighlightInfo checkExtendsAllowed(@NotNull PsiReferenceList list) { if (list.getParent() instanceof PsiClass) { PsiClass aClass = (PsiClass)list.getParent(); - if (aClass.isEnum()) { + if (aClass.isEnum() || aClass.isRecord()) { boolean isExtends = list.equals(aClass.getExtendsList()); if (isExtends) { - String description = JavaErrorMessages.message("extends.after.enum"); - return HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(list).descriptionAndTooltip(description).create(); + String description = JavaErrorMessages.message(aClass.isRecord() ? "record.extends" : "extends.after.enum"); + HighlightInfo info = + HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(list).descriptionAndTooltip(description).create(); + QuickFixAction.registerQuickFixAction(info, QUICK_FIX_FACTORY.createDeleteFix(list)); + return info; } } } @@ -873,4 +876,24 @@ public class HighlightClassUtil { } return null; } + + public static HighlightInfo checkWellFormedRecord(PsiClass psiClass) { + PsiRecordHeader header = psiClass.getRecordHeader(); + if (!psiClass.isRecord()) { + if (header != null) { + HighlightInfo info = HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(header) + .descriptionAndTooltip(JavaErrorMessages.message("record.header.regular.class")).create(); + QuickFixAction.registerQuickFixAction(info, QUICK_FIX_FACTORY.createDeleteFix(header)); + return info; + } + return null; + } + PsiIdentifier identifier = psiClass.getNameIdentifier(); + if (identifier == null) return null; + if (header == null) { + return HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(identifier) + .descriptionAndTooltip(JavaErrorMessages.message("record.no.header")).create(); + } + return null; + } } diff --git a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightUtil.java b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightUtil.java index 7d4f62f35ce6..603126192ab0 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightUtil.java +++ b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightUtil.java @@ -86,6 +86,8 @@ public class HighlightUtil extends HighlightUtilBase { ContainerUtil.newTroveSet(PsiModifier.ABSTRACT, PsiModifier.STATIC, PsiModifier.NATIVE, PsiModifier.FINAL, PsiModifier.STRICTFP, PsiModifier.SYNCHRONIZED); private static final String SERIAL_PERSISTENT_FIELDS_FIELD_NAME = "serialPersistentFields"; + private static final Set RESTRICTED_RECORD_COMPONENT_NAMES = ContainerUtil.immutableSet( + "clone", "finalize", "getClass", "hashCode", "notify", "notifyAll", "toString", "wait"); static { ourClassIncompatibleModifiers.put(PsiModifier.ABSTRACT, ContainerUtil.newTroveSet(PsiModifier.FINAL)); @@ -915,6 +917,10 @@ public class HighlightUtil extends HighlightUtilBase { isAllowed &= !(PsiModifier.FINAL.equals(modifier) || PsiModifier.ABSTRACT.equals(modifier)); } + if (aClass.isRecord()) { + isAllowed &= !PsiModifier.ABSTRACT.equals(modifier); + } + if (aClass.getContainingClass() instanceof PsiAnonymousClass) { isAllowed &= !privateOrProtected; } @@ -958,7 +964,8 @@ public class HighlightUtil extends HighlightUtilBase { else if (modifierOwner instanceof PsiClassInitializer) { isAllowed = PsiModifier.STATIC.equals(modifier); } - else if (modifierOwner instanceof PsiLocalVariable || modifierOwner instanceof PsiParameter) { + else if (modifierOwner instanceof PsiLocalVariable || modifierOwner instanceof PsiParameter || + modifierOwner instanceof PsiRecordComponent) { isAllowed = PsiModifier.FINAL.equals(modifier); } else if (modifierOwner instanceof PsiReceiverParameter) { @@ -1464,6 +1471,26 @@ public class HighlightUtil extends HighlightUtilBase { return infos; } + static HighlightInfo checkRecordComponentName(PsiRecordComponent component) { + PsiIdentifier identifier = component.getNameIdentifier(); + if (identifier != null) { + String name = identifier.getText(); + if (RESTRICTED_RECORD_COMPONENT_NAMES.contains(name)) { + return HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(identifier) + .descriptionAndTooltip(JavaErrorMessages.message("record.component.restricted.name", name)).create(); + } + } + return null; + } + + static HighlightInfo checkRecordComponentVarArg(PsiRecordComponent recordComponent) { + if (recordComponent.isVarArgs() && PsiTreeUtil.getNextSiblingOfType(recordComponent, PsiRecordComponent.class) != null) { + return HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(recordComponent) + .descriptionAndTooltip(JavaErrorMessages.message("record.component.vararg.not.last")).create(); + } + return null; + } + private enum SelectorKind { INT, ENUM, STRING } private static SelectorKind getSwitchSelectorKind(@NotNull PsiType type) { diff --git a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightVisitorImpl.java b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightVisitorImpl.java index 6a86f33370ba..42482fca83fa 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightVisitorImpl.java +++ b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightVisitorImpl.java @@ -469,6 +469,7 @@ public class HighlightVisitorImpl extends JavaElementVisitor implements Highligh if (!myHolder.hasErrorResults()) myHolder.add(HighlightUtil.checkImplicitThisReferenceBeforeSuper(aClass, myJavaSdkVersion)); if (!myHolder.hasErrorResults()) myHolder.add(HighlightClassUtil.checkClassAndPackageConflict(aClass)); if (!myHolder.hasErrorResults()) myHolder.add(HighlightClassUtil.checkPublicClassInRightFile(aClass)); + if (!myHolder.hasErrorResults()) myHolder.add(HighlightClassUtil.checkWellFormedRecord(aClass)); if (!myHolder.hasErrorResults()) myHolder.add(GenericsHighlightUtil.checkTypeParameterOverrideEquivalentMethods(aClass, myLanguageLevel)); } @@ -1062,6 +1063,13 @@ public class HighlightVisitorImpl extends JavaElementVisitor implements Highligh } } + @Override + public void visitRecordComponent(PsiRecordComponent recordComponent) { + super.visitRecordComponent(recordComponent); + if (!myHolder.hasErrorResults()) myHolder.add(HighlightUtil.checkRecordComponentVarArg(recordComponent)); + if (!myHolder.hasErrorResults()) myHolder.add(HighlightUtil.checkRecordComponentName(recordComponent)); + } + @Override public void visitParameter(PsiParameter parameter) { super.visitParameter(parameter); diff --git a/java/java-psi-api/src/com/intellij/codeInsight/AnnotationTargetUtil.java b/java/java-psi-api/src/com/intellij/codeInsight/AnnotationTargetUtil.java index 56c572234c2c..4d97f8f4d8f2 100644 --- a/java/java-psi-api/src/com/intellij/codeInsight/AnnotationTargetUtil.java +++ b/java/java-psi-api/src/com/intellij/codeInsight/AnnotationTargetUtil.java @@ -33,6 +33,7 @@ public class AnnotationTargetUtil { private static final TargetType[] CONSTRUCTOR_TARGETS = {TargetType.CONSTRUCTOR, TargetType.TYPE_USE}; private static final TargetType[] METHOD_TARGETS = {TargetType.METHOD, TargetType.TYPE_USE}; private static final TargetType[] FIELD_TARGETS = {TargetType.FIELD, TargetType.TYPE_USE}; + private static final TargetType[] RECORD_COMPONENT_TARGETS = {TargetType.RECORD_COMPONENT, TargetType.FIELD, TargetType.METHOD, TargetType.TYPE_USE}; private static final TargetType[] PARAMETER_TARGETS = {TargetType.PARAMETER, TargetType.TYPE_USE}; private static final TargetType[] LOCAL_VARIABLE_TARGETS = {TargetType.LOCAL_VARIABLE, TargetType.TYPE_USE}; private static final TargetType[] MODULE_TARGETS = {TargetType.MODULE}; @@ -67,6 +68,9 @@ public class AnnotationTargetUtil { return TYPE_TARGETS; } } + if (element instanceof PsiRecordComponent) { + return RECORD_COMPONENT_TARGETS; + } if (element instanceof PsiMethod) { if (((PsiMethod)element).isConstructor()) { return CONSTRUCTOR_TARGETS; diff --git a/java/java-psi-api/src/com/intellij/psi/PsiAnnotation.java b/java/java-psi-api/src/com/intellij/psi/PsiAnnotation.java index b2c61fa6768b..cc69c0a16561 100644 --- a/java/java-psi-api/src/com/intellij/psi/PsiAnnotation.java +++ b/java/java-psi-api/src/com/intellij/psi/PsiAnnotation.java @@ -32,7 +32,7 @@ public interface PsiAnnotation extends PsiAnnotationMemberValue, JvmAnnotation { */ enum TargetType { // see java.lang.annotation.ElementType - TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE, TYPE_USE, TYPE_PARAMETER, MODULE, + TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE, TYPE_USE, TYPE_PARAMETER, MODULE, RECORD_COMPONENT, // auxiliary value, used when it's impossible to determine annotation's targets UNKNOWN; diff --git a/java/java-psi-api/src/com/intellij/psi/PsiRecordComponent.java b/java/java-psi-api/src/com/intellij/psi/PsiRecordComponent.java index e5e7a807ebe7..735b7be82049 100644 --- a/java/java-psi-api/src/com/intellij/psi/PsiRecordComponent.java +++ b/java/java-psi-api/src/com/intellij/psi/PsiRecordComponent.java @@ -3,4 +3,13 @@ package com.intellij.psi; public interface PsiRecordComponent extends PsiMember, PsiVariable { PsiRecordComponent[] EMPTY_ARRAY = new PsiRecordComponent[]{}; + + /** + * Checks if the record component accepts a variable number of arguments in canonical constructor. + * + * @return true if the record component is a vararg, false otherwise + */ + default boolean isVarArgs() { + return getType() instanceof PsiEllipsisType; + } } diff --git a/java/java-psi-impl/src/com/intellij/psi/impl/source/PsiModifierListImpl.java b/java/java-psi-impl/src/com/intellij/psi/impl/source/PsiModifierListImpl.java index b769d2d69d85..367335fb2fb5 100644 --- a/java/java-psi-impl/src/com/intellij/psi/impl/source/PsiModifierListImpl.java +++ b/java/java-psi-impl/src/com/intellij/psi/impl/source/PsiModifierListImpl.java @@ -120,6 +120,12 @@ public class PsiModifierListImpl extends JavaStubPsiElement implicitModifiers.add(STATIC); } } + if (((PsiClass)parent).isRecord()) { + if (!(grandParent instanceof PsiFile)) { + implicitModifiers.add(STATIC); + } + implicitModifiers.add(FINAL); + } if (((PsiClass)parent).isEnum()) { if (!(grandParent instanceof PsiFile)) { implicitModifiers.add(STATIC); @@ -155,6 +161,9 @@ public class PsiModifierListImpl extends JavaStubPsiElement implicitModifiers.add(PRIVATE); } } + else if (parent instanceof PsiRecordComponent) { + implicitModifiers.add(FINAL); + } else if (parent instanceof PsiField) { if (parent instanceof PsiEnumConstant) { Collections.addAll(implicitModifiers, PUBLIC, STATIC, FINAL); diff --git a/java/java-psi-impl/src/messages/JavaErrorMessages.properties b/java/java-psi-impl/src/messages/JavaErrorMessages.properties index 93d234db4824..747339c4c21b 100644 --- a/java/java-psi-impl/src/messages/JavaErrorMessages.properties +++ b/java/java-psi-impl/src/messages/JavaErrorMessages.properties @@ -59,6 +59,8 @@ annotation.target.LOCAL_VARIABLE=local variable annotation.target.PACKAGE=package # suppress inspection "UnusedProperty" annotation.target.MODULE=module +# suppress inspection "UnusedProperty" +annotation.target.RECORD_COMPONENT=record component # generics related messages generics.holder.type=Type @@ -465,6 +467,12 @@ lvti.null=Cannot infer type: variable initializer is 'null' lvti.void=Cannot infer type: variable initializer is 'void' lvti.selfReferenced=Cannot infer type: variable initializer is self-referencing +record.no.header=Record has no header declared +record.header.regular.class=Record header declared for non-record +record.extends=No extends clause allowed for record +record.component.vararg.not.last=Vararg record component must be the last in the list +record.component.restricted.name=Illegal record component name ''{0}'' + feature.generics=Generics feature.annotations=Annotations feature.static.imports=Static imports diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advHighlightingRecords/RecordBasics.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advHighlightingRecords/RecordBasics.java new file mode 100644 index 000000000000..552ad5726009 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advHighlightingRecords/RecordBasics.java @@ -0,0 +1,37 @@ +import java.lang.annotation.*; + +record NoComponentList {} +record NoComponents() {} +class ClassWithComponents(int x) {} +class ClassWithComponents2(int x, int y) {} +abstract record AbstractRecord() {} +record ExtendsObject() extends Object {} +class ExtendsRecord extends NoComponents {} + +record ComponentModifiers( + public int x, + static int y, + final int z) {} +record ComponentDuplicateName(int x, int x) {} +record VarArgOk(int... x) {} +record VarArgOk2(int x, int... y) {} +record VarArgNotOk(int... x, int y) {} +record IllegalComponentName( + int clone, + int finalize, + int getClass, + int hashCode, + int notify, + int notifyAll, + int toString, + int wait) {} + +@interface SimpleAnno {} +@Target(ElementType.CONSTRUCTOR) +@interface ConstructorAnno {} +@Target(ElementType.METHOD) +@interface MethodAnno {} +record AnnotatedComponents( + @SimpleAnno int x, + @ConstructorAnno int y, + @MethodAnno int z) {} \ No newline at end of file