diff --git a/java/java-analysis-impl/src/com/siyeh/ig/migration/MarkdownDocumentationCommentsMigrationInspection.java b/java/java-analysis-impl/src/com/siyeh/ig/migration/MarkdownDocumentationCommentsMigrationInspection.java index abd747888d89..522baa2b46ea 100644 --- a/java/java-analysis-impl/src/com/siyeh/ig/migration/MarkdownDocumentationCommentsMigrationInspection.java +++ b/java/java-analysis-impl/src/com/siyeh/ig/migration/MarkdownDocumentationCommentsMigrationInspection.java @@ -1,6 +1,7 @@ // Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.siyeh.ig.migration; +import com.intellij.codeInsight.intention.AddAnnotationPsiFix; import com.intellij.codeInsight.javadoc.JavaDocUtil; import com.intellij.codeInspection.LocalQuickFix; import com.intellij.modcommand.ModPsiUpdater; @@ -8,11 +9,19 @@ import com.intellij.modcommand.PsiUpdateModCommandQuickFix; import com.intellij.openapi.editor.Document; import com.intellij.openapi.project.DumbAware; import com.intellij.openapi.project.Project; +import com.intellij.openapi.roots.LanguageLevelProjectExtension; import com.intellij.openapi.util.NlsSafe; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.util.text.Strings; +import com.intellij.pom.java.JavaFeature; +import com.intellij.psi.CommonClassNames; import com.intellij.psi.JavaDocTokenType; +import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiJavaDocumentedElement; +import com.intellij.psi.PsiModifierList; +import com.intellij.psi.PsiModifierListOwner; +import com.intellij.psi.PsiNameValuePair; import com.intellij.psi.PsiWhiteSpace; import com.intellij.psi.impl.source.javadoc.PsiDocMethodOrFieldRef; import com.intellij.psi.impl.source.javadoc.PsiDocParamRef; @@ -91,9 +100,27 @@ public final class MarkdownDocumentationCommentsMigrationInspection extends Base @Override protected void applyFix(@NotNull Project project, @NotNull PsiElement element, @NotNull ModPsiUpdater updater) { if (element instanceof PsiDocToken) element = element.getParent(); - if (!(element instanceof PsiDocComment)) return; - String markdown = convertToMarkdown(appendElementText(element, new StringBuilder()).toString()); - String indent = getElementIndent(element); + if (!(element instanceof PsiDocComment docComment)) return; + + String result = convertAndPostProcess(element); + Document document = element.getContainingFile().getFileDocument(); + + if (addDeprecatedAnnotationIfNecessary(project, docComment)) { + PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(document); + } + + int startOffset = element.getTextOffset(); + int endOffset = element.getNextSibling() instanceof PsiWhiteSpace whiteSpace + ? whiteSpace.getTextOffset() + whiteSpace.getTextLength() + : startOffset + element.getTextLength(); + document.replaceString(startOffset, endOffset, result); + } + + /// @return The converted and indent post-processed Markdown comment + private static String convertAndPostProcess(PsiElement docComment) { + String markdown = convertToMarkdown(appendElementText(docComment, new StringBuilder()).toString()); + + String indent = getElementIndent(docComment); String[] lines = markdown.split("\n"); StringBuilder result = new StringBuilder(markdown.length() + (indent.length() + 4) * lines.length); for (String line : lines) { @@ -108,13 +135,7 @@ public final class MarkdownDocumentationCommentsMigrationInspection extends Base result.append('\n'); } result.append(indent); - - Document document = element.getContainingFile().getFileDocument(); - int startOffset = element.getTextOffset(); - int endOffset = element.getNextSibling() instanceof PsiWhiteSpace whiteSpace - ? whiteSpace.getTextOffset() + whiteSpace.getTextLength() - : startOffset + element.getTextLength(); - document.replaceString(startOffset, endOffset, result); + return result.toString(); } private static StringBuilder appendElementText(@NotNull PsiElement element, StringBuilder result) { @@ -197,7 +218,7 @@ public final class MarkdownDocumentationCommentsMigrationInspection extends Base String result = visitor.getResult(); - // (mbo) Not the proudest of this one but some combinations of Javadoc tag and HTML cannot reasonnably be handled with jsoup + // (mbo) Not the proudest of this one, but some combinations of Javadoc tag and HTML cannot reasonably be handled with jsoup // unescape element between internal HTML tags. It is expected that internal tags are not nested. Matcher internalTagMatcher = Pattern.compile( "<(?:%s|%s)>(.*?)".formatted( @@ -328,6 +349,28 @@ public final class MarkdownDocumentationCommentsMigrationInspection extends Base .append(']'); } } + + /// Add the annotation if necessary, as the deprecated tag alone is not enough to indicate deprecation + /// according to the javadoc Markdown specs + /// + /// @return Whether the annotation was added (or is already there) + @ApiStatus.Internal + private static boolean addDeprecatedAnnotationIfNecessary(Project project, PsiDocComment docComment) { + if (!JavaFeature.ANNOTATIONS.isSufficient(LanguageLevelProjectExtension.getInstance(project).getLanguageLevel()) || + docComment.findTagByName("deprecated") == null) { + return false; + } + PsiJavaDocumentedElement owner = docComment.getOwner(); + if (owner instanceof PsiModifierListOwner modifierListOwner) { + PsiModifierList modifierList = modifierListOwner.getModifierList(); + if (modifierList != null) { + AddAnnotationPsiFix.addPhysicalAnnotationIfAbsent(CommonClassNames.JAVA_LANG_DEPRECATED, PsiNameValuePair.EMPTY_ARRAY, + modifierList); + return true; + } + } + return false; + } } diff --git a/java/java-backend/resources/META-INF/InspectionGadgets.xml b/java/java-backend/resources/META-INF/InspectionGadgets.xml index 6887336a6515..110838d17c0e 100644 --- a/java/java-backend/resources/META-INF/InspectionGadgets.xml +++ b/java/java-backend/resources/META-INF/InspectionGadgets.xml @@ -1261,7 +1261,7 @@ implementationClass="com.siyeh.ig.javadoc.HtmlTagCanBeJavadocTagInspection" cleanupTool="true"/>
 X
public <T extends Number> double calculateMd(
@NotNulli T a, +
 X
@Deprecated 
+public <T extends Number> double calculateMd(
@NotNulli T a, @NotNulli T b
)

Sample method demonstrating all Javadoc tags
Returns calculated result.
Math.E
diff --git a/java/java-tests/testData/codeInsight/javadocIG/allTagsMarkdown.java b/java/java-tests/testData/codeInsight/javadocIG/allTagsMarkdown.java index cb963b5a4d7b..2643b006e674 100644 --- a/java/java-tests/testData/codeInsight/javadocIG/allTagsMarkdown.java +++ b/java/java-tests/testData/codeInsight/javadocIG/allTagsMarkdown.java @@ -1,4 +1,5 @@ import java.lang.Math; +import java.lang.Deprecated; class X { @@ -30,6 +31,7 @@ class X { /// @provides Math /// @uses Math /// @hidden + @Deprecated public double calculateMd(T a, T b) { return a.doubleValue() + b.doubleValue(); } diff --git a/java/java-tests/testData/codeInsight/javadocIG/deprecatedTagNoAnnotationMarkdown.html b/java/java-tests/testData/codeInsight/javadocIG/deprecatedTagNoAnnotationMarkdown.html new file mode 100644 index 000000000000..000cb2da9ac5 --- /dev/null +++ b/java/java-tests/testData/codeInsight/javadocIG/deprecatedTagNoAnnotationMarkdown.html @@ -0,0 +1 @@ +

class X

\ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/javadocIG/deprecatedTagNoAnnotationMarkdown.java b/java/java-tests/testData/codeInsight/javadocIG/deprecatedTagNoAnnotationMarkdown.java new file mode 100644 index 000000000000..4af3a41be95a --- /dev/null +++ b/java/java-tests/testData/codeInsight/javadocIG/deprecatedTagNoAnnotationMarkdown.java @@ -0,0 +1,2 @@ +/// @deprecated Oops look at who forgot the annotation +class X {} \ No newline at end of file diff --git a/java/java-tests/testData/ig/com/siyeh/igtest/migration/markdown_documentation_comments_migration/DeprecatedUsages.after.java b/java/java-tests/testData/ig/com/siyeh/igtest/migration/markdown_documentation_comments_migration/DeprecatedUsages.after.java new file mode 100644 index 000000000000..398105decab2 --- /dev/null +++ b/java/java-tests/testData/ig/com/siyeh/igtest/migration/markdown_documentation_comments_migration/DeprecatedUsages.after.java @@ -0,0 +1,22 @@ +import java.lang.Deprecated; +public class DeprecatedUsages { + + /// @deprecated We before java 5 there were no annotations + /// so that how you were expected to deprecate something + @Deprecated + void onlyTag() {} + + /// @deprecated + @Deprecated + void emptyTag() {} + + /// @deprecated + @Deprecated + void emptyTagWithAnnotation() {} + + + /// @deprecated Markdown Javadoc updated the meaning of the deprecated tag. + /// This is a bit of heresy and honestly artificial friction but so be it. + @Deprecated + void tagWithAnnotation() {} +} \ No newline at end of file diff --git a/java/java-tests/testData/ig/com/siyeh/igtest/migration/markdown_documentation_comments_migration/DeprecatedUsages.java b/java/java-tests/testData/ig/com/siyeh/igtest/migration/markdown_documentation_comments_migration/DeprecatedUsages.java new file mode 100644 index 000000000000..fe9c52a96e0a --- /dev/null +++ b/java/java-tests/testData/ig/com/siyeh/igtest/migration/markdown_documentation_comments_migration/DeprecatedUsages.java @@ -0,0 +1,24 @@ +import java.lang.Deprecated; +public class DeprecatedUsages { + + /** + * @deprecated We before java 5 there were no annotations + * so that how you were expected to deprecate something + */ + void onlyTag() {} + + /** @deprecated */ + void emptyTag() {} + + /** @deprecated */ + @Deprecated + void emptyTagWithAnnotation() {} + + + /** + * @deprecated Markdown Javadoc updated the meaning of the deprecated tag. + * This is a bit of heresy and honestly artificial friction but so be it. + */ + @Deprecated + void tagWithAnnotation() {} +} \ No newline at end of file diff --git a/java/java-tests/testData/ig/com/siyeh/igtest/migration/markdown_documentation_comments_migration/MarkdownDocumentationCommentsMigration.after.java b/java/java-tests/testData/ig/com/siyeh/igtest/migration/markdown_documentation_comments_migration/MarkdownDocumentationCommentsMigration.after.java index 0f16fbc16abc..797145228cec 100644 --- a/java/java-tests/testData/ig/com/siyeh/igtest/migration/markdown_documentation_comments_migration/MarkdownDocumentationCommentsMigration.after.java +++ b/java/java-tests/testData/ig/com/siyeh/igtest/migration/markdown_documentation_comments_migration/MarkdownDocumentationCommentsMigration.after.java @@ -298,6 +298,7 @@ public class MarkdownDocumentationCommentsMigration { /// /// This method will remain in place until finalizers have been removed from /// most existing code. + @Deprecated @Override protected void finalize() throws Throwable { super.finalize(); diff --git a/java/java-tests/testData/psi/repositoryUse/src/pack/MyClass2.java b/java/java-tests/testData/psi/repositoryUse/src/pack/MyClass2.java index 2eb4bcbf7cab..82ffe2a3c0d1 100644 --- a/java/java-tests/testData/psi/repositoryUse/src/pack/MyClass2.java +++ b/java/java-tests/testData/psi/repositoryUse/src/pack/MyClass2.java @@ -6,6 +6,7 @@ public class MyClass2 extends String implements Runnable{ */ int field1 = 0; + /// @deprecated The annotation is missing so no real deprecation Object field2[]; java.lang.Object[] field3; diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/javadoc/JavaDocInfoGeneratorTest.java b/java/java-tests/testSrc/com/intellij/java/codeInsight/javadoc/JavaDocInfoGeneratorTest.java index d5f79381351e..e1a271a080b0 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInsight/javadoc/JavaDocInfoGeneratorTest.java +++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/javadoc/JavaDocInfoGeneratorTest.java @@ -268,6 +268,7 @@ public class JavaDocInfoGeneratorTest extends JavaCodeInsightTestCase { public void testWrongfulInnerClassReferences() { doTestClass(); } public void testImplicitConstructor() { doTestClass(); } public void testCommatHtmlEntity() { doTestClass(); } + public void testDeprecatedTagNoAnnotationMarkdown() { doTestClass(); } public void testRepeatableAnnotations() { useJava8(); diff --git a/java/java-tests/testSrc/com/intellij/java/psi/JavaStubsTest.java b/java/java-tests/testSrc/com/intellij/java/psi/JavaStubsTest.java index 58dd14538c9b..ecbd6e258cee 100644 --- a/java/java-tests/testSrc/com/intellij/java/psi/JavaStubsTest.java +++ b/java/java-tests/testSrc/com/intellij/java/psi/JavaStubsTest.java @@ -180,12 +180,18 @@ public class JavaStubsTest extends LightJavaCodeInsightFixtureTestCase { } public void test_deprecated_enum_constant() { - PsiClass cls = myFixture.addClass("enum Foo { c1, @Deprecated c2, /** @deprecated */ c3 }"); + PsiClass cls = myFixture.addClass(""" + enum Foo { + c1, @Deprecated c2, /** @deprecated */ c3, + /// @deprecated no real deprecation + c4 + }"""); assertFalse(((PsiFileImpl)cls.getContainingFile()).isContentsLoaded()); assertFalse(cls.getFields()[0].isDeprecated()); assertTrue(cls.getFields()[1].isDeprecated()); assertTrue(cls.getFields()[2].isDeprecated()); + assertFalse(cls.getFields()[3].isDeprecated()); assertFalse(((PsiFileImpl)cls.getContainingFile()).isContentsLoaded()); } diff --git a/java/java-tests/testSrc/com/intellij/java/psi/SrcRepositoryUseTest.java b/java/java-tests/testSrc/com/intellij/java/psi/SrcRepositoryUseTest.java index ed2431a94bae..a38fb6608226 100644 --- a/java/java-tests/testSrc/com/intellij/java/psi/SrcRepositoryUseTest.java +++ b/java/java-tests/testSrc/com/intellij/java/psi/SrcRepositoryUseTest.java @@ -308,6 +308,9 @@ public class SrcRepositoryUseTest extends JavaPsiTestCase { PsiField field = aClass.findFieldByName("field1", false); assertTrue(field.isDeprecated()); + + PsiField field2 = aClass.findFieldByName("field2", false); + assertFalse(field2.isDeprecated()); teardownLoadingFilter(); } diff --git a/java/java-tests/testSrc/com/siyeh/ig/migration/MarkdownDocumentationCommentsMigrationInspectionTest.java b/java/java-tests/testSrc/com/siyeh/ig/migration/MarkdownDocumentationCommentsMigrationInspectionTest.java index 68823795d063..64ef44ed4b62 100644 --- a/java/java-tests/testSrc/com/siyeh/ig/migration/MarkdownDocumentationCommentsMigrationInspectionTest.java +++ b/java/java-tests/testSrc/com/siyeh/ig/migration/MarkdownDocumentationCommentsMigrationInspectionTest.java @@ -2,18 +2,20 @@ package com.siyeh.ig.migration; import com.intellij.codeInspection.InspectionProfileEntry; +import com.intellij.testFramework.TestDataPath; import com.siyeh.ig.LightJavaInspectionTestCase; import org.jetbrains.annotations.Nullable; /** * @author Bas Leijdekkers */ +@TestDataPath("$CONTENT_ROOT/testData/ig/com/siyeh/igtest/migration/markdown_documentation_comments_migration") public class MarkdownDocumentationCommentsMigrationInspectionTest extends LightJavaInspectionTestCase { public void testMarkdownDocumentationCommentsMigration() { check(); } public void testReferencesNoEscape() { check(); } public void testCodeBlocks() { check(); } - + public void testDeprecatedUsages() { check(); } @Override protected @Nullable InspectionProfileEntry getInspection() { @@ -24,5 +26,4 @@ public class MarkdownDocumentationCommentsMigrationInspectionTest extends LightJ doTest(); checkQuickFixAll(); } - } \ No newline at end of file