From 3ef67e1d9dd14af1f07512025518803b5188e8e9 Mon Sep 17 00:00:00 2001 From: Nikolay Krasko Date: Mon, 19 Mar 2018 01:38:44 +0300 Subject: [PATCH] Inspection for highlighting problem API usage It's planned to use this inspection for show patchset branches problems during development process. --- .../inspections/AbstractKotlinInspection.kt | 2 +- .../IncompatibleAPI.html | 5 + .../api/AddIncompatibleApiAction.kt | 40 ++++ .../api/AddToIncompatibleApiDialog.form | 60 ++++++ .../api/AddToIncompatibleApiDialog.java | 81 +++++++ .../api/IncompatibleAPIInspection.kt | 201 ++++++++++++++++++ .../api/IncompatibleAPIJavaVisitor.kt | 106 +++++++++ .../api/IncompatibleAPIKotlinVisitor.kt | 86 ++++++++ idea/src/META-INF/jvm.xml | 12 ++ .../inspections/incompatibleAPI/JavaTest.java | 45 ++++ .../inspections/incompatibleAPI/classes.kt | 17 ++ .../incompatibleAPI/constructors.kt | 8 + .../inspectionData/expected.xml | 87 ++++++++ .../inspectionData/inspections.test | 1 + .../inspectionData/settings.xml | 12 ++ .../incompatibleAPI/lib/LibClass.java | 4 + .../incompatibleAPI/lib/LibConstructor.java | 9 + .../incompatibleAPI/lib/LibMethods.java | 12 ++ .../incompatibleAPI/lib/LibSuper.java | 7 + .../inspections/incompatibleAPI/overloads.kt | 11 + .../inspections/incompatibleAPI/static.kt | 7 + .../inspections/incompatibleAPI/super.kt | 14 ++ .../codeInsight/AbstractInspectionTest.kt | 13 +- .../codeInsight/InspectionTestGenerated.java | 5 + .../idea/inspections/InspectionUtils.kt | 7 +- 25 files changed, 849 insertions(+), 3 deletions(-) create mode 100644 idea/idea-jvm/resources/inspectionDescriptions/IncompatibleAPI.html create mode 100644 idea/idea-jvm/src/org/jetbrains/kotlin/idea/inspections/api/AddIncompatibleApiAction.kt create mode 100644 idea/idea-jvm/src/org/jetbrains/kotlin/idea/inspections/api/AddToIncompatibleApiDialog.form create mode 100644 idea/idea-jvm/src/org/jetbrains/kotlin/idea/inspections/api/AddToIncompatibleApiDialog.java create mode 100644 idea/idea-jvm/src/org/jetbrains/kotlin/idea/inspections/api/IncompatibleAPIInspection.kt create mode 100644 idea/idea-jvm/src/org/jetbrains/kotlin/idea/inspections/api/IncompatibleAPIJavaVisitor.kt create mode 100644 idea/idea-jvm/src/org/jetbrains/kotlin/idea/inspections/api/IncompatibleAPIKotlinVisitor.kt create mode 100644 idea/testData/inspections/incompatibleAPI/JavaTest.java create mode 100644 idea/testData/inspections/incompatibleAPI/classes.kt create mode 100644 idea/testData/inspections/incompatibleAPI/constructors.kt create mode 100644 idea/testData/inspections/incompatibleAPI/inspectionData/expected.xml create mode 100644 idea/testData/inspections/incompatibleAPI/inspectionData/inspections.test create mode 100644 idea/testData/inspections/incompatibleAPI/inspectionData/settings.xml create mode 100644 idea/testData/inspections/incompatibleAPI/lib/LibClass.java create mode 100644 idea/testData/inspections/incompatibleAPI/lib/LibConstructor.java create mode 100644 idea/testData/inspections/incompatibleAPI/lib/LibMethods.java create mode 100644 idea/testData/inspections/incompatibleAPI/lib/LibSuper.java create mode 100644 idea/testData/inspections/incompatibleAPI/overloads.kt create mode 100644 idea/testData/inspections/incompatibleAPI/static.kt create mode 100644 idea/testData/inspections/incompatibleAPI/super.kt diff --git a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/inspections/AbstractKotlinInspection.kt b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/inspections/AbstractKotlinInspection.kt index 40d14fc5cce..59d43d993ae 100644 --- a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/inspections/AbstractKotlinInspection.kt +++ b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/inspections/AbstractKotlinInspection.kt @@ -72,7 +72,7 @@ abstract class AbstractKotlinInspection: LocalInspectionTool(), CustomSuppressab } } -private fun toSeverity(highlightDisplayLevel: HighlightDisplayLevel): Severity { +fun toSeverity(highlightDisplayLevel: HighlightDisplayLevel): Severity { return when (highlightDisplayLevel) { HighlightDisplayLevel.DO_NOT_SHOW -> Severity.INFO diff --git a/idea/idea-jvm/resources/inspectionDescriptions/IncompatibleAPI.html b/idea/idea-jvm/resources/inspectionDescriptions/IncompatibleAPI.html new file mode 100644 index 00000000000..99aab83016d --- /dev/null +++ b/idea/idea-jvm/resources/inspectionDescriptions/IncompatibleAPI.html @@ -0,0 +1,5 @@ + + +This inspection reports usages of incompatible API. + + diff --git a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/inspections/api/AddIncompatibleApiAction.kt b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/inspections/api/AddIncompatibleApiAction.kt new file mode 100644 index 00000000000..c8ee27ee89a --- /dev/null +++ b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/inspections/api/AddIncompatibleApiAction.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.inspections.api + +import com.intellij.codeInsight.TargetElementUtil +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.editor.Caret +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.actionSystem.EditorAction +import com.intellij.openapi.editor.actionSystem.EditorActionHandler +import com.intellij.profile.codeInspection.ProjectInspectionProfileManager + +class AddIncompatibleApiAction : EditorAction(AddIncompatibleApiInspectionHandler()) + +private class AddIncompatibleApiInspectionHandler : EditorActionHandler() { + override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean { + if (!super.isEnabledForCaret(editor, caret, dataContext)) { + return false + } + + val project = editor.project ?: return false + val incompatibleAPIToolState = ProjectInspectionProfileManager.getInstance(project).currentProfile.getToolDefaultState( + IncompatibleAPIInspection.SHORT_NAME, project + ) + + return incompatibleAPIToolState.isEnabled + } + + override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { + val project = editor.project ?: return + val element = TargetElementUtil.findTargetElement( + editor, TargetElementUtil.ELEMENT_NAME_ACCEPTED or TargetElementUtil.REFERENCED_ELEMENT_ACCEPTED + ) ?: return + val qualified = getQualifiedNameFromProviders(element) ?: return + AddToIncompatibleApiDialog(project, qualified).show() + } +} \ No newline at end of file diff --git a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/inspections/api/AddToIncompatibleApiDialog.form b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/inspections/api/AddToIncompatibleApiDialog.form new file mode 100644 index 00000000000..b0cc73c13bb --- /dev/null +++ b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/inspections/api/AddToIncompatibleApiDialog.form @@ -0,0 +1,60 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/inspections/api/AddToIncompatibleApiDialog.java b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/inspections/api/AddToIncompatibleApiDialog.java new file mode 100644 index 00000000000..3e56596f011 --- /dev/null +++ b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/inspections/api/AddToIncompatibleApiDialog.java @@ -0,0 +1,81 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.inspections.api; + +import com.intellij.codeInspection.ex.InspectionProfileModifiableModelKt; +import com.intellij.codeInspection.ex.ToolsImpl; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.DialogWrapper; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; + +public class AddToIncompatibleApiDialog extends DialogWrapper { + private JPanel contentPane; + private JTextField referenceTextField; + private JTextField reasonTextField; + + private final Project project; + + public AddToIncompatibleApiDialog(@NotNull Project project, @NotNull String qualifiedReference) { + super(project, true); + this.project = project; + + setModal(true); + setTitle("Report as Incompatible API"); + referenceTextField.setText(qualifiedReference); + + init(); + } + + @Nullable + @Override + public JComponent getPreferredFocusedComponent() { + return reasonTextField; + } + + @Override + protected JComponent createCenterPanel() { + return contentPane; + } + + @Override + public Container getContentPane() { + return contentPane; + } + + @Override + protected void doOKAction() { + String reference = referenceTextField.getText(); + String reason = reasonTextField.getText(); + + reference = reference != null ? reference.trim() : null; + reason = reason != null ? reason.trim() : null; + + if (reference != null && !reference.isEmpty()) { + String finalReference = reference; + String finalReason = reason == null || reason.isEmpty() ? null : reason; + + InspectionProfileModifiableModelKt.modifyAndCommitProjectProfile(project, inspectionProfileModifiableModel -> { + ToolsImpl toolImpl = inspectionProfileModifiableModel.getToolsOrNull(IncompatibleAPIInspection.SHORT_NAME, project); + if (toolImpl != null) { + IncompatibleAPIInspection incompatibleAPIInspection = + (IncompatibleAPIInspection) toolImpl.getDefaultState().getTool().getTool(); + incompatibleAPIInspection.addProblem(finalReference, finalReason); + } + }); + } + + super.doOKAction(); + } + + @Override + public void dispose() { + super.dispose(); + } +} diff --git a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/inspections/api/IncompatibleAPIInspection.kt b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/inspections/api/IncompatibleAPIInspection.kt new file mode 100644 index 00000000000..5ed74d6e1c0 --- /dev/null +++ b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/inspections/api/IncompatibleAPIInspection.kt @@ -0,0 +1,201 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.inspections.api + +import com.intellij.codeInsight.daemon.HighlightDisplayKey +import com.intellij.codeInspection.* +import com.intellij.ide.actions.QualifiedNameProvider +import com.intellij.lang.java.JavaLanguage +import com.intellij.openapi.extensions.Extensions +import com.intellij.openapi.project.DumbService +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiElementVisitor +import com.intellij.psi.PsiReference +import com.intellij.util.xmlb.annotations.Attribute +import org.jdom.Element +import org.jetbrains.kotlin.caches.resolve.KotlinCacheService +import org.jetbrains.kotlin.idea.KotlinLanguage +import org.jetbrains.kotlin.idea.highlighter.createSuppressWarningActions +import org.jetbrains.kotlin.idea.inspections.api.IncompatibleAPIInspection.Companion.DEFAULT_REASON +import org.jetbrains.kotlin.idea.inspections.toSeverity + +class IncompatibleAPIInspection : LocalInspectionTool(), CustomSuppressableInspectionTool { + class Problem( + @Attribute var reference: String? = "", + @Attribute var reason: String? = "" + ) + + // Stored in inspection setting + var problems: List = arrayListOf() + + fun addProblem(reference: String, reason: String?) { + problems += Problem(reference, reason) + problemsCache.update(problems) + } + + override fun readSettings(node: Element) { + super.readSettings(node) + problemsCache.update(problems) + } + + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { + if (problems.isEmpty()) { + return super.buildVisitor(holder, isOnTheFly) + } + + return when (holder.file.language) { + KotlinLanguage.INSTANCE -> { + IncompatibleAPIKotlinVisitor(holder, problemsCache) + } + + JavaLanguage.INSTANCE -> { + IncompatibleAPIJavaVisitor(holder, problemsCache) + } + + else -> { + super.buildVisitor(holder, isOnTheFly) + } + } + } + + override fun getSuppressActions(element: PsiElement?): Array? { + return when { + element == null -> emptyArray() + + element.language == JavaLanguage.INSTANCE -> { + val key = HighlightDisplayKey.find(shortName) + ?: throw AssertionError("HighlightDisplayKey.find($shortName) is null. Inspection: $javaClass") + return SuppressManager.getInstance().createSuppressActions(key) + } + + element.language == KotlinLanguage.INSTANCE -> { + createSuppressWarningActions(element, toSeverity(defaultLevel), suppressionKey).toTypedArray() + } + + else -> emptyArray() + } + } + + override fun isSuppressedFor(element: PsiElement): Boolean { + if (SuppressManager.getInstance()!!.isSuppressedFor(element, id)) { + return true + } + + val project = element.project + if (KotlinCacheService.getInstance(project).getSuppressionCache().isSuppressed(element, suppressionKey, toSeverity(defaultLevel))) { + return true + } + + return false + } + + private val suppressionKey: String get() = this.shortName + private val problemsCache = ProblemsCache() + + companion object { + const val SHORT_NAME = "IncompatibleAPI" + const val DEFAULT_REASON = "Incompatible API" + } +} + +internal class ProblemsCache { + var forbiddenApiReferences: Map = emptyMap() + private set + + private var words: Set = emptySet() + + fun update(problems: List) { + val validProblems = problems.filter { !it.reference.isNullOrBlank() } + + forbiddenApiReferences = validProblems.map { it.reference!! to it }.toMap() + + words = validProblems.mapTo(HashSet()) { + val reference = it.reference!! + if (reference.contains('#')) { + reference.substringAfterLast('#').substringBefore('(') + } else { + reference.substringAfterLast('.') + } + } + } + + fun containsWord(word: String) = word in words +} + +internal fun registerProblemForReference( + reference: PsiReference?, + holder: ProblemsHolder, + problem: IncompatibleAPIInspection.Problem +) { + if (reference == null) return + holder.registerProblem( + reference, + problem.reason ?: DEFAULT_REASON, + ProblemHighlightType.GENERIC_ERROR_OR_WARNING + ) +} + +internal fun registerProblemForElement( + element: PsiElement?, + holder: ProblemsHolder, + problem: IncompatibleAPIInspection.Problem +) { + if (element == null) return + holder.registerProblem( + element, + problem.reason ?: DEFAULT_REASON, + ProblemHighlightType.GENERIC_ERROR_OR_WARNING + ) +} + +internal fun findProblem( + resolvedTo: PsiElement?, + problemsCache: ProblemsCache +): IncompatibleAPIInspection.Problem? { + if (resolvedTo == null) return null + val referenceStr = getQualifiedNameFromProviders(resolvedTo) ?: return null + + val forbiddenApiReferences = problemsCache.forbiddenApiReferences + + return forbiddenApiReferences[referenceStr] ?: run { + // check constructor + val lastPart = referenceStr.substringAfterLast('#', "") + if (lastPart.isNotEmpty()) { + val classFqName = referenceStr.substringBeforeLast('#') + val className = classFqName.substringAfterLast('.') + if (lastPart.startsWith("$className(")) { + return@run forbiddenApiReferences[classFqName] + } + } + + null + } ?: run { + // Check reference without params + val beforeParams = referenceStr.substringBeforeLast('(', "") + if (beforeParams.isNotEmpty()) { + val overloadSignatureKey = "$beforeParams(" + val particularOverloadPresent = forbiddenApiReferences.keys.any { it.startsWith(overloadSignatureKey) } + if (!particularOverloadPresent) { + return@run forbiddenApiReferences[beforeParams] + } + } + + null + } +} + +internal fun getQualifiedNameFromProviders(element: PsiElement): String? { + DumbService.getInstance(element.project).isAlternativeResolveEnabled = true + try { + for (provider in Extensions.getExtensions(QualifiedNameProvider.EP_NAME)) { + val result = provider.getQualifiedName(element) + if (result != null) return result + } + } finally { + DumbService.getInstance(element.project).isAlternativeResolveEnabled = false + } + return null +} \ No newline at end of file diff --git a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/inspections/api/IncompatibleAPIJavaVisitor.kt b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/inspections/api/IncompatibleAPIJavaVisitor.kt new file mode 100644 index 00000000000..c4787349e84 --- /dev/null +++ b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/inspections/api/IncompatibleAPIJavaVisitor.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.inspections.api + +import com.intellij.codeInsight.AnnotationUtil +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.module.ModuleUtilCore +import com.intellij.psi.* +import com.intellij.psi.javadoc.PsiDocComment +import org.jetbrains.kotlin.psi.psiUtil.parents + +class IncompatibleAPIJavaVisitor internal constructor( + private val myHolder: ProblemsHolder, + private val problemsCache: ProblemsCache +) : JavaElementVisitor() { + override fun visitDocComment(comment: PsiDocComment) { + // No references inside doc comment are of interest. + } + + override fun visitImportList(list: PsiImportList?) { + // Do not report anything in imports + } + + override fun visitClass(aClass: PsiClass) {} + + override fun visitReferenceExpression(expression: PsiReferenceExpression) { + visitReferenceElement(expression) + } + + override fun visitReferenceElement(reference: PsiJavaCodeReferenceElement) { + val isInsideImport = reference.element.parents + .takeWhile { it is PsiJavaCodeReferenceElement || it is PsiImportStatement } + .any { it is PsiImportStatement } + if (isInsideImport) { + return + } + + ModuleUtilCore.findModuleForPsiElement(reference.element) ?: return + super.visitReferenceElement(reference) + + val name = reference.referenceName + if (name == null || !problemsCache.containsWord(name)) { + return + } + + val psiMember = reference.resolve() as? PsiMember ?: return + val problem = findProblem(psiMember, problemsCache) ?: return + registerProblemForReference(reference, myHolder, problem) + } + + override fun visitNewExpression(expression: PsiNewExpression) { + ModuleUtilCore.findModuleForPsiElement(expression) ?: return + super.visitNewExpression(expression) + + val name = expression.classReference?.referenceName + if (name == null || !problemsCache.containsWord(name)) { + return + } + + val constructor = expression.resolveConstructor() + if (exitOnNonCompiled(constructor)) return + + val problem = findProblem(constructor, problemsCache) + if (problem != null) { + registerProblemForReference(expression.classReference, myHolder, problem) + } + } + + override fun visitMethod(method: PsiMethod) { + ModuleUtilCore.findModuleForPsiElement(method) ?: return + super.visitMethod(method) + + if (!problemsCache.containsWord(method.name)) { + return + } + + (if (!method.isConstructor) AnnotationUtil.findAnnotation(method, CommonClassNames.JAVA_LANG_OVERRIDE) else null) ?: return + + val methods = method.findSuperMethods() + for (superMethod in methods) { + if (exitOnNonCompiled(superMethod)) { + return + } + + val problem = findProblem(superMethod, problemsCache) + if (problem != null) { + registerProblemForElement(method.nameIdentifier, myHolder, problem) + return + } + } + } + + private fun exitOnNonCompiled(psiElement: PsiElement?): Boolean { + if (psiElement != null && psiElement !is PsiCompiledElement) { + if (!ApplicationManager.getApplication().isInternal && !ApplicationManager.getApplication().isUnitTestMode) { + return true + } + } + + return false + } +} \ No newline at end of file diff --git a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/inspections/api/IncompatibleAPIKotlinVisitor.kt b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/inspections/api/IncompatibleAPIKotlinVisitor.kt new file mode 100644 index 00000000000..78af8d9620c --- /dev/null +++ b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/inspections/api/IncompatibleAPIKotlinVisitor.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.inspections.api + +import com.intellij.codeInspection.ProblemsHolder +import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.isInImportDirective +import org.jetbrains.kotlin.resolve.source.getPsi +import org.jetbrains.kotlin.synthetic.JavaSyntheticPropertiesScope + +internal class IncompatibleAPIKotlinVisitor( + private val holder: ProblemsHolder, + private val problemsCache: ProblemsCache +) : KtVisitorVoid() { + override fun visitImportList(importList: KtImportList) { + // Do not report anything in imports + } + + override fun visitSimpleNameExpression(expression: KtSimpleNameExpression) { + val nameStr = expression.text + if (!Name.isValidIdentifier(nameStr)) { + return + } + + val names = HashSet() + names.add(nameStr) + + val gettersNames = JavaSyntheticPropertiesScope.possibleGetMethodNames( + Name.identifier(nameStr) + ) + if (!gettersNames.isEmpty()) { + names.addAll(gettersNames.map { it.identifier }) + names.add(JavaSyntheticPropertiesScope.setMethodName(gettersNames.first()).identifier) + } + + if (names.none { name -> problemsCache.containsWord(name) }) { + return + } + + checkReference(expression) + } + + override fun visitNamedFunction(function: KtNamedFunction) { + super.visitNamedFunction(function) + + if (function.modifierList?.getModifier(KtTokens.OVERRIDE_KEYWORD) == null) { + return + } + + val funName = function.name + if (funName == null || !problemsCache.containsWord(funName)) { + return + } + + val functionDescriptor = function.resolveToDescriptorIfAny() ?: return + for (overriddenDescriptor in functionDescriptor.original.overriddenDescriptors) { + val psi = overriddenDescriptor.source.getPsi() ?: continue + val problem = findProblem(psi, problemsCache) + if (problem != null) { + registerProblemForElement(function.nameIdentifier, holder, problem) + return + } + } + } + + private fun checkReference(expression: KtSimpleNameExpression) { + if (expression.isInImportDirective()) { + // Ignore imports + return + } + + for (reference in expression.references) { + val resolveTo = reference.resolve() + val problem = findProblem(resolveTo, problemsCache) ?: continue + + registerProblemForReference(reference, holder, problem) + break + } + } +} \ No newline at end of file diff --git a/idea/src/META-INF/jvm.xml b/idea/src/META-INF/jvm.xml index dbdaf80482f..750c29549aa 100644 --- a/idea/src/META-INF/jvm.xml +++ b/idea/src/META-INF/jvm.xml @@ -57,6 +57,10 @@ text="Kotlin Project Post-Open Activity" internal="true"> + + + @@ -118,6 +122,14 @@ + + diff --git a/idea/testData/inspections/incompatibleAPI/JavaTest.java b/idea/testData/inspections/incompatibleAPI/JavaTest.java new file mode 100644 index 00000000000..da64b4af870 --- /dev/null +++ b/idea/testData/inspections/incompatibleAPI/JavaTest.java @@ -0,0 +1,45 @@ +package problem.api; + +import lib.LibClass; +import lib.LibMethods; +import lib.LibSuper; +import lib.LibConstructor; + +public class JavaTest { + void test() { + new LibClass(); + LibMethods.staticMethod(); + } + + static void overloads(LibMethods lib) { + lib.overload1(12); + lib.overload1("Some"); + + lib.overload2(12); + lib.overload2("Some"); + + //noinspection IncompatibleAPI + lib.overload2(13); + } + + public static class Extends extends LibClass { + } + + public class Subclass extends LibSuper { + @Override + public void test(String str) { + } + } + + public class SubclassSuppress extends LibSuper { + @SuppressWarnings("IncompatibleAPI") + @Override + public void test(String str) { + } + } + + public static constructor() { + new LibConstructor(null, "some"); + new LibConstructor(null); + } +} diff --git a/idea/testData/inspections/incompatibleAPI/classes.kt b/idea/testData/inspections/incompatibleAPI/classes.kt new file mode 100644 index 00000000000..a3894b08e27 --- /dev/null +++ b/idea/testData/inspections/incompatibleAPI/classes.kt @@ -0,0 +1,17 @@ +package problem.api.kotlin.classes + +import lib.LibClass + +fun ktTest() { + LibClass() +} + +class KtUsage : LibClass() + +fun ktTestSuppress() { + @Suppress("IncompatibleAPI") + LibClass() +} + +@Suppress("IncompatibleAPI") +class KtUsageSuppress : LibClass() \ No newline at end of file diff --git a/idea/testData/inspections/incompatibleAPI/constructors.kt b/idea/testData/inspections/incompatibleAPI/constructors.kt new file mode 100644 index 00000000000..a65f9714ddb --- /dev/null +++ b/idea/testData/inspections/incompatibleAPI/constructors.kt @@ -0,0 +1,8 @@ +package problem.api.kotlin.constructors + +import lib.LibConstructor + +fun ktOverloads(lib: LibMethods) { + LibConstructor(null, "some") + LibConstructor(null) +} \ No newline at end of file diff --git a/idea/testData/inspections/incompatibleAPI/inspectionData/expected.xml b/idea/testData/inspections/incompatibleAPI/inspectionData/expected.xml new file mode 100644 index 00000000000..34bb4f2eff4 --- /dev/null +++ b/idea/testData/inspections/incompatibleAPI/inspectionData/expected.xml @@ -0,0 +1,87 @@ + + + classes.kt + 6 + class example + + + classes.kt + 9 + class example + + + + overloads.kt + 6 + concrete overload example + + + overloads.kt + 9 + overload with no params example + + + overloads.kt + 10 + overload with no params example + + + + static.kt + 6 + static method example + + + + JavaTest.java + 10 + class example + + + JavaTest.java + 11 + static method example + + + JavaTest.java + 15 + concrete overload example + + + JavaTest.java + 18 + overload with no params example + + + JavaTest.java + 19 + overload with no params example + + + JavaTest.java + 25 + class example + + + JavaTest.java + 30 + signature changed example + + + JavaTest.java + 43 + constructor example + + + + super.kt + 6 + signature changed example + + + + constructors.kt + 7 + constructor example + + diff --git a/idea/testData/inspections/incompatibleAPI/inspectionData/inspections.test b/idea/testData/inspections/incompatibleAPI/inspectionData/inspections.test new file mode 100644 index 00000000000..5a796ea58b1 --- /dev/null +++ b/idea/testData/inspections/incompatibleAPI/inspectionData/inspections.test @@ -0,0 +1 @@ +// INSPECTION_CLASS: org.jetbrains.kotlin.idea.inspections.api.IncompatibleAPIInspection \ No newline at end of file diff --git a/idea/testData/inspections/incompatibleAPI/inspectionData/settings.xml b/idea/testData/inspections/incompatibleAPI/inspectionData/settings.xml new file mode 100644 index 00000000000..b6a82b110b9 --- /dev/null +++ b/idea/testData/inspections/incompatibleAPI/inspectionData/settings.xml @@ -0,0 +1,12 @@ + + + \ No newline at end of file diff --git a/idea/testData/inspections/incompatibleAPI/lib/LibClass.java b/idea/testData/inspections/incompatibleAPI/lib/LibClass.java new file mode 100644 index 00000000000..6d5c492b18d --- /dev/null +++ b/idea/testData/inspections/incompatibleAPI/lib/LibClass.java @@ -0,0 +1,4 @@ +package lib; + +public class LibClass { +} \ No newline at end of file diff --git a/idea/testData/inspections/incompatibleAPI/lib/LibConstructor.java b/idea/testData/inspections/incompatibleAPI/lib/LibConstructor.java new file mode 100644 index 00000000000..3e7911891dc --- /dev/null +++ b/idea/testData/inspections/incompatibleAPI/lib/LibConstructor.java @@ -0,0 +1,9 @@ +package lib; + +public class LibConstructor { + public LibConstructor(Object object) { + } + + public LibConstructor(Object object, String other) { + } +} diff --git a/idea/testData/inspections/incompatibleAPI/lib/LibMethods.java b/idea/testData/inspections/incompatibleAPI/lib/LibMethods.java new file mode 100644 index 00000000000..48b9af33999 --- /dev/null +++ b/idea/testData/inspections/incompatibleAPI/lib/LibMethods.java @@ -0,0 +1,12 @@ +package lib; + +public class LibMethods { + public static void staticMethod() { + } + + public void overload1(int i) {} + public void overload1(String s) {} + + public void overload2(int i) {} + public void overload2(String s) {} +} diff --git a/idea/testData/inspections/incompatibleAPI/lib/LibSuper.java b/idea/testData/inspections/incompatibleAPI/lib/LibSuper.java new file mode 100644 index 00000000000..747845497cd --- /dev/null +++ b/idea/testData/inspections/incompatibleAPI/lib/LibSuper.java @@ -0,0 +1,7 @@ +package lib; + +import org.jetbrains.annotations.Nullable; + +public class LibSuper { + public void test(@Nullable String str) {} +} diff --git a/idea/testData/inspections/incompatibleAPI/overloads.kt b/idea/testData/inspections/incompatibleAPI/overloads.kt new file mode 100644 index 00000000000..22795600b21 --- /dev/null +++ b/idea/testData/inspections/incompatibleAPI/overloads.kt @@ -0,0 +1,11 @@ +package problem.api.kotlin.overloads + +import lib.LibMethods + +fun ktOverloads(lib: LibMethods) { + lib.overload1(12) + lib.overload1("Some") + + lib.overload2(12) + lib.overload2("Some") +} \ No newline at end of file diff --git a/idea/testData/inspections/incompatibleAPI/static.kt b/idea/testData/inspections/incompatibleAPI/static.kt new file mode 100644 index 00000000000..ecd88e8c10b --- /dev/null +++ b/idea/testData/inspections/incompatibleAPI/static.kt @@ -0,0 +1,7 @@ +package problem.api.kotlin.statics + +import lib.LibMethods + +fun ktTest() { + LibMethods.staticMethod() +} \ No newline at end of file diff --git a/idea/testData/inspections/incompatibleAPI/super.kt b/idea/testData/inspections/incompatibleAPI/super.kt new file mode 100644 index 00000000000..40e619d9366 --- /dev/null +++ b/idea/testData/inspections/incompatibleAPI/super.kt @@ -0,0 +1,14 @@ +package problem.api.kotlin.s + +import lib.LibSuper + +class Subclass : LibSuper() { + override fun test(str: String?) { + } +} + +class SubclassSuppress : LibSuper() { + @Suppress("IncompatibleAPI") + override fun test(str: String?) { + } +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/AbstractInspectionTest.kt b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/AbstractInspectionTest.kt index acd662294d2..db06949b519 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/AbstractInspectionTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/AbstractInspectionTest.kt @@ -21,6 +21,8 @@ import com.intellij.openapi.command.CommandProcessor import com.intellij.openapi.util.io.FileUtil import com.intellij.psi.PsiFile import com.intellij.testFramework.TestLoggerFactory +import org.jdom.Document +import org.jdom.input.SAXBuilder import org.jetbrains.kotlin.idea.inspections.runInspection import org.jetbrains.kotlin.idea.test.* import org.jetbrains.kotlin.idea.util.application.runWriteAction @@ -66,6 +68,13 @@ abstract class AbstractInspectionTest : KotlinLightCodeInsightFixtureTestCase() val inspectionsTestDir = optionsFile.parentFile!! val srcDir = inspectionsTestDir.parentFile!! + val settingsFile = File(inspectionsTestDir, "settings.xml") + val settingsElement = if (settingsFile.exists()) { + (SAXBuilder().build(settingsFile) as Document).rootElement + } else { + null + } + with(myFixture) { testDataPath = "${KotlinTestUtils.getHomeDirectory()}/$srcDir" @@ -110,7 +119,9 @@ abstract class AbstractInspectionTest : KotlinLightCodeInsightFixtureTestCase() configExtra(psiFiles, options) val presentation = runInspection( - inspectionClass, project, files = psiFiles.map { it.virtualFile!! }, withTestDir = inspectionsTestDir.path + inspectionClass, project, + settings = settingsElement, + files = psiFiles.map { it.virtualFile!! }, withTestDir = inspectionsTestDir.path ) if (afterFiles.isNotEmpty()) { diff --git a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InspectionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InspectionTestGenerated.java index 654087e1bca..c38f2fb3905 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InspectionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InspectionTestGenerated.java @@ -174,6 +174,11 @@ public class InspectionTestGenerated extends AbstractInspectionTest { runTest("idea/testData/inspections/hasPlatformType/inspectionData/inspections.test"); } + @TestMetadata("incompatibleAPI/inspectionData/inspections.test") + public void testIncompatibleAPI_inspectionData_Inspections_test() throws Exception { + runTest("idea/testData/inspections/incompatibleAPI/inspectionData/inspections.test"); + } + @TestMetadata("javaCollectionsStaticMethodOnImmutableList/inspectionData/inspections.test") public void testJavaCollectionsStaticMethodOnImmutableList_inspectionData_Inspections_test() throws Exception { runTest("idea/testData/inspections/javaCollectionsStaticMethodOnImmutableList/inspectionData/inspections.test"); diff --git a/idea/tests/org/jetbrains/kotlin/idea/inspections/InspectionUtils.kt b/idea/tests/org/jetbrains/kotlin/idea/inspections/InspectionUtils.kt index 08b44405e02..bd468b3f97c 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/inspections/InspectionUtils.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/inspections/InspectionUtils.kt @@ -24,6 +24,7 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile import com.intellij.testFramework.InspectionTestUtil import com.intellij.testFramework.createGlobalContextForTool +import org.jdom.Element fun runInspection( inspection: LocalInspectionTool, project: Project, files: List? = null, withTestDir: String? = null @@ -46,8 +47,12 @@ fun runInspection( } fun runInspection( - inspectionClass: Class<*>, project: Project, files: List? = null, withTestDir: String? = null + inspectionClass: Class<*>, project: Project, + settings: Element? = null, files: List? = null, withTestDir: String? = null ): InspectionToolPresentation { val inspection = inspectionClass.newInstance() as LocalInspectionTool + if (settings != null) { + inspection.readSettings(settings) + } return runInspection(inspection, project, files, withTestDir) }