From 798b0daf38064368618588a88c7ad9b8df6b510d Mon Sep 17 00:00:00 2001
From: Roman Shevchenko
Date: Wed, 30 Mar 2016 12:49:55 +0200
Subject: [PATCH] [java] javadoc HTML lint inspection (IDEA-114977)
---
.../javaDoc/JavadocHtmlLintAnnotator.kt | 212 ++++++++++++++++++
.../javaDoc/JavadocHtmlLintInspection.kt | 39 ++++
java/java-tests/java-tests.iml | 1 +
.../daemon/JavadocHtmlLintInspectionTest.kt | 67 ++++++
.../configurations/SimpleJavaParameters.java | 21 +-
.../src/messages/InspectionsBundle.properties | 1 +
.../JavadocHtmlLint.html | 5 +
resources/src/META-INF/IdeaPlugin.xml | 6 +
8 files changed, 344 insertions(+), 8 deletions(-)
create mode 100644 java/java-impl/src/com/intellij/codeInspection/javaDoc/JavadocHtmlLintAnnotator.kt
create mode 100644 java/java-impl/src/com/intellij/codeInspection/javaDoc/JavadocHtmlLintInspection.kt
create mode 100644 java/java-tests/testSrc/com/intellij/codeInsight/daemon/JavadocHtmlLintInspectionTest.kt
create mode 100644 resources-en/src/inspectionDescriptions/JavadocHtmlLint.html
diff --git a/java/java-impl/src/com/intellij/codeInspection/javaDoc/JavadocHtmlLintAnnotator.kt b/java/java-impl/src/com/intellij/codeInspection/javaDoc/JavadocHtmlLintAnnotator.kt
new file mode 100644
index 000000000000..ee3904d7e78d
--- /dev/null
+++ b/java/java-impl/src/com/intellij/codeInspection/javaDoc/JavadocHtmlLintAnnotator.kt
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2000-2016 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.codeInspection.javaDoc
+
+import com.intellij.codeInsight.daemon.HighlightDisplayKey
+import com.intellij.codeInsight.intention.EmptyIntentionAction
+import com.intellij.codeInspection.InspectionsBundle
+import com.intellij.execution.configurations.GeneralCommandLine
+import com.intellij.execution.configurations.SimpleJavaParameters
+import com.intellij.execution.util.ExecUtil
+import com.intellij.lang.annotation.Annotation
+import com.intellij.lang.annotation.AnnotationHolder
+import com.intellij.lang.annotation.ExternalAnnotator
+import com.intellij.openapi.application.PathManager
+import com.intellij.openapi.diagnostic.Logger
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.projectRoots.JavaSdk
+import com.intellij.openapi.projectRoots.JavaSdkVersion
+import com.intellij.openapi.projectRoots.Sdk
+import com.intellij.openapi.roots.ModuleRootManager
+import com.intellij.openapi.roots.ProjectFileIndex
+import com.intellij.openapi.roots.ProjectRootManager
+import com.intellij.openapi.util.TextRange
+import com.intellij.openapi.util.io.FileUtil
+import com.intellij.openapi.util.text.StringUtil
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.pom.java.LanguageLevel
+import com.intellij.profile.codeInspection.InspectionProjectProfileManager
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiFile
+import com.intellij.psi.PsiJavaFile
+import com.intellij.psi.javadoc.PsiDocComment
+import com.intellij.psi.util.PsiTreeUtil
+import com.sun.tools.doclint.DocLint
+import java.io.File
+
+class JavadocHtmlLintAnnotator : ExternalAnnotator() {
+ data class Info(val file: PsiFile)
+ data class Anno(val row: Int, val col: Int, val error: Boolean, val message: String)
+ data class Result(val annotations: List)
+
+ override fun collectInformation(file: PsiFile): Info? =
+ if (isJava8SourceFile(file) && file.text.contains("/**") && isToolEnabled(file)) Info(file) else null
+
+ override fun doAnnotate(collectedInfo: Info): Result? {
+ val file = collectedInfo.file.virtualFile!!
+ val copy = createTempFile(collectedInfo.file.text.toByteArray(file.charset))
+
+ try {
+ val command = toolCommand(file, collectedInfo.file.project, copy)
+
+ val output = ExecUtil.execAndGetOutput(command)
+ if (output.exitCode != 0) {
+ val log = Logger.getInstance(JavadocHtmlLintAnnotator::class.java)
+ if (log.isDebugEnabled) log.debug("${file}: ${output.exitCode}, ${output.stderr}")
+ return null
+ }
+
+ val annotations = parse(output.stdoutLines)
+ return if (annotations.isNotEmpty()) Result(annotations) else null
+ }
+ finally {
+ FileUtil.delete(copy)
+ }
+ }
+
+ override fun apply(file: PsiFile, annotationResult: Result, holder: AnnotationHolder) {
+ val text = file.text
+ val offsets = text.foldIndexed(mutableListOf(0)) { i, offsets, c -> if (c == '\n') offsets += (i + 1); offsets }
+
+ for ((row, col, error, message) in annotationResult.annotations) {
+ if (row < offsets.size) {
+ val offset = offsets[row] + col
+ val element = file.findElementAt(offset)
+ if (element != null && PsiTreeUtil.getParentOfType(element, PsiDocComment::class.java) != null) {
+ val range = adjust(element, text, offset)
+ val description = StringUtil.capitalize(message)
+ val annotation = when (error) {
+ true -> holder.createErrorAnnotation(range, description)
+ false -> holder.createWarningAnnotation(range, description)
+ }
+ registerFix(annotation)
+ }
+ }
+ }
+ }
+
+ //
+
+ private val jdk = lazy {
+ var jdkHome = File(System.getProperty("java.home"))
+ if (jdkHome.name == "jre") jdkHome = jdkHome.parentFile
+ JavaSdk.getInstance().createJdk("(internal JDK)", jdkHome.path)
+ }
+
+ private val key = lazy { HighlightDisplayKey.find(JavadocHtmlLintInspection.SHORT_NAME) }
+
+ private val lintOptions = "${DocLint.XMSGS_CUSTOM_PREFIX}html/private,accessibility/private"
+ private val lintPattern = "^[^:]+:(\\d+):\\s+(error|warning):\\s+(.+)$".toPattern()
+
+ private fun isJava8SourceFile(file: PsiFile) =
+ file is PsiJavaFile && file.languageLevel.isAtLeast(LanguageLevel.JDK_1_8) &&
+ file.virtualFile != null && ProjectFileIndex.SERVICE.getInstance(file.project).isInSourceContent(file.virtualFile)
+
+ private fun isToolEnabled(file: PsiFile) =
+ InspectionProjectProfileManager.getInstance(file.project).inspectionProfile.isToolEnabled(key.value, file)
+
+ private fun createTempFile(bytes: ByteArray): File {
+ val tempFile = FileUtil.createTempFile(File(PathManager.getTempPath()), "javadocHtmlLint", ".java")
+ tempFile.writeBytes(bytes)
+ return tempFile
+ }
+
+ private fun toolCommand(file: VirtualFile, project: Project, copy: File): GeneralCommandLine {
+ val parameters = SimpleJavaParameters()
+
+ val jdk = findJdk(file, project)
+ parameters.jdk = jdk
+
+ val toolsJar = File("${jdk.homePath}/lib/tools.jar")
+ if (toolsJar.exists()) parameters.classPath.add(toolsJar.path)
+
+ parameters.charset = file.charset
+ parameters.vmParametersList.addProperty("user.language", "en")
+ parameters.mainClass = DocLint::class.qualifiedName
+ parameters.programParametersList.add(lintOptions)
+ parameters.programParametersList.add(copy.path)
+
+ val cmd = parameters.toCommandLine()
+ val exeFile = File(cmd.exePath)
+ if (!exeFile.exists()) cmd.exePath = File(exeFile.parentFile.parentFile, "jre/bin/${exeFile.name}").path
+
+ return cmd
+ }
+
+ private fun findJdk(file: VirtualFile, project: Project): Sdk {
+ val rootManager = ProjectRootManager.getInstance(project)
+
+ val module = rootManager.fileIndex.getModuleForFile(file)
+ if (module != null) {
+ val sdk = ModuleRootManager.getInstance(module).sdk
+ if (isJdk8(sdk)) return sdk!!
+ }
+
+ val sdk = rootManager.projectSdk
+ if (isJdk8(sdk)) return sdk!!
+
+ return jdk.value
+ }
+
+ private fun isJdk8(sdk: Sdk?) =
+ sdk != null &&
+ sdk.sdkType is JavaSdk && (sdk.sdkType as JavaSdk).isOfVersionOrHigher(sdk, JavaSdkVersion.JDK_1_8) &&
+ JavaSdk.checkForJdk(File(sdk.homePath))
+
+ private fun parse(lines: List): List {
+ val result = mutableListOf()
+
+ val i = lines.iterator()
+ while (i.hasNext()) {
+ val line = i.next()
+ val matcher = lintPattern.matcher(line)
+ if (matcher.matches() && i.hasNext() && !i.next().isEmpty() && i.hasNext()) {
+ val row = matcher.group(1).toInt() - 1
+ val col = i.next().indexOf('^')
+ val error = matcher.group(2) == "error"
+ val message = matcher.group(3)
+ result += Anno(row, col, error, message)
+ }
+ }
+
+ return result
+ }
+
+ private fun adjust(element: PsiElement, text: String, offset: Int): TextRange {
+ val range = element.textRange
+
+ if (text[offset] == '<') {
+ val right = text.indexOf('>', offset)
+ if (right > 0) return TextRange(offset, Integer.min(right + 1, range.endOffset))
+ }
+ else if (text[offset] == '&') {
+ val right = text.indexOf(';', offset)
+ if (right > 0) return TextRange(offset, Integer.min(right + 1, range.endOffset))
+ }
+ else if (text[offset].isLetter() && !text[offset - 1].isLetter()) {
+ var right = offset + 1
+ while (text[right].isLetter() && right <= range.endOffset) right++
+ return TextRange(offset, right)
+ }
+
+ return range
+ }
+
+ private fun registerFix(annotation: Annotation) =
+ annotation.registerFix(EmptyIntentionAction(InspectionsBundle.message("inspection.javadoc.lint.display.name")), null, key.value)
+
+ //
+}
\ No newline at end of file
diff --git a/java/java-impl/src/com/intellij/codeInspection/javaDoc/JavadocHtmlLintInspection.kt b/java/java-impl/src/com/intellij/codeInspection/javaDoc/JavadocHtmlLintInspection.kt
new file mode 100644
index 000000000000..5eb815baf900
--- /dev/null
+++ b/java/java-impl/src/com/intellij/codeInspection/javaDoc/JavadocHtmlLintInspection.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2000-2016 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.codeInspection.javaDoc
+
+import com.intellij.codeInspection.ExternalAnnotatorInspectionVisitor
+import com.intellij.codeInspection.LocalInspectionTool
+import com.intellij.codeInspection.ProblemsHolder
+import com.intellij.codeInspection.SuppressQuickFix
+import com.intellij.codeInspection.ex.UnfairLocalInspectionTool
+import com.intellij.lang.ExternalLanguageAnnotators
+import com.intellij.lang.java.JavaLanguage
+import com.intellij.psi.PsiElement
+
+class JavadocHtmlLintInspection : LocalInspectionTool(), UnfairLocalInspectionTool {
+ companion object {
+ val SHORT_NAME = "JavadocHtmlLint"
+ }
+
+ private val annotator = lazy {
+ ExternalLanguageAnnotators.INSTANCE.allForLanguage(JavaLanguage.INSTANCE).find { it is JavadocHtmlLintAnnotator }
+ }
+
+ override fun buildVisitor(holder: ProblemsHolder, onTheFly: Boolean) = ExternalAnnotatorInspectionVisitor(holder, annotator.value, onTheFly)
+
+ override fun getBatchSuppressActions(element: PsiElement?) = SuppressQuickFix.EMPTY_ARRAY
+}
\ No newline at end of file
diff --git a/java/java-tests/java-tests.iml b/java/java-tests/java-tests.iml
index 18d22b84d6cb..ab5d1ee8eff4 100644
--- a/java/java-tests/java-tests.iml
+++ b/java/java-tests/java-tests.iml
@@ -51,5 +51,6 @@
+
\ No newline at end of file
diff --git a/java/java-tests/testSrc/com/intellij/codeInsight/daemon/JavadocHtmlLintInspectionTest.kt b/java/java-tests/testSrc/com/intellij/codeInsight/daemon/JavadocHtmlLintInspectionTest.kt
new file mode 100644
index 000000000000..d7c7ae0c159f
--- /dev/null
+++ b/java/java-tests/testSrc/com/intellij/codeInsight/daemon/JavadocHtmlLintInspectionTest.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2000-2016 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.codeInsight.daemon
+
+import com.intellij.codeInspection.javaDoc.JavadocHtmlLintInspection
+import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase
+
+class JavadocHtmlLintInspectionTest : LightCodeInsightFixtureTestCase() {
+ fun testNoComment() = doTest("class C { }")
+ fun testEmptyComment() = doTest("/** */\nclass C { }")
+
+ fun testCommonErrors() = doTest("""
+ package pkg;
+ /**
+ *
+ *
+ * Empty paragraph:
+ *
+ * Line break:
+ * Another one:
+ * And the last one:
+ *
+ * Missing open tag:
+ *
+ * Unescaped angle brackets for generics: List
+ * (closing it here to avoid further confusion: )
+ * Correct: {@code List}
+ *
+ * Unknown attribute:
a="">
+ *
+ * id="1" id="2">Some repeated attributes
+ *
+ * Empty ref: href="">link
+ *
+ * Incorrect header
+ *
+ * Unknown entity: &wtf;
+ *
+ * @see bad_link should report no error
+ */
+ class C { }""".trimIndent())
+
+ fun testPackageInfo() = doTest("""
+ /**
+ * Another self-closed paragraph:
+ */
+ package pkg;""".trimIndent(), "package-info.java")
+
+ private fun doTest(text: String, name: String? = null) {
+ myFixture.enableInspections(JavadocHtmlLintInspection())
+ myFixture.configureByText(name ?: "${getTestName(false)}.java", text)
+ myFixture.checkHighlighting(true, false, false)
+ }
+}
\ No newline at end of file
diff --git a/platform/lang-api/src/com/intellij/execution/configurations/SimpleJavaParameters.java b/platform/lang-api/src/com/intellij/execution/configurations/SimpleJavaParameters.java
index 67cf6f9fea99..3316113fb520 100644
--- a/platform/lang-api/src/com/intellij/execution/configurations/SimpleJavaParameters.java
+++ b/platform/lang-api/src/com/intellij/execution/configurations/SimpleJavaParameters.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2000-2015 JetBrains s.r.o.
+ * Copyright 2000-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package com.intellij.execution.configurations;
import com.intellij.execution.ExecutionException;
@@ -25,6 +24,7 @@ import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.vfs.CharsetToolkit;
import com.intellij.util.PathsList;
import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.nio.charset.Charset;
@@ -109,13 +109,18 @@ public class SimpleJavaParameters extends SimpleProgramParameters {
myUseClasspathJar = useClasspathJar;
}
+ @NotNull
+ public GeneralCommandLine toCommandLine() {
+ Sdk jdk = getJdk();
+ if (jdk == null) throw new IllegalArgumentException("SDK should be defined");
+ String exePath = ((JavaSdkType)jdk.getSdkType()).getVMExecutablePath(jdk);
+ return JdkUtil.setupJVMCommandLine(exePath, this, myUseDynamicClasspath);
+ }
+
+ @NotNull
public OSProcessHandler createOSProcessHandler() throws ExecutionException {
- final Sdk sdk = getJdk();
- assert sdk != null : "SDK should be defined";
- final String exePath = ((JavaSdkType)sdk.getSdkType()).getVMExecutablePath(sdk);
- final GeneralCommandLine commandLine = JdkUtil.setupJVMCommandLine(exePath, this, myUseDynamicClasspath);
- final OSProcessHandler processHandler = new OSProcessHandler(commandLine);
+ OSProcessHandler processHandler = new OSProcessHandler(toCommandLine());
ProcessTerminatedListener.attach(processHandler);
return processHandler;
}
-}
+}
\ No newline at end of file
diff --git a/platform/platform-resources-en/src/messages/InspectionsBundle.properties b/platform/platform-resources-en/src/messages/InspectionsBundle.properties
index c620f262226f..785cac12fcac 100644
--- a/platform/platform-resources-en/src/messages/InspectionsBundle.properties
+++ b/platform/platform-resources-en/src/messages/InspectionsBundle.properties
@@ -256,6 +256,7 @@ inspection.actiongo.prev=Go Prev Problem
inspection.javadoc.display.name=Declaration has Javadoc problems
inspection.javadoc.ref.display.name=Declaration has problems in Javadoc references
+inspection.javadoc.lint.display.name=HTML problems in Javadoc (DocLint)
inspection.javadoc.option.tab.title.package=Package
inspection.javadoc.option.tab.title=Class
inspection.javadoc.option.tab.title.method=Method
diff --git a/resources-en/src/inspectionDescriptions/JavadocHtmlLint.html b/resources-en/src/inspectionDescriptions/JavadocHtmlLint.html
new file mode 100644
index 000000000000..96dcb29310dc
--- /dev/null
+++ b/resources-en/src/inspectionDescriptions/JavadocHtmlLint.html
@@ -0,0 +1,5 @@
+
+
+This inspection detects HTML issues in the Javadoc comments (such as unclosed, empty, or misplaced tags) outlawed since Java 8.
+
+
\ No newline at end of file
diff --git a/resources/src/META-INF/IdeaPlugin.xml b/resources/src/META-INF/IdeaPlugin.xml
index ddab7edfdfbb..423f65555023 100644
--- a/resources/src/META-INF/IdeaPlugin.xml
+++ b/resources/src/META-INF/IdeaPlugin.xml
@@ -629,12 +629,18 @@
+
+
+
+