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 e1f12f6777cf..3b1eec453821 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 @@ -896,4 +896,18 @@ public class HighlightClassUtil { } return null; } + + public static HighlightInfo checkIllegalInstanceMemberInRecord(PsiMember member) { + if (!member.hasModifierProperty(PsiModifier.STATIC)) { + PsiClass aClass = member.getContainingClass(); + if (aClass != null && aClass.isRecord()) { + HighlightInfo info = HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(member) + .descriptionAndTooltip(JavaErrorMessages.message(member instanceof PsiClassInitializer ? + "record.instance.initializer" : "record.instance.field")).create(); + QuickFixAction.registerQuickFixAction(info, QUICK_FIX_FACTORY.createModifierListFix(member, PsiModifier.STATIC, true, false)); + return info; + } + } + return null; + } } diff --git a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightMethodUtil.java b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightMethodUtil.java index e354574c4c7b..e0db4019cf98 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightMethodUtil.java +++ b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightMethodUtil.java @@ -1910,6 +1910,48 @@ public class HighlightMethodUtil { return Objects.requireNonNull(GenericsUtil.getLeastUpperBound(currentType, valueType, manager)); } + public static HighlightInfo checkRecordAccessorDeclaration(PsiMethod method) { + PsiRecordComponent component = JavaPsiRecordUtil.getRecordComponentForAccessor(method); + if (component == null) return null; + PsiIdentifier identifier = method.getNameIdentifier(); + if (identifier == null) return null; + PsiType componentType = component.getType(); + PsiType methodType = method.getReturnType(); + if (methodType == null) return null; // Either constructor or incorrect method, will be reported in another way + if (!componentType.equals(methodType)) { + String message = + JavaErrorMessages.message("record.accessor.wrong.return.type", componentType.getPresentableText(), methodType.getPresentableText()); + HighlightInfo info = HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range( + Objects.requireNonNull(method.getReturnTypeElement())).descriptionAndTooltip(message).create(); + QuickFixAction.registerQuickFixAction(info, QUICK_FIX_FACTORY.createMethodReturnFix(method, componentType, false)); + return info; + } + PsiTypeParameterList typeParameterList = method.getTypeParameterList(); + if (typeParameterList != null && typeParameterList.getTypeParameters().length > 0) { + HighlightInfo info = HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(typeParameterList) + .descriptionAndTooltip(JavaErrorMessages.message("record.accessor.type.parameters")) + .create(); + QuickFixAction.registerQuickFixAction(info, QUICK_FIX_FACTORY.createDeleteFix(typeParameterList)); + return info; + } + if (!method.hasModifierProperty(PsiModifier.PUBLIC)) { + HighlightInfo info = HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(identifier) + .descriptionAndTooltip(JavaErrorMessages.message("record.accessor.non.public")) + .create(); + QuickFixAction.registerQuickFixAction(info, QUICK_FIX_FACTORY.createModifierListFix(method, PsiModifier.PUBLIC, true, false)); + return info; + } + PsiReferenceList throwsList = method.getThrowsList(); + if (throwsList.getReferenceElements().length > 0) { + HighlightInfo info = HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(throwsList) + .descriptionAndTooltip(JavaErrorMessages.message("record.accessor.throws")) + .create(); + QuickFixAction.registerQuickFixAction(info, QUICK_FIX_FACTORY.createDeleteFix(throwsList)); + return info; + } + return null; + } + private static class ReturnModel { final PsiReturnStatement myStatement; final PsiType myType; 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 603126192ab0..462d053f0a1f 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 @@ -946,7 +946,7 @@ public class HighlightUtil extends HighlightUtilBase { isAllowed &= !isInterface; } - if (containingClass != null && containingClass.isInterface()) { + if (containingClass != null && (containingClass.isInterface() || containingClass.isRecord())) { isAllowed &= !PsiModifier.NATIVE.equals(modifier); } @@ -954,6 +954,10 @@ public class HighlightUtil extends HighlightUtilBase { isAllowed &= !PsiModifier.STATIC.equals(modifier); isAllowed &= !PsiModifier.DEFAULT.equals(modifier); } + + if (JavaPsiRecordUtil.getRecordComponentForAccessor(method) != null) { + isAllowed &= !PsiModifier.STATIC.equals(modifier); + } } else if (modifierOwner instanceof PsiField) { if (PsiModifier.PRIVATE.equals(modifier) || PsiModifier.PROTECTED.equals(modifier) || PsiModifier.TRANSIENT.equals(modifier) || 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 42482fca83fa..712d62cb4086 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 @@ -476,6 +476,7 @@ public class HighlightVisitorImpl extends JavaElementVisitor implements Highligh @Override public void visitClassInitializer(PsiClassInitializer initializer) { super.visitClassInitializer(initializer); + if (!myHolder.hasErrorResults()) myHolder.add(HighlightClassUtil.checkIllegalInstanceMemberInRecord(initializer)); if (!myHolder.hasErrorResults()) myHolder.add(HighlightControlFlowUtil.checkInitializerCompleteNormally(initializer)); if (!myHolder.hasErrorResults()) myHolder.add(HighlightControlFlowUtil.checkUnreachableStatement(initializer.getBody())); if (!myHolder.hasErrorResults()) { @@ -639,6 +640,7 @@ public class HighlightVisitorImpl extends JavaElementVisitor implements Highligh @Override public void visitField(PsiField field) { super.visitField(field); + if (!myHolder.hasErrorResults()) myHolder.add(HighlightClassUtil.checkIllegalInstanceMemberInRecord(field)); if (!myHolder.hasErrorResults()) myHolder.add(HighlightControlFlowUtil.checkFinalFieldInitialized(field)); } @@ -879,6 +881,7 @@ public class HighlightVisitorImpl extends JavaElementVisitor implements Highligh if (!myHolder.hasErrorResults()) myHolder.add(HighlightMethodUtil.checkConstructorHandleSuperClassExceptions(method)); if (!myHolder.hasErrorResults()) myHolder.add(HighlightMethodUtil.checkRecursiveConstructorInvocation(method)); if (!myHolder.hasErrorResults()) myHolder.add(GenericsHighlightUtil.checkSafeVarargsAnnotation(method, myLanguageLevel)); + if (!myHolder.hasErrorResults()) myHolder.add(HighlightMethodUtil.checkRecordAccessorDeclaration(method)); PsiClass aClass = method.getContainingClass(); if (!myHolder.hasErrorResults() && method.isConstructor()) { diff --git a/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/AddExceptionToThrowsFix.java b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/AddExceptionToThrowsFix.java index 9ed087410e6a..2248800231fa 100644 --- a/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/AddExceptionToThrowsFix.java +++ b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/AddExceptionToThrowsFix.java @@ -21,7 +21,6 @@ import com.intellij.codeInsight.daemon.QuickFixBundle; import com.intellij.codeInsight.intention.impl.BaseIntentionAction; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.WriteAction; -import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Messages; @@ -41,7 +40,6 @@ import java.util.*; * @author mike */ public class AddExceptionToThrowsFix extends BaseIntentionAction { - private static final Logger LOG = Logger.getInstance(AddExceptionToThrowsFix.class); private final PsiElement myWrongElement; public AddExceptionToThrowsFix(@NotNull PsiElement wrongElement) { @@ -199,6 +197,7 @@ public class AddExceptionToThrowsFix extends BaseIntentionAction { } if (targetElement == null || targetMethod == null || !targetMethod.getThrowsList().isPhysical()) return null; + if (!ExceptionUtil.canDeclareThrownExceptions(targetMethod)) return null; List exceptions = getUnhandledExceptions(myWrongElement, targetElement, targetMethod); if (exceptions == null || exceptions.isEmpty()) return null; unhandled.addAll(exceptions); diff --git a/java/java-psi-api/src/com/intellij/psi/util/JavaPsiRecordUtil.java b/java/java-psi-api/src/com/intellij/psi/util/JavaPsiRecordUtil.java new file mode 100644 index 000000000000..3a1375807435 --- /dev/null +++ b/java/java-psi-api/src/com/intellij/psi/util/JavaPsiRecordUtil.java @@ -0,0 +1,32 @@ +// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.psi.util; + +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiRecordComponent; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Utility methods to support Java records + */ +public class JavaPsiRecordUtil { + /** + * @param accessor accessor method for record component + * @return a corresponding record component, or null if the supplied method is not an accessor for the record component. + * Note that if accessor is not well-formed (e.g. has wrong return type), the corresponding record component will still be returned. + */ + @Nullable + public static PsiRecordComponent getRecordComponentForAccessor(@NotNull PsiMethod accessor) { + PsiClass aClass = accessor.getContainingClass(); + if (aClass == null || !aClass.isRecord()) return null; + if (!accessor.getParameterList().isEmpty()) return null; + String name = accessor.getName(); + for (PsiRecordComponent c : aClass.getRecordComponents()) { + if (name.equals(c.getName())) { + return c; + } + } + return null; + } +} diff --git a/java/java-psi-impl/src/com/intellij/codeInsight/ExceptionUtil.java b/java/java-psi-impl/src/com/intellij/codeInsight/ExceptionUtil.java index 5dab4e76bcdc..9261e002d691 100644 --- a/java/java-psi-impl/src/com/intellij/codeInsight/ExceptionUtil.java +++ b/java/java-psi-impl/src/com/intellij/codeInsight/ExceptionUtil.java @@ -921,4 +921,12 @@ public class ExceptionUtil { } } } + + /** + * @param method method + * @return true if given method can declare thrown exceptions, according to the specification; false otherwise + */ + public static boolean canDeclareThrownExceptions(@NotNull PsiMethod method) { + return JavaPsiRecordUtil.getRecordComponentForAccessor(method) == null; + } } diff --git a/java/java-psi-impl/src/messages/JavaErrorMessages.properties b/java/java-psi-impl/src/messages/JavaErrorMessages.properties index 747339c4c21b..625fcaf50db3 100644 --- a/java/java-psi-impl/src/messages/JavaErrorMessages.properties +++ b/java/java-psi-impl/src/messages/JavaErrorMessages.properties @@ -472,6 +472,12 @@ 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}'' +record.instance.initializer=Instance initializer is not allowed in record +record.instance.field=Instance field is not allowed in record +record.accessor.wrong.return.type=Incorrect component accessor return type. Expected: ''{0}'', found: ''{1}'' +record.accessor.type.parameters=Record component accessor cannot have type parameters +record.accessor.non.public=Record component accessor must be 'public' +record.accessor.throws=Record component accessor cannot declare thrown exceptions feature.generics=Generics feature.annotations=Annotations diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advHighlightingRecords/RecordAccessors.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advHighlightingRecords/RecordAccessors.java new file mode 100644 index 000000000000..5c7e19dba905 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advHighlightingRecords/RecordAccessors.java @@ -0,0 +1,20 @@ +record Rec(int x, int y) { + public void x() {} + + public void y(int x) {} +} +record RecTypeParam(int x) { + public int x() {return this.x;} +} +record RecStaticAccessor(int x) { + public static int x() {return 0;} +} +record RecNonPublic(int x, int y, int z) { + protected int x() {return x;} + int y() {return y;} + private int z() {return z;} +} +record RecThrows(int x) { + public int x() throws Exception {return x;} + public int y() throws Exception {return x;} +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advHighlightingRecords/RecordBasics.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advHighlightingRecords/RecordBasics.java index 552ad5726009..adb7396318c0 100644 --- a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advHighlightingRecords/RecordBasics.java +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advHighlightingRecords/RecordBasics.java @@ -34,4 +34,20 @@ record IllegalComponentName( record AnnotatedComponents( @SimpleAnno int x, @ConstructorAnno int y, - @MethodAnno int z) {} \ No newline at end of file + @MethodAnno int z) {} + +class Outer { + record NestedRecord() {} + class Inner { + record InnerRecord() {} + } +} + +record ProhibitedMembers() { + int x = 5; + + { + System.out.println("initializer"); + } + native void test(); +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addToThrows/beforeRecordAccessor.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addToThrows/beforeRecordAccessor.java new file mode 100644 index 000000000000..2194dadd80d1 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addToThrows/beforeRecordAccessor.java @@ -0,0 +1,6 @@ +// "Add exception to method signature" "false" +record X(int foo) { + public int foo() { + throw new Exception(); + } +} \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/LightRecordsHighlightingTest.java b/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/LightRecordsHighlightingTest.java index 66d5a76ae721..de81374f2c64 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/LightRecordsHighlightingTest.java +++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/LightRecordsHighlightingTest.java @@ -21,6 +21,9 @@ public class LightRecordsHighlightingTest extends LightJavaCodeInsightFixtureTes public void testRecordBasics() { doTest(); } + public void testRecordAccessors() { + doTest(); + } private void doTest() { myFixture.configureByFile(getTestName(false) + ".java"); diff --git a/plugins/IntentionPowerPak/src/com/siyeh/ipp/modifiers/ChangeModifierIntention.java b/plugins/IntentionPowerPak/src/com/siyeh/ipp/modifiers/ChangeModifierIntention.java index 8c599b00e4df..52e17a044741 100644 --- a/plugins/IntentionPowerPak/src/com/siyeh/ipp/modifiers/ChangeModifierIntention.java +++ b/plugins/IntentionPowerPak/src/com/siyeh/ipp/modifiers/ChangeModifierIntention.java @@ -38,6 +38,7 @@ import com.intellij.psi.codeStyle.CodeStyleManager; import com.intellij.psi.impl.source.resolve.JavaResolveUtil; import com.intellij.psi.search.SearchScope; import com.intellij.psi.search.searches.ReferencesSearch; +import com.intellij.psi.util.JavaPsiRecordUtil; import com.intellij.psi.util.MethodSignatureUtil; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.util.PsiUtil; @@ -129,6 +130,9 @@ public class ChangeModifierIntention extends BaseElementAtCaretIntentionAction { } if (member instanceof PsiMethod) { if (containingClass == null || containingClass.isEnum() && ((PsiMethod)member).isConstructor()) return Collections.emptyList(); + if (JavaPsiRecordUtil.getRecordComponentForAccessor((PsiMethod)member) != null) { + return Collections.singletonList(AccessModifier.PUBLIC); + } if (containingClass.isInterface()) { if (PsiUtil.isLanguageLevel9OrHigher(member)) { return PUBLIC_PRIVATE;