diff --git a/.idea/modules.xml b/.idea/modules.xml index b261c380a21a..d608389ec304 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -426,6 +426,7 @@ + diff --git a/java/codeserver/highlighting/intellij.java.codeserver.highlighting.iml b/java/codeserver/highlighting/intellij.java.codeserver.highlighting.iml new file mode 100644 index 000000000000..b78c2920f47c --- /dev/null +++ b/java/codeserver/highlighting/intellij.java.codeserver.highlighting.iml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java/codeserver/highlighting/resources/messages/JavaCompilationErrorBundle.properties b/java/codeserver/highlighting/resources/messages/JavaCompilationErrorBundle.properties new file mode 100644 index 000000000000..91ee28142333 --- /dev/null +++ b/java/codeserver/highlighting/resources/messages/JavaCompilationErrorBundle.properties @@ -0,0 +1,11 @@ +insufficient.language.level={0} are not supported at language level ''{1}'' + +annotation.not.allowed.here=Annotations are not allowed here +annotation.not.applicable=''@{0}'' not applicable to {1} +annotation.not.allowed.void='void' type may not be annotated +annotation.not.allowed.var='var' type may not be annotated +annotation.not.allowed.class=Class literal type may not be annotated +annotation.not.allowed.ref=Annotation is not applicable to this kind of reference +annotation.not.allowed.static=Static member qualifying type may not be annotated +annotation.annotation.type.expected=Annotation type expected +annotation.missing.attribute={0} missing though required diff --git a/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/AnnotationChecker.java b/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/AnnotationChecker.java new file mode 100644 index 000000000000..0f44b185b801 --- /dev/null +++ b/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/AnnotationChecker.java @@ -0,0 +1,188 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.java.codeserver.highlighting; + +import com.intellij.codeInsight.AnnotationTargetUtil; +import com.intellij.java.codeserver.highlighting.errors.JavaErrorKinds; +import com.intellij.patterns.ElementPattern; +import com.intellij.pom.java.JavaFeature; +import com.intellij.psi.*; +import com.intellij.psi.impl.source.PsiClassReferenceType; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.psi.util.PsiUtil; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +import static com.intellij.patterns.PsiJavaPatterns.psiElement; + +final class AnnotationChecker { + private static final ElementPattern ANY_ANNOTATION_ALLOWED = psiElement().andOr( + psiElement().withParent(PsiNameValuePair.class), + psiElement().withParents(PsiArrayInitializerMemberValue.class, PsiNameValuePair.class), + psiElement().withParents(PsiArrayInitializerMemberValue.class, PsiAnnotationMethod.class), + psiElement().withParent(PsiAnnotationMethod.class).afterLeaf(PsiKeyword.DEFAULT), + // Unterminated parameter list like "void test(@NotNull String)": error on annotation looks annoying here + psiElement().withParents(PsiModifierList.class, PsiParameterList.class) + ); + + private final JavaErrorVisitor myVisitor; + + AnnotationChecker(JavaErrorVisitor visitor) { myVisitor = visitor; } + + private void checkReferenceTarget(@NotNull PsiAnnotation annotation, @Nullable PsiJavaCodeReferenceElement ref) { + if (ref == null) return; + PsiElement refTarget = ref.resolve(); + if (refTarget == null) return; + + if (!(refTarget instanceof PsiClass)) { + myVisitor.report(JavaErrorKinds.ANNOTATION_NOT_ALLOWED_REF.create(annotation)); + return; + } + + PsiElement parent = ref.getParent(); + while (parent instanceof PsiJavaCodeReferenceElement referenceElement) { + PsiElement qualified = referenceElement.resolve(); + if (qualified instanceof PsiMember member && member.hasModifierProperty(PsiModifier.STATIC)) { + myVisitor.report(JavaErrorKinds.ANNOTATION_NOT_ALLOWED_STATIC.create(annotation)); + return; + } + if (qualified instanceof PsiClass) { + parent = parent.getParent(); + } + else { + break; + } + } + } + + @Contract("null->null; !null->!null") + private static PsiJavaCodeReferenceElement getOutermostReferenceElement(@Nullable PsiJavaCodeReferenceElement ref) { + if (ref == null) return null; + + while (ref.getQualifier() instanceof PsiJavaCodeReferenceElement referenceElement) { + ref = referenceElement; + } + return ref; + } + + void checkAnnotation(@NotNull PsiAnnotation annotation) { + if (!myVisitor.hasErrorResults()) checkAnnotationApplicability(annotation); + if (!myVisitor.hasErrorResults()) checkAnnotationType(annotation); + if (!myVisitor.hasErrorResults()) checkMissingAttributes(annotation); + } + + private void checkMissingAttributes(@NotNull PsiAnnotation annotation) { + PsiJavaCodeReferenceElement nameRef = annotation.getNameReferenceElement(); + if (nameRef == null) return; + PsiElement aClass = nameRef.resolve(); + if (aClass instanceof PsiClass psiClass && psiClass.isAnnotationType()) { + Set names = new HashSet<>(); + PsiNameValuePair[] attributes = annotation.getParameterList().getAttributes(); + for (PsiNameValuePair attribute : attributes) { + String name = attribute.getName(); + names.add(Objects.requireNonNullElse(name, PsiAnnotation.DEFAULT_REFERENCED_METHOD_NAME)); + } + + PsiMethod[] annotationMethods = psiClass.getMethods(); + List missed = new ArrayList<>(); + for (PsiMethod method : annotationMethods) { + if (PsiUtil.isAnnotationMethod(method)) { + PsiAnnotationMethod annotationMethod = (PsiAnnotationMethod)method; + if (annotationMethod.getDefaultValue() == null) { + if (!names.contains(annotationMethod.getName())) { + missed.add(annotationMethod.getName()); + } + } + } + } + + if (!missed.isEmpty()) { + myVisitor.report(JavaErrorKinds.ANNOTATION_MISSING_ATTRIBUTE.create(annotation, missed)); + } + } + } + + private void checkAnnotationType(@NotNull PsiAnnotation annotation) { + PsiJavaCodeReferenceElement nameReferenceElement = annotation.getNameReferenceElement(); + if (nameReferenceElement != null) { + PsiElement resolved = nameReferenceElement.resolve(); + if (resolved != null && (!(resolved instanceof PsiClass psiClass) || !psiClass.isAnnotationType())) { + myVisitor.report(JavaErrorKinds.ANNOTATION_TYPE_EXPECTED.create(nameReferenceElement)); + } + } + } + + private void checkAnnotationApplicability(@NotNull PsiAnnotation annotation) { + if (ANY_ANNOTATION_ALLOWED.accepts(annotation)) return; + PsiJavaCodeReferenceElement nameRef = annotation.getNameReferenceElement(); + if (nameRef == null) return; + + PsiAnnotationOwner owner = annotation.getOwner(); + PsiAnnotation.TargetType[] targets = AnnotationTargetUtil.getTargetsForLocation(owner); + if (owner == null || targets.length == 0) { + myVisitor.report(JavaErrorKinds.ANNOTATION_NOT_ALLOWED_HERE.create(annotation)); + return; + } + if (!(owner instanceof PsiModifierList)) { + myVisitor.checkFeature(annotation, JavaFeature.TYPE_ANNOTATIONS); + if (myVisitor.hasErrorResults()) return; + } + + PsiAnnotation.TargetType applicable = AnnotationTargetUtil.findAnnotationTarget(annotation, targets); + if (applicable == PsiAnnotation.TargetType.UNKNOWN) return; + + if (applicable == null) { + if (targets.length == 1 && targets[0] == PsiAnnotation.TargetType.TYPE_USE) { + PsiElement parent = annotation.getParent(); + if (parent instanceof PsiTypeElement) { + PsiElement modifierList = + PsiTreeUtil.skipSiblingsBackward(parent, PsiWhiteSpace.class, PsiComment.class, PsiTypeParameterList.class); + if (modifierList instanceof PsiModifierList psiModifierList) { + targets = AnnotationTargetUtil.getTargetsForLocation(psiModifierList); + if (AnnotationTargetUtil.findAnnotationTarget(annotation, targets) == null) { + myVisitor.report(JavaErrorKinds.ANNOTATION_NOT_APPLICABLE.create(annotation, Arrays.asList(targets))); + } + return; + } + } + } + myVisitor.report(JavaErrorKinds.ANNOTATION_NOT_APPLICABLE.create(annotation, Arrays.asList(targets))); + return; + } + + if (applicable == PsiAnnotation.TargetType.TYPE_USE) { + if (PsiTreeUtil.skipParentsOfType(annotation, PsiTypeElement.class) instanceof PsiClassObjectAccessExpression) { + myVisitor.report(JavaErrorKinds.ANNOTATION_NOT_ALLOWED_CLASS.create(annotation)); + return; + } + if (owner instanceof PsiClassReferenceType referenceType) { + PsiJavaCodeReferenceElement ref = referenceType.getReference(); + checkReferenceTarget(annotation, ref); + } + else if (owner instanceof PsiModifierList || owner instanceof PsiTypeElement) { + PsiElement nextElement = owner instanceof PsiTypeElement typeElementOwner + ? typeElementOwner + : PsiTreeUtil.skipSiblingsForward((PsiModifierList)owner, PsiComment.class, PsiWhiteSpace.class, + PsiTypeParameterList.class); + if (nextElement instanceof PsiTypeElement typeElement) { + PsiType type = typeElement.getType(); + //see JLS 9.7.4 Where Annotations May Appear + if (PsiTypes.voidType().equals(type)) { + myVisitor.report(JavaErrorKinds.ANNOTATION_NOT_ALLOWED_VOID.create(annotation)); + return; + } + if (typeElement.isInferredType()) { + myVisitor.report(JavaErrorKinds.ANNOTATION_NOT_ALLOWED_VAR.create(annotation)); + return; + } + if (!(type instanceof PsiPrimitiveType || type instanceof PsiArrayType)) { + PsiJavaCodeReferenceElement ref = getOutermostReferenceElement(typeElement.getInnermostComponentReferenceElement()); + checkReferenceTarget(annotation, ref); + } + } + } + } + } +} diff --git a/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/JavaCompilationErrorBundle.java b/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/JavaCompilationErrorBundle.java new file mode 100644 index 000000000000..a9a03f84ea0f --- /dev/null +++ b/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/JavaCompilationErrorBundle.java @@ -0,0 +1,26 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.java.codeserver.highlighting; + +import com.intellij.DynamicBundle; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.PropertyKey; + +import java.util.function.Supplier; + +public final class JavaCompilationErrorBundle { + public static final @NonNls String BUNDLE = "messages.JavaCompilationErrorBundle"; + private static final DynamicBundle INSTANCE = new DynamicBundle(JavaCompilationErrorBundle.class, BUNDLE); + + private JavaCompilationErrorBundle() { + } + + public static @NotNull @Nls String message(@NotNull @PropertyKey(resourceBundle = BUNDLE) String key, Object @NotNull ... params) { + return INSTANCE.getMessage(key, params); + } + + public static @NotNull Supplier<@Nls String> messagePointer(@NotNull @PropertyKey(resourceBundle = BUNDLE) String key, Object @NotNull ... params) { + return INSTANCE.getLazyMessage(key, params); + } +} diff --git a/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/JavaErrorCollector.java b/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/JavaErrorCollector.java new file mode 100644 index 000000000000..6e1017f27d0f --- /dev/null +++ b/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/JavaErrorCollector.java @@ -0,0 +1,37 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.java.codeserver.highlighting; + +import com.intellij.java.codeserver.highlighting.errors.JavaCompilationError; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Consumer; + +/** + * A starting point to highlight errors in Java code + */ +public final class JavaErrorCollector { + private final JavaErrorVisitor myVisitor; + + /** + * Create a collector to collect Java errors. After creation every single PSI element in the desired range should be passed into + * {@link #processElement(PsiElement)} to actually gather errors + * + * @param file Java file to process + * @param consumer a consumer to get errors + */ + public JavaErrorCollector(@NotNull PsiFile file, @NotNull Consumer<@NotNull JavaCompilationError> consumer) { + myVisitor = new JavaErrorVisitor(file, consumer); + } + + /** + * Finds the errors related to a given element. One must call this method for every element recursively in the desired range + * to get all the errors. + * + * @param element element to find the errors at. + */ + public void processElement(@NotNull PsiElement element) { + element.accept(myVisitor); + } +} diff --git a/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/JavaErrorVisitor.java b/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/JavaErrorVisitor.java new file mode 100644 index 000000000000..b4f031fcc4ea --- /dev/null +++ b/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/JavaErrorVisitor.java @@ -0,0 +1,62 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.java.codeserver.highlighting; + +import com.intellij.java.codeserver.highlighting.errors.JavaCompilationError; +import com.intellij.java.codeserver.highlighting.errors.JavaErrorKinds; +import com.intellij.openapi.project.Project; +import com.intellij.pom.java.JavaFeature; +import com.intellij.pom.java.LanguageLevel; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiAnnotation; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.util.PsiUtil; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Consumer; + +final class JavaErrorVisitor extends JavaElementVisitor { + private final @NotNull Consumer> myErrorConsumer; + private final @NotNull Project myProject; + private final @NotNull PsiFile myFile; + private final @NotNull LanguageLevel myLanguageLevel; + private final @NotNull AnnotationChecker myAnnotationChecker = new AnnotationChecker(this); + private boolean myHasError; // true if myHolder.add() was called with HighlightInfo of >=ERROR severity. On each .visit(PsiElement) call this flag is reset. Useful to determine whether the error was already reported while visiting this PsiElement. + + JavaErrorVisitor(@NotNull PsiFile file, @NotNull Consumer> consumer) { + myFile = file; + myProject = file.getProject(); + myLanguageLevel = PsiUtil.getLanguageLevel(file); + myErrorConsumer = consumer; + } + + void report(@NotNull JavaCompilationError error) { + myErrorConsumer.accept(error); + myHasError = true; + } + + @Contract(pure = true) + boolean hasErrorResults() { + return myHasError; + } + + @Override + public void visitElement(@NotNull PsiElement element) { + super.visitElement(element); + myHasError = false; + } + + @Override + public void visitAnnotation(@NotNull PsiAnnotation annotation) { + super.visitAnnotation(annotation); + if (!hasErrorResults()) checkFeature(annotation, JavaFeature.ANNOTATIONS); + myAnnotationChecker.checkAnnotation(annotation); + } + + void checkFeature(@NotNull PsiElement element, @NotNull JavaFeature feature) { + if (!feature.isSufficient(myLanguageLevel)) { + report(JavaErrorKinds.UNSUPPORTED_FEATURE.create(element, feature)); + } + } +} diff --git a/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/errors/JavaCompilationError.java b/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/errors/JavaCompilationError.java new file mode 100644 index 000000000000..7dc446932384 --- /dev/null +++ b/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/errors/JavaCompilationError.java @@ -0,0 +1,36 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.java.codeserver.highlighting.errors; + +import com.intellij.openapi.util.text.HtmlChunk; +import com.intellij.psi.PsiElement; +import org.jetbrains.annotations.NotNull; + +/** + * A concrete instance of a Java compilation error + * + * @param kind error kind + * @param psi PSI element where the error occurred + * @param context additional context necessary to properly render the error and the corresponding quick-fixes + * @param type of the context + */ +public record JavaCompilationError(@NotNull JavaErrorKind kind, + @NotNull Psi psi, + Context context) { + public JavaCompilationError { + kind.validate(psi, context); + } + + /** + * @return a desired anchor to put the error message at + */ + public @NotNull PsiElement anchor() { + return kind.anchor(psi, context); + } + + /** + * @return a user-readable localized error description + */ + public @NotNull HtmlChunk description() { + return kind.description(psi, context); + } +} diff --git a/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/errors/JavaErrorKind.java b/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/errors/JavaErrorKind.java new file mode 100644 index 000000000000..b069d8c0a03a --- /dev/null +++ b/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/errors/JavaErrorKind.java @@ -0,0 +1,53 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.java.codeserver.highlighting.errors; + +import com.intellij.java.codeserver.highlighting.JavaCompilationErrorBundle; +import com.intellij.openapi.util.text.HtmlChunk; +import com.intellij.psi.PsiElement; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.PropertyKey; + +/** + * A kind of Java compilation error + * + * @param type of context PSI element required for this error + * @param additional context required for a particular kind, if any + */ +public sealed interface JavaErrorKind permits Parameterized, JavaSimpleErrorKind { + /** + * @return a key, which uniquely identifies the error kind + */ + @NotNull @PropertyKey(resourceBundle = JavaCompilationErrorBundle.BUNDLE) String key(); + + /** + * @param psi PSI element associated with an error + * @param context a context in which the error should be rendered + * @return rendered localized error description + */ + @NotNull HtmlChunk description(@NotNull Psi psi, Context context); + + /** + * @param psi PSI element associated with an error + * @param context a context in which the error should be rendered + * @return error message anchor (must be within the psi) + */ + default @NotNull PsiElement anchor(@NotNull Psi psi, Context context) { + return psi; + } + + /** + * @param psi PSI element associated with an error + * @param context a context in which the error should be rendered + * @throws IllegalArgumentException if the context or PSI element are not applicable to this error kind + */ + default void validate(@NotNull Psi psi, Context context) throws IllegalArgumentException { + } + + /** + * @param context context to bind an instance to + * @return create an instance of this error + */ + default @NotNull JavaCompilationError create(@NotNull Psi psi, Context context) { + return new JavaCompilationError<>(this, psi, context); + } +} diff --git a/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/errors/JavaErrorKinds.java b/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/errors/JavaErrorKinds.java new file mode 100644 index 000000000000..51d18cbfcdea --- /dev/null +++ b/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/errors/JavaErrorKinds.java @@ -0,0 +1,80 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.java.codeserver.highlighting.errors; + +import com.intellij.core.JavaPsiBundle; +import com.intellij.java.codeserver.highlighting.JavaCompilationErrorBundle; +import com.intellij.openapi.projectRoots.JavaSdkVersion; +import com.intellij.openapi.util.text.HtmlChunk; +import com.intellij.pom.java.JavaFeature; +import com.intellij.psi.PsiAnnotation; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiJavaCodeReferenceElement; +import com.intellij.psi.util.PsiUtil; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * All possible Java error kinds + */ +public final class JavaErrorKinds { + private JavaErrorKinds() {} + + public static final JavaErrorKind UNSUPPORTED_FEATURE = + new Parameterized<>("insufficient.language.level") { + @Override + public @NotNull HtmlChunk description(@NotNull PsiElement element, @NotNull JavaFeature feature) { + String name = feature.getFeatureName(); + String version = JavaSdkVersion.fromLanguageLevel(PsiUtil.getLanguageLevel(element)).getDescription(); + return HtmlChunk.raw(JavaCompilationErrorBundle.message("insufficient.language.level", name, version)); + } + }; + + public static final JavaSimpleErrorKind ANNOTATION_NOT_ALLOWED_HERE = + new JavaSimpleErrorKind<>("annotation.not.allowed.here"); + public static final JavaSimpleErrorKind ANNOTATION_NOT_ALLOWED_VAR = + new JavaSimpleErrorKind<>("annotation.not.allowed.var"); + public static final JavaSimpleErrorKind ANNOTATION_NOT_ALLOWED_VOID = + new JavaSimpleErrorKind<>("annotation.not.allowed.void"); + public static final JavaSimpleErrorKind ANNOTATION_NOT_ALLOWED_CLASS = + new JavaSimpleErrorKind<>("annotation.not.allowed.class"); + public static final JavaSimpleErrorKind ANNOTATION_NOT_ALLOWED_REF = + new JavaSimpleErrorKind<>("annotation.not.allowed.ref"); + public static final JavaSimpleErrorKind ANNOTATION_NOT_ALLOWED_STATIC = + new JavaSimpleErrorKind<>("annotation.not.allowed.static"); + public static final JavaSimpleErrorKind ANNOTATION_TYPE_EXPECTED = + new JavaSimpleErrorKind<>("annotation.annotation.type.expected"); + public static final JavaErrorKind> ANNOTATION_NOT_APPLICABLE = + new Parameterized<>("annotation.not.applicable") { + @Override + public void validate(@NotNull PsiAnnotation annotation, @NotNull List types) + throws IllegalArgumentException { + if (types.isEmpty()) { + throw new IllegalArgumentException("types must not be empty"); + } + } + + @Override + public @NotNull HtmlChunk description(@NotNull PsiAnnotation annotation, @NotNull List types) { + String target = JavaPsiBundle.message("annotation.target." + types.get(0)); + PsiJavaCodeReferenceElement nameRef = annotation.getNameReferenceElement(); + return HtmlChunk.raw(JavaCompilationErrorBundle.message( + "annotation.not.applicable", nameRef != null ? nameRef.getText() : annotation.getText(), target)); + } + }; + public static final JavaErrorKind> ANNOTATION_MISSING_ATTRIBUTE = + new Parameterized<>("annotation.missing.attribute") { + @Override + public @NotNull PsiElement anchor(@NotNull PsiAnnotation annotation, @NotNull List strings) { + return Objects.requireNonNull(annotation.getNameReferenceElement()); + } + + @Override + public @NotNull HtmlChunk description(@NotNull PsiAnnotation annotation, @NotNull List attributeNames) { + return HtmlChunk.raw(JavaCompilationErrorBundle.message( + "annotation.missing.attribute", attributeNames.stream().map(attr -> "'" + attr + "'").collect(Collectors.joining(", ")))); + } + }; +} diff --git a/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/errors/JavaSimpleErrorKind.java b/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/errors/JavaSimpleErrorKind.java new file mode 100644 index 000000000000..2fb889dea7b9 --- /dev/null +++ b/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/errors/JavaSimpleErrorKind.java @@ -0,0 +1,38 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.java.codeserver.highlighting.errors; + +import com.intellij.java.codeserver.highlighting.JavaCompilationErrorBundle; +import com.intellij.openapi.util.text.HtmlChunk; +import com.intellij.psi.PsiElement; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.PropertyKey; + +/** + * Java compilation error without additional context + */ +public final class JavaSimpleErrorKind implements JavaErrorKind { + private final @NotNull @PropertyKey(resourceBundle = JavaCompilationErrorBundle.BUNDLE) String myKey; + + JavaSimpleErrorKind(@NotNull @PropertyKey(resourceBundle = JavaCompilationErrorBundle.BUNDLE) String key) { + myKey = key; + } + + @Override + public @PropertyKey(resourceBundle = JavaCompilationErrorBundle.BUNDLE) @NotNull String key() { + return myKey; + } + + @Override + public @NotNull HtmlChunk description(@NotNull Psi psi, Void unused) { + return HtmlChunk.raw(JavaCompilationErrorBundle.message(myKey)); + } + + public @NotNull JavaCompilationError create(@NotNull Psi psi) { + return JavaErrorKind.super.create(psi, null); + } + + @Override + public String toString() { + return "JavaErrorKind[" + myKey + "]"; + } +} diff --git a/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/errors/Parameterized.java b/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/errors/Parameterized.java new file mode 100644 index 000000000000..861467a06359 --- /dev/null +++ b/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/errors/Parameterized.java @@ -0,0 +1,25 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.java.codeserver.highlighting.errors; + +import com.intellij.java.codeserver.highlighting.JavaCompilationErrorBundle; +import com.intellij.psi.PsiElement; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.PropertyKey; + +non-sealed abstract class Parameterized implements JavaErrorKind { + private final @NotNull @PropertyKey(resourceBundle = JavaCompilationErrorBundle.BUNDLE) String myKey; + + Parameterized(@NotNull @PropertyKey(resourceBundle = JavaCompilationErrorBundle.BUNDLE) String key) { + myKey = key; + } + + @Override + public @PropertyKey(resourceBundle = JavaCompilationErrorBundle.BUNDLE) @NotNull String key() { + return myKey; + } + + @Override + public String toString() { + return "JavaErrorKind[" + myKey + "]"; + } +} diff --git a/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/errors/package-info.java b/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/errors/package-info.java new file mode 100644 index 000000000000..10947215b9af --- /dev/null +++ b/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/errors/package-info.java @@ -0,0 +1,5 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +@Experimental +package com.intellij.java.codeserver.highlighting.errors; + +import org.jetbrains.annotations.ApiStatus.Experimental; diff --git a/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/package-info.java b/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/package-info.java new file mode 100644 index 000000000000..25c8e4f5d1c1 --- /dev/null +++ b/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/package-info.java @@ -0,0 +1,8 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +/** + * Error highlighting of Java code + */ +@Experimental +package com.intellij.java.codeserver.highlighting; + +import static org.jetbrains.annotations.ApiStatus.Experimental; diff --git a/java/java-analysis-api/src/com/intellij/codeInsight/intention/QuickFixFactory.java b/java/java-analysis-api/src/com/intellij/codeInsight/intention/QuickFixFactory.java index 7ff3b39fac28..b971bcca5284 100644 --- a/java/java-analysis-api/src/com/intellij/codeInsight/intention/QuickFixFactory.java +++ b/java/java-analysis-api/src/com/intellij/codeInsight/intention/QuickFixFactory.java @@ -312,6 +312,9 @@ public abstract class QuickFixFactory { public abstract @NotNull List<@NotNull LocalQuickFix> registerOrderEntryFixes(@NotNull PsiReference reference, @NotNull List registrar); + /** + * @param annotationMethods unused, could be empty array + */ public abstract @NotNull IntentionAction createAddMissingRequiredAnnotationParametersFix(@NotNull PsiAnnotation annotation, PsiMethod @NotNull [] annotationMethods, @NotNull Collection missedElements); diff --git a/java/java-analysis-impl/intellij.java.analysis.impl.iml b/java/java-analysis-impl/intellij.java.analysis.impl.iml index 44c41eae087c..7979e49e72d8 100644 --- a/java/java-analysis-impl/intellij.java.analysis.impl.iml +++ b/java/java-analysis-impl/intellij.java.analysis.impl.iml @@ -29,5 +29,6 @@ + \ No newline at end of file diff --git a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/AnnotationsHighlightUtil.java b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/AnnotationsHighlightUtil.java index 2246acc93932..fafbc22d566d 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/AnnotationsHighlightUtil.java +++ b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/AnnotationsHighlightUtil.java @@ -6,33 +6,27 @@ import com.intellij.codeInsight.AnnotationUtil; import com.intellij.codeInsight.daemon.JavaErrorBundle; import com.intellij.codeInsight.daemon.impl.HighlightInfo; import com.intellij.codeInsight.daemon.impl.HighlightInfoType; -import com.intellij.codeInsight.daemon.impl.quickfix.MoveAnnotationOnStaticMemberQualifyingTypeFix; import com.intellij.codeInsight.daemon.impl.quickfix.MoveAnnotationToPackageInfoFileFix; -import com.intellij.codeInsight.daemon.impl.quickfix.ReplaceVarWithExplicitTypeFix; import com.intellij.codeInsight.intention.IntentionAction; import com.intellij.codeInsight.intention.QuickFixFactory; import com.intellij.codeInsight.intention.impl.BaseIntentionAction; import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement; import com.intellij.core.JavaPsiBundle; import com.intellij.java.analysis.JavaAnalysisBundle; -import com.intellij.modcommand.ModCommandAction; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.NlsContexts; import com.intellij.openapi.util.text.StringUtil; -import com.intellij.patterns.ElementPattern; import com.intellij.pom.java.JavaFeature; import com.intellij.pom.java.LanguageLevel; import com.intellij.psi.*; import com.intellij.psi.impl.IncompleteModelUtil; import com.intellij.psi.impl.PsiImplUtil; -import com.intellij.psi.impl.source.PsiClassReferenceType; import com.intellij.psi.impl.source.PsiImmediateClassType; import com.intellij.psi.util.*; import com.intellij.util.ArrayUtil; import com.intellij.util.ObjectUtils; import com.siyeh.ig.psiutils.TypeUtils; -import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -40,9 +34,9 @@ import org.jetbrains.annotations.Nullable; import java.lang.annotation.Documented; import java.lang.annotation.Inherited; import java.lang.annotation.RetentionPolicy; -import java.util.*; - -import static com.intellij.patterns.PsiJavaPatterns.psiElement; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; public final class AnnotationsHighlightUtil { private static final Logger LOG = Logger.getInstance(AnnotationsHighlightUtil.class); @@ -313,51 +307,6 @@ public final class AnnotationsHighlightUtil { return false; } - static HighlightInfo.Builder checkMissingAttributes(@NotNull PsiAnnotation annotation) { - PsiJavaCodeReferenceElement nameRef = annotation.getNameReferenceElement(); - if (nameRef == null) return null; - PsiElement aClass = nameRef.resolve(); - if (aClass instanceof PsiClass psiClass && psiClass.isAnnotationType()) { - Set names = new HashSet<>(); - PsiNameValuePair[] attributes = annotation.getParameterList().getAttributes(); - for (PsiNameValuePair attribute : attributes) { - String name = attribute.getName(); - names.add(Objects.requireNonNullElse(name, PsiAnnotation.DEFAULT_REFERENCED_METHOD_NAME)); - } - - PsiMethod[] annotationMethods = psiClass.getMethods(); - List missed = new ArrayList<>(); - for (PsiMethod method : annotationMethods) { - if (PsiUtil.isAnnotationMethod(method)) { - PsiAnnotationMethod annotationMethod = (PsiAnnotationMethod)method; - if (annotationMethod.getDefaultValue() == null) { - if (!names.contains(annotationMethod.getName())) { - missed.add(annotationMethod.getName()); - } - } - } - } - - if (!missed.isEmpty()) { - StringBuffer buff = new StringBuffer("'" + missed.get(0) + "'"); - for (int i = 1; i < missed.size(); i++) { - buff.append(", "); - buff.append("'").append(missed.get(i)).append("'"); - } - - String description = JavaErrorBundle.message("annotation.missing.attribute", buff); - HighlightInfo.Builder info = - HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(nameRef).descriptionAndTooltip(description); - IntentionAction fix = QuickFixFactory.getInstance().createAddMissingRequiredAnnotationParametersFix( - annotation, annotationMethods, missed); - info.registerFix(fix, null, null, null, null); - return info; - } - } - - return null; - } - static HighlightInfo.Builder checkConstantExpression(@NotNull PsiExpression expression) { PsiElement parent = expression.getParent(); if (PsiUtil.isAnnotationMethod(parent) || parent instanceof PsiNameValuePair || parent instanceof PsiArrayInitializerMemberValue) { @@ -383,115 +332,6 @@ public final class AnnotationsHighlightUtil { return HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(typeElement).descriptionAndTooltip(description); } - private static final ElementPattern ANY_ANNOTATION_ALLOWED = psiElement().andOr( - psiElement().withParent(PsiNameValuePair.class), - psiElement().withParents(PsiArrayInitializerMemberValue.class, PsiNameValuePair.class), - psiElement().withParents(PsiArrayInitializerMemberValue.class, PsiAnnotationMethod.class), - psiElement().withParent(PsiAnnotationMethod.class).afterLeaf(PsiKeyword.DEFAULT), - // Unterminated parameter list like "void test(@NotNull String)": error on annotation looks annoying here - psiElement().withParents(PsiModifierList.class, PsiParameterList.class) - ); - - public static HighlightInfo.Builder checkApplicability(@NotNull PsiAnnotation annotation, @NotNull LanguageLevel level, @NotNull PsiFile file) { - if (ANY_ANNOTATION_ALLOWED.accepts(annotation)) { - return null; - } - - PsiJavaCodeReferenceElement nameRef = annotation.getNameReferenceElement(); - if (nameRef == null) return null; - - PsiAnnotationOwner owner = annotation.getOwner(); - PsiAnnotation.TargetType[] targets = AnnotationTargetUtil.getTargetsForLocation(owner); - if (owner == null || targets.length == 0) { - String message = JavaErrorBundle.message("annotation.not.allowed.here"); - return createAnnotationError(annotation, message); - } - - if (!(owner instanceof PsiModifierList)) { - HighlightInfo.Builder info = HighlightUtil.checkFeature(annotation, JavaFeature.TYPE_ANNOTATIONS, level, file); - if (info != null) return info; - } - - PsiAnnotation.TargetType applicable = AnnotationTargetUtil.findAnnotationTarget(annotation, targets); - if (applicable == PsiAnnotation.TargetType.UNKNOWN) return null; - - if (applicable == null) { - if (targets.length == 1 && targets[0] == PsiAnnotation.TargetType.TYPE_USE) { - PsiElement parent = annotation.getParent(); - if (parent instanceof PsiTypeElement) { - PsiElement modifierList = PsiTreeUtil.skipSiblingsBackward(parent, PsiWhiteSpace.class, PsiComment.class, PsiTypeParameterList.class); - if (modifierList instanceof PsiModifierList psiModifierList) { - targets = AnnotationTargetUtil.getTargetsForLocation(psiModifierList); - if (AnnotationTargetUtil.findAnnotationTarget(annotation, targets) == null) { - return createNotApplicableTargetInfo(annotation, nameRef, targets); - } - return null; - } - } - } - return createNotApplicableTargetInfo(annotation, nameRef, targets); - } - - - if (applicable == PsiAnnotation.TargetType.TYPE_USE) { - if (owner instanceof PsiClassReferenceType referenceType) { - PsiJavaCodeReferenceElement ref = referenceType.getReference(); - HighlightInfo.Builder info = checkReferenceTarget(annotation, ref); - if (info != null) return info; - } - else if (owner instanceof PsiModifierList || owner instanceof PsiTypeElement) { - PsiElement nextElement = owner instanceof PsiTypeElement typeElementOwner - ? typeElementOwner - : PsiTreeUtil.skipSiblingsForward((PsiModifierList)owner, PsiComment.class, PsiWhiteSpace.class, PsiTypeParameterList.class); - if (nextElement instanceof PsiTypeElement typeElement) { - PsiType type = typeElement.getType(); - //see JLS 9.7.4 Where Annotations May Appear - if (PsiTypes.voidType().equals(type)) { - String message = JavaErrorBundle.message("annotation.not.allowed.void"); - return createAnnotationError(annotation, message); - } - if (typeElement.isInferredType()) { - HighlightInfo.Builder info = HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR) - .range(annotation) - .descriptionAndTooltip(JavaErrorBundle.message("annotation.not.allowed.var")); - IntentionAction action1 = - QuickFixFactory.getInstance().createDeleteFix(annotation, JavaAnalysisBundle.message("intention.text.remove.annotation")); - info.registerFix(action1, null, null, null, null); - ModCommandAction action = new ReplaceVarWithExplicitTypeFix(typeElement); - info.registerFix(action, null, null, null, null); - return info; - } - if (!(type instanceof PsiPrimitiveType || type instanceof PsiArrayType)) { - PsiJavaCodeReferenceElement ref = getOutermostReferenceElement(typeElement.getInnermostComponentReferenceElement()); - HighlightInfo.Builder info = checkReferenceTarget(annotation, ref); - if (info != null) return info; - } - } - } - if (PsiTreeUtil.skipParentsOfType(annotation, PsiTypeElement.class) instanceof PsiClassObjectAccessExpression) { - String message = JavaErrorBundle.message("annotation.not.allowed.class"); - return createAnnotationError(annotation, message); - } - } - - return null; - } - - private static @NotNull HighlightInfo.Builder createNotApplicableTargetInfo(@NotNull PsiAnnotation annotation, - PsiJavaCodeReferenceElement nameRef, - PsiAnnotation.TargetType[] targets) { - String target = JavaPsiBundle.message("annotation.target." + targets[0]); - String message = JavaErrorBundle.message("annotation.not.applicable", nameRef.getText(), target); - HighlightInfo.Builder info = createAnnotationError(annotation, message); - if (BaseIntentionAction.canModify(Objects.requireNonNull(annotation.resolveAnnotationType()))) { - for (PsiAnnotation.TargetType targetType : targets) { - IntentionAction action = QuickFixFactory.getInstance().createAddAnnotationTargetFix(annotation, targetType); - info.registerFix(action, null, null, null, null); - } - } - return info; - } - private static @NotNull HighlightInfo.Builder createAnnotationError(@NotNull PsiAnnotation annotation, @NotNull @NlsContexts.DetailedDescription String message) { LocalQuickFixAndIntentionActionOnPsiElement fix = QuickFixFactory.getInstance() .createDeleteFix(annotation, JavaAnalysisBundle.message("intention.text.remove.annotation")); @@ -508,55 +348,6 @@ public final class AnnotationsHighlightUtil { return info; } - private static @Nullable HighlightInfo.Builder checkReferenceTarget(@NotNull PsiAnnotation annotation, @Nullable PsiJavaCodeReferenceElement ref) { - if (ref == null) return null; - PsiElement refTarget = ref.resolve(); - if (refTarget == null) return null; - - if (!(refTarget instanceof PsiClass)) { - return createAnnotationError(annotation, JavaErrorBundle.message("annotation.not.allowed.ref")); - } - - PsiElement parent = ref.getParent(); - while (parent instanceof PsiJavaCodeReferenceElement referenceElement) { - PsiElement qualified = referenceElement.resolve(); - if (qualified instanceof PsiMember member && member.hasModifierProperty(PsiModifier.STATIC)) { - return createAnnotationError(annotation, - JavaErrorBundle.message("annotation.not.allowed.static"), - new MoveAnnotationOnStaticMemberQualifyingTypeFix(annotation).asIntention()); - } - if (qualified instanceof PsiClass) { - parent = parent.getParent(); - } - else { - break; - } - } - return null; - } - - @Contract("null->null; !null->!null") - private static PsiJavaCodeReferenceElement getOutermostReferenceElement(@Nullable PsiJavaCodeReferenceElement ref) { - if (ref == null) return null; - - while (ref.getQualifier() instanceof PsiJavaCodeReferenceElement referenceElement) { - ref = referenceElement; - } - return ref; - } - - static HighlightInfo.Builder checkAnnotationType(@NotNull PsiAnnotation annotation) { - PsiJavaCodeReferenceElement nameReferenceElement = annotation.getNameReferenceElement(); - if (nameReferenceElement != null) { - PsiElement resolved = nameReferenceElement.resolve(); - if (resolved != null && (!(resolved instanceof PsiClass psiClass) || !psiClass.isAnnotationType())) { - String description = JavaErrorBundle.message("annotation.annotation.type.expected"); - return HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(nameReferenceElement).descriptionAndTooltip(description); - } - } - return null; - } - static HighlightInfo.Builder checkCyclicMemberType(@NotNull PsiTypeElement typeElement, @NotNull PsiClass aClass) { PsiType type = typeElement.getType(); Set checked = new HashSet<>(); 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 669fd9c45f82..ea389a2ad01f 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 @@ -9,9 +9,11 @@ import com.intellij.codeInsight.daemon.impl.HighlightInfoType; import com.intellij.codeInsight.daemon.impl.HighlightVisitor; import com.intellij.codeInsight.daemon.impl.quickfix.AdjustFunctionContextFix; import com.intellij.codeInsight.daemon.impl.quickfix.QuickFixAction; +import com.intellij.codeInsight.intention.CommonIntentionAction; import com.intellij.codeInsight.intention.IntentionAction; import com.intellij.codeInsight.intention.QuickFixFactory; import com.intellij.codeInspection.ex.GlobalInspectionContextBase; +import com.intellij.java.codeserver.highlighting.JavaErrorCollector; import com.intellij.lang.annotation.HighlightSeverity; import com.intellij.lang.injection.InjectedLanguageManager; import com.intellij.lang.jvm.JvmModifier; @@ -64,6 +66,7 @@ public class HighlightVisitorImpl extends JavaElementVisitor implements Highligh private @NotNull PsiFile myFile; private PsiJavaModule myJavaModule; + private JavaErrorCollector myCollector; private PreviewFeatureUtil.PreviewFeatureVisitor myPreviewFeatureVisitor; @@ -164,6 +167,7 @@ public class HighlightVisitorImpl extends JavaElementVisitor implements Highligh public void visit(@NotNull PsiElement element) { myHasError = false; element.accept(this); + myCollector.processElement(element); } @Override @@ -215,6 +219,15 @@ public class HighlightVisitorImpl extends JavaElementVisitor implements Highligh .notNull(JavaVersionService.getInstance().getJavaSdkVersion(file), JavaSdkVersion.fromLanguageLevel(myLanguageLevel)); myJavaModule = JavaFeature.MODULES.isSufficient(myLanguageLevel) ? JavaModuleGraphUtil.findDescriptorByElement(file) : null; myPreviewFeatureVisitor = myLanguageLevel.isPreview() ? null : new PreviewFeatureUtil.PreviewFeatureVisitor(myLanguageLevel, myErrorSink); + myCollector = new JavaErrorCollector(myFile, error -> { + HighlightInfo.Builder info = HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR) + .range(error.anchor()) + .descriptionAndTooltip(error.description().toString()); + for (CommonIntentionAction fix : JavaErrorFixProvider.getFixes(error)) { + info.registerFix(fix.asIntention(), null, null, null, null); + } + add(info); + }); } @Override @@ -231,10 +244,6 @@ public class HighlightVisitorImpl extends JavaElementVisitor implements Highligh @Override public void visitAnnotation(@NotNull PsiAnnotation annotation) { super.visitAnnotation(annotation); - if (!hasErrorResults()) add(checkFeature(annotation, JavaFeature.ANNOTATIONS)); - if (!hasErrorResults()) add(AnnotationsHighlightUtil.checkApplicability(annotation, myLanguageLevel, myFile)); - if (!hasErrorResults()) add(AnnotationsHighlightUtil.checkAnnotationType(annotation)); - if (!hasErrorResults()) add(AnnotationsHighlightUtil.checkMissingAttributes(annotation)); if (!hasErrorResults()) add(AnnotationsHighlightUtil.checkTargetAnnotationDuplicates(annotation)); if (!hasErrorResults()) add(AnnotationsHighlightUtil.checkDuplicateAnnotations(annotation, myLanguageLevel)); if (!hasErrorResults()) add(AnnotationsHighlightUtil.checkFunctionalInterface(annotation, myLanguageLevel)); diff --git a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/JavaErrorFixProvider.java b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/JavaErrorFixProvider.java new file mode 100644 index 000000000000..10a6e07c76b2 --- /dev/null +++ b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/JavaErrorFixProvider.java @@ -0,0 +1,90 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.codeInsight.daemon.impl.analysis; + +import com.intellij.codeInsight.daemon.impl.quickfix.MoveAnnotationOnStaticMemberQualifyingTypeFix; +import com.intellij.codeInsight.daemon.impl.quickfix.ReplaceVarWithExplicitTypeFix; +import com.intellij.codeInsight.intention.CommonIntentionAction; +import com.intellij.codeInsight.intention.QuickFixFactory; +import com.intellij.codeInsight.intention.impl.BaseIntentionAction; +import com.intellij.java.analysis.JavaAnalysisBundle; +import com.intellij.java.codeserver.highlighting.errors.JavaCompilationError; +import com.intellij.java.codeserver.highlighting.errors.JavaErrorKind; +import com.intellij.psi.*; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.util.containers.ContainerUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +import static com.intellij.java.codeserver.highlighting.errors.JavaErrorKinds.*; + +/** + * Fixes attached to error messages provided by {@link com.intellij.java.codeserver.highlighting.JavaErrorCollector} + */ +final class JavaErrorFixProvider { + @FunctionalInterface + private interface JavaFixProvider { + @Nullable CommonIntentionAction provide(@NotNull JavaCompilationError error); + + default JavaFixesProvider asMulti() { + return error -> ContainerUtil.createMaybeSingletonList(provide(error)); + } + } + + @FunctionalInterface + private interface JavaFixesProvider { + @NotNull List<@NotNull CommonIntentionAction> provide(@NotNull JavaCompilationError error); + } + + private static final Map, List>> FIXES = new HashMap<>(); + + static { + JavaFixProvider annotationRemover = error -> + QuickFixFactory.getInstance().createDeleteFix(error.psi(), JavaAnalysisBundle.message("intention.text.remove.annotation")); + single(ANNOTATION_NOT_ALLOWED_CLASS, annotationRemover); + single(ANNOTATION_NOT_ALLOWED_HERE, annotationRemover); + single(ANNOTATION_NOT_ALLOWED_REF, annotationRemover); + single(ANNOTATION_NOT_ALLOWED_VAR, annotationRemover); + single(ANNOTATION_NOT_ALLOWED_VOID, annotationRemover); + single(ANNOTATION_NOT_APPLICABLE, annotationRemover); + single(ANNOTATION_NOT_ALLOWED_VAR, error -> { + PsiAnnotationOwner owner = error.psi().getOwner(); + PsiTypeElement type = owner instanceof PsiTypeElement te ? te : + PsiTreeUtil.skipSiblingsForward((PsiModifierList)owner, PsiComment.class, PsiWhiteSpace.class, + PsiTypeParameterList.class) instanceof PsiTypeElement te ? te : + null; + return type != null && type.isInferredType() ? new ReplaceVarWithExplicitTypeFix(type) : null; + }); + multi(ANNOTATION_NOT_APPLICABLE, error -> { + if (!BaseIntentionAction.canModify(Objects.requireNonNull(error.psi().resolveAnnotationType()))) return List.of(); + return ContainerUtil.map(error.context(), + targetType -> QuickFixFactory.getInstance().createAddAnnotationTargetFix(error.psi(), targetType)); + }); + single(ANNOTATION_NOT_ALLOWED_STATIC, error -> new MoveAnnotationOnStaticMemberQualifyingTypeFix(error.psi())); + multi(UNSUPPORTED_FEATURE, error -> HighlightUtil.getIncreaseLanguageLevelFixes(error.psi(), error.context())); + single(ANNOTATION_MISSING_ATTRIBUTE, error -> QuickFixFactory.getInstance().createAddMissingRequiredAnnotationParametersFix( + error.psi(), PsiMethod.EMPTY_ARRAY, error.context())); + } + + private static void single(@NotNull JavaErrorKind kind, + @NotNull JavaFixProvider fixProvider) { + multi(kind, fixProvider.asMulti()); + } + + private static void multi(@NotNull JavaErrorKind kind, + @NotNull JavaFixesProvider fixProvider) { + FIXES.computeIfAbsent(kind, k -> new ArrayList<>()).add(fixProvider); + } + + static @NotNull List getFixes(@NotNull JavaCompilationError error) { + var providers = FIXES.get(error.kind()); + if (providers == null) return List.of(); + List fixes = new ArrayList<>(); + for (var provider : providers) { + @SuppressWarnings("unchecked") var fn = (JavaFixesProvider)provider; + fixes.addAll(fn.provide(error)); + } + return fixes; + } +} diff --git a/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/AddMissingRequiredAnnotationParametersFix.java b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/AddMissingRequiredAnnotationParametersFix.java index 39d810b07132..ef935da07a45 100644 --- a/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/AddMissingRequiredAnnotationParametersFix.java +++ b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/AddMissingRequiredAnnotationParametersFix.java @@ -3,6 +3,7 @@ package com.intellij.codeInsight.daemon.impl.quickfix; import com.intellij.codeInsight.daemon.QuickFixBundle; import com.intellij.codeInsight.template.impl.TextExpression; +import com.intellij.java.JavaBundle; import com.intellij.modcommand.*; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.Pair; @@ -25,17 +26,13 @@ import java.util.TreeSet; public final class AddMissingRequiredAnnotationParametersFix extends PsiUpdateModCommandAction { private static final Logger LOG = Logger.getInstance(AddMissingRequiredAnnotationParametersFix.class); - private final PsiMethod[] myAnnotationMethods; private final Collection myMissedElements; - public AddMissingRequiredAnnotationParametersFix(final PsiAnnotation annotation, - final PsiMethod[] annotationMethods, - final Collection missedElements) { + public AddMissingRequiredAnnotationParametersFix(final PsiAnnotation annotation, final Collection missedElements) { super(annotation); if (missedElements.isEmpty()) { throw new IllegalArgumentException("missedElements can't be empty"); } - myAnnotationMethods = annotationMethods; myMissedElements = missedElements; } @@ -55,7 +52,13 @@ public final class AddMissingRequiredAnnotationParametersFix extends PsiUpdateMo protected void invoke(@NotNull ActionContext context, @NotNull PsiAnnotation annotation, @NotNull ModPsiUpdater updater) { final PsiNameValuePair[] addedParameters = annotation.getParameterList().getAttributes(); - final Object2IntMap annotationsOrderMap = getAnnotationsOrderMap(); + PsiClass aClass = annotation.resolveAnnotationType(); + if (aClass == null) { + updater.cancel(JavaBundle.message("error.no.annotation.class.found")); + return; + } + PsiMethod[] methods = aClass.getMethods(); + final Object2IntMap annotationsOrderMap = getAnnotationsOrderMap(methods); final SortedSet> newParameters = new TreeSet<>(Comparator.comparingInt(o -> annotationsOrderMap.getInt(o.getFirst()))); @@ -80,7 +83,7 @@ public final class AddMissingRequiredAnnotationParametersFix extends PsiUpdateMo } PsiElementFactory factory = JavaPsiFacade.getElementFactory(context.project()); - for (PsiMethod method : myAnnotationMethods) { + for (PsiMethod method : methods) { if (myMissedElements.contains(method.getName())) { PsiType type = method.getReturnType(); String defaultValue; @@ -106,10 +109,10 @@ public final class AddMissingRequiredAnnotationParametersFix extends PsiUpdateMo } } - private Object2IntMap getAnnotationsOrderMap() { + private Object2IntMap getAnnotationsOrderMap(PsiMethod[] methods) { final Object2IntMap map = new Object2IntOpenHashMap<>(); - for (int i = 0; i < myAnnotationMethods.length; i++) { - map.put(myAnnotationMethods[i].getName(), i); + for (int i = 0; i < methods.length; i++) { + map.put(methods[i].getName(), i); } return map; } diff --git a/java/java-impl/src/com/intellij/codeInsight/intention/impl/config/QuickFixFactoryImpl.java b/java/java-impl/src/com/intellij/codeInsight/intention/impl/config/QuickFixFactoryImpl.java index 5600d8316d63..5fff262d3cd1 100644 --- a/java/java-impl/src/com/intellij/codeInsight/intention/impl/config/QuickFixFactoryImpl.java +++ b/java/java-impl/src/com/intellij/codeInsight/intention/impl/config/QuickFixFactoryImpl.java @@ -747,7 +747,7 @@ public final class QuickFixFactoryImpl extends QuickFixFactory { public @NotNull IntentionAction createAddMissingRequiredAnnotationParametersFix(final @NotNull PsiAnnotation annotation, final PsiMethod @NotNull [] annotationMethods, final @NotNull Collection missedElements) { - return new AddMissingRequiredAnnotationParametersFix(annotation, annotationMethods, missedElements).asIntention(); + return new AddMissingRequiredAnnotationParametersFix(annotation, missedElements).asIntention(); } @Override diff --git a/java/openapi/resources/messages/JavaBundle.properties b/java/openapi/resources/messages/JavaBundle.properties index 41c347032f49..116a22b0db63 100644 --- a/java/openapi/resources/messages/JavaBundle.properties +++ b/java/openapi/resources/messages/JavaBundle.properties @@ -1964,4 +1964,5 @@ advanced.setting.java.show.irrelevant.templates.in.source.roots=Show irrelevant java.test.use.wall.time=Use Wall Time java.test.use.live.time=Use Live Time java.test.overall.time=Overall time: {0} -java.test.sum.time=Sum time: {0} \ No newline at end of file +java.test.sum.time=Sum time: {0} +error.no.annotation.class.found=No annotation class found \ No newline at end of file diff --git a/java/plugin/plugin-content.yaml b/java/plugin/plugin-content.yaml index 50e22cb0cac4..4e09534b1ad9 100644 --- a/java/plugin/plugin-content.yaml +++ b/java/plugin/plugin-content.yaml @@ -37,6 +37,7 @@ - name: intellij.platform.uast - name: intellij.platform.uast.ide - name: intellij.java.uast.ide + - name: intellij.java.codeserver.highlighting - name: intellij.java.compiler.impl - name: intellij.java.debugger.impl - name: intellij.java.debugger.memory.agent diff --git a/platform/build-scripts/src/org/jetbrains/intellij/build/JavaPluginLayout.kt b/platform/build-scripts/src/org/jetbrains/intellij/build/JavaPluginLayout.kt index d347945bf5d7..d312bfcbb24f 100644 --- a/platform/build-scripts/src/org/jetbrains/intellij/build/JavaPluginLayout.kt +++ b/platform/build-scripts/src/org/jetbrains/intellij/build/JavaPluginLayout.kt @@ -59,6 +59,7 @@ object JavaPluginLayout { } spec.withModules(listOf( + "intellij.java.codeserver.highlighting", "intellij.java.compiler.impl", "intellij.java.debugger.impl", "intellij.java.terminal",