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 e0db4019cf98..1c3280681834 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 @@ -1271,17 +1271,22 @@ public class HighlightMethodUtil { return info; } - static HighlightInfo checkConstructorCallMustBeFirstStatement(@NotNull PsiMethodCallExpression methodCall) { + static HighlightInfo checkConstructorCallProblems(@NotNull PsiMethodCallExpression methodCall) { if (!JavaPsiConstructorUtil.isConstructorCall(methodCall)) return null; PsiElement codeBlock = methodCall.getParent().getParent(); - if (codeBlock instanceof PsiCodeBlock - && codeBlock.getParent() instanceof PsiMethod - && ((PsiMethod)codeBlock.getParent()).isConstructor()) { - PsiElement prevSibling = methodCall.getParent().getPrevSibling(); - while (true) { - if (prevSibling == null) return null; - if (prevSibling instanceof PsiStatement) break; - prevSibling = prevSibling.getPrevSibling(); + if (codeBlock instanceof PsiCodeBlock) { + PsiMethod ctor = ObjectUtils.tryCast(codeBlock.getParent(), PsiMethod.class); + if (ctor != null && ctor.isConstructor()) { + if (JavaPsiRecordUtil.isCanonicalConstructor(ctor)) { + String message = JavaErrorMessages.message("record.constructor.call.in.canonical"); + return HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(methodCall).descriptionAndTooltip(message).create(); + } + PsiElement prevSibling = methodCall.getParent().getPrevSibling(); + while (true) { + if (prevSibling == null) return null; + if (prevSibling instanceof PsiStatement) break; + prevSibling = prevSibling.getPrevSibling(); + } } } PsiReferenceExpression expression = methodCall.getMethodExpression(); @@ -1926,17 +1931,62 @@ public class HighlightMethodUtil { QuickFixAction.registerQuickFixAction(info, QUICK_FIX_FACTORY.createMethodReturnFix(method, componentType, false)); return info; } + return checkRecordSpecialMethodDeclaration(method, JavaErrorMessages.message("record.accessor")); + } + + public static HighlightInfo checkRecordConstructorDeclaration(PsiMethod method) { + if (!method.isConstructor()) return null; + PsiClass aClass = method.getContainingClass(); + if (aClass == null || !aClass.isRecord()) return null; + if (JavaPsiRecordUtil.isCanonicalConstructor(method)) { + PsiParameter[] parameters = method.getParameterList().getParameters(); + PsiRecordComponent[] components = aClass.getRecordComponents(); + assert parameters.length == components.length; + for (int i = 0; i < parameters.length; i++) { + PsiType componentType = components[i].getType(); + PsiType parameterType = parameters[i].getType(); + if (!parameterType.equals(componentType)) { + String message = + JavaErrorMessages.message("record.canonical.constructor.wrong.parameter.type", components[i].getName(), + componentType.getPresentableText(), parameterType.getPresentableText()); + HighlightInfo info = HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range( + Objects.requireNonNull(parameters[i].getTypeElement())).descriptionAndTooltip(message).create(); + QuickFixAction.registerQuickFixAction(info, QUICK_FIX_FACTORY.createMethodParameterTypeFix(method, i, componentType, false)); + return info; + } + } + return checkRecordSpecialMethodDeclaration(method, JavaErrorMessages.message("record.canonical.constructor")); + } + else { + // Non-canonical constructor + PsiMethodCallExpression call = JavaPsiConstructorUtil.findThisOrSuperCallInConstructor(method); + if (call == null || JavaPsiConstructorUtil.isSuperConstructorCall(call)) { + PsiIdentifier identifier = method.getNameIdentifier(); + if (identifier != null) { + String message = JavaErrorMessages.message("record.no.constructor.call.in.non.canonical"); + return HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(identifier) + .descriptionAndTooltip(message).create(); + } + } + return null; + } + } + + @Nullable + private static HighlightInfo checkRecordSpecialMethodDeclaration(PsiMethod method, String methodTitle) { + PsiIdentifier identifier = method.getNameIdentifier(); + if (identifier == null) return null; 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")) + .descriptionAndTooltip(JavaErrorMessages.message("record.special.method.type.parameters", methodTitle)) .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")) + .descriptionAndTooltip(JavaErrorMessages.message("record.special.method.non.public", methodTitle)) .create(); QuickFixAction.registerQuickFixAction(info, QUICK_FIX_FACTORY.createModifierListFix(method, PsiModifier.PUBLIC, true, false)); return info; @@ -1944,7 +1994,7 @@ public class HighlightMethodUtil { PsiReferenceList throwsList = method.getThrowsList(); if (throwsList.getReferenceElements().length > 0) { HighlightInfo info = HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(throwsList) - .descriptionAndTooltip(JavaErrorMessages.message("record.accessor.throws")) + .descriptionAndTooltip(JavaErrorMessages.message("record.special.method.throws", methodTitle)) .create(); QuickFixAction.registerQuickFixAction(info, QUICK_FIX_FACTORY.createDeleteFix(throwsList)); return info; 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 712d62cb4086..d4507f68c880 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 @@ -882,6 +882,7 @@ public class HighlightVisitorImpl extends JavaElementVisitor implements Highligh 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)); + if (!myHolder.hasErrorResults()) myHolder.add(HighlightMethodUtil.checkRecordConstructorDeclaration(method)); PsiClass aClass = method.getContainingClass(); if (!myHolder.hasErrorResults() && method.isConstructor()) { @@ -962,7 +963,7 @@ public class HighlightVisitorImpl extends JavaElementVisitor implements Highligh catch (IndexNotReadyException ignored) { } } - if (!myHolder.hasErrorResults()) myHolder.add(HighlightMethodUtil.checkConstructorCallMustBeFirstStatement(expression)); + if (!myHolder.hasErrorResults()) myHolder.add(HighlightMethodUtil.checkConstructorCallProblems(expression)); if (!myHolder.hasErrorResults()) myHolder.add(HighlightMethodUtil.checkSuperAbstractMethodDirectCall(expression)); if (!myHolder.hasErrorResults()) visitExpression(expression); 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 index 3a1375807435..f809ce0de003 100644 --- a/java/java-psi-api/src/com/intellij/psi/util/JavaPsiRecordUtil.java +++ b/java/java-psi-api/src/com/intellij/psi/util/JavaPsiRecordUtil.java @@ -1,9 +1,7 @@ // 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 com.intellij.psi.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -29,4 +27,23 @@ public class JavaPsiRecordUtil { } return null; } + + /** + * @param method to check + * @return true if given method is a canonical constructor for a record class + */ + public static boolean isCanonicalConstructor(@NotNull PsiMethod method) { + if (!method.isConstructor()) return false; + PsiClass aClass = method.getContainingClass(); + if (aClass == null || !aClass.isRecord()) return false; + PsiRecordComponent[] components = aClass.getRecordComponents(); + PsiParameter[] parameters = method.getParameterList().getParameters(); + if (components.length != parameters.length) return false; + for (int i = 0; i < parameters.length; i++) { + PsiType componentType = components[i].getType(); + PsiType parameterType = parameters[i].getType(); + if (!TypeConversionUtil.erasure(componentType).equals(TypeConversionUtil.erasure(parameterType))) return false; + } + return true; + } } 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 9261e002d691..32f3eafa67d9 100644 --- a/java/java-psi-impl/src/com/intellij/codeInsight/ExceptionUtil.java +++ b/java/java-psi-impl/src/com/intellij/codeInsight/ExceptionUtil.java @@ -927,6 +927,7 @@ public class ExceptionUtil { * @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; + return JavaPsiRecordUtil.getRecordComponentForAccessor(method) == null && + !JavaPsiRecordUtil.isCanonicalConstructor(method); } } diff --git a/java/java-psi-impl/src/messages/JavaErrorMessages.properties b/java/java-psi-impl/src/messages/JavaErrorMessages.properties index 625fcaf50db3..9e3e6fd2637e 100644 --- a/java/java-psi-impl/src/messages/JavaErrorMessages.properties +++ b/java/java-psi-impl/src/messages/JavaErrorMessages.properties @@ -475,9 +475,14 @@ 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 +record.canonical.constructor.wrong.parameter.type=Incorrect parameter type for record component ''{0}''. Expected: ''{1}'', found: ''{2}'' +record.constructor.call.in.canonical=Canonical constructor cannot delegate to another constructor +record.no.constructor.call.in.non.canonical=Non-canonical record constructor must delegate to another constructor +record.special.method.type.parameters={0} cannot have type parameters +record.special.method.non.public={0} must be ''public'' +record.special.method.throws={0} cannot declare thrown exceptions +record.canonical.constructor=Canonical constructor +record.accessor=Record component accessor feature.generics=Generics feature.annotations=Annotations diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advHighlightingRecords/RecordConstructors.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advHighlightingRecords/RecordConstructors.java new file mode 100644 index 000000000000..125c916b46fe --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advHighlightingRecords/RecordConstructors.java @@ -0,0 +1,29 @@ +record NotPublic(int x, int y) { + NotPublic(int x, int y) {this.x = x; this.y = y;} + NotPublic() {this(0,0);} +} +record Generic(String x) { + public Generic(String x) {this.x = x;} + public Generic() {this("");} +} +record Throws() { + public Throws() throws Throwable {} + public Throws(int x) throws Throwable { this(); } +} +record TypeMismatch(T t) { + public TypeMismatch(Object t) { + this.t = null; + } +} +record Delegate(int x) { + public Delegate(int x) { + this(); + } + + public Delegate() { + } + + public Delegate(int x, int y) { + super(); + } +} \ 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 de81374f2c64..43a91438a5b2 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 @@ -24,6 +24,9 @@ public class LightRecordsHighlightingTest extends LightJavaCodeInsightFixtureTes public void testRecordAccessors() { doTest(); } + public void testRecordConstructors() { + 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 52e17a044741..9bd17468d819 100644 --- a/plugins/IntentionPowerPak/src/com/siyeh/ipp/modifiers/ChangeModifierIntention.java +++ b/plugins/IntentionPowerPak/src/com/siyeh/ipp/modifiers/ChangeModifierIntention.java @@ -130,7 +130,8 @@ 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) { + if (JavaPsiRecordUtil.getRecordComponentForAccessor((PsiMethod)member) != null || + JavaPsiRecordUtil.isCanonicalConstructor((PsiMethod)member)) { return Collections.singletonList(AccessModifier.PUBLIC); } if (containingClass.isInterface()) {