Inspection for highlighting problem API usage
It's planned to use this inspection for show patchset branches problems during development process.
This commit is contained in:
committed by
Nikolay Krasko
parent
a95ec598f2
commit
3ef67e1d9d
+1
-1
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<html>
|
||||
<body>
|
||||
This inspection reports usages of incompatible API.
|
||||
</body>
|
||||
</html>
|
||||
+40
@@ -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()
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="org.jetbrains.kotlin.idea.inspections.api.AddToIncompatibleApiDialog">
|
||||
<grid id="cbd77" binding="contentPane" layout-manager="GridLayoutManager" row-count="1" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
|
||||
<margin top="10" left="10" bottom="10" right="10"/>
|
||||
<constraints>
|
||||
<xy x="48" y="54" width="436" height="297"/>
|
||||
</constraints>
|
||||
<properties/>
|
||||
<border type="none"/>
|
||||
<children>
|
||||
<grid id="e3588" layout-manager="GridLayoutManager" row-count="3" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
|
||||
<margin top="0" left="0" bottom="0" right="0"/>
|
||||
<constraints>
|
||||
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="6" anchor="0" fill="3" indent="0" use-parent-layout="false">
|
||||
<minimum-size width="600" height="-1"/>
|
||||
</grid>
|
||||
</constraints>
|
||||
<properties/>
|
||||
<border type="none"/>
|
||||
<children>
|
||||
<component id="35cae" class="javax.swing.JLabel">
|
||||
<constraints>
|
||||
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
|
||||
</constraints>
|
||||
<properties>
|
||||
<labelFor value="49f89"/>
|
||||
<text value="Re&ference"/>
|
||||
</properties>
|
||||
</component>
|
||||
<vspacer id="eac19">
|
||||
<constraints>
|
||||
<grid row="2" column="0" row-span="1" col-span="2" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
|
||||
</constraints>
|
||||
</vspacer>
|
||||
<component id="49f89" class="javax.swing.JTextField" binding="referenceTextField">
|
||||
<constraints>
|
||||
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false"/>
|
||||
</constraints>
|
||||
<properties/>
|
||||
</component>
|
||||
<component id="bcdcc" class="javax.swing.JLabel">
|
||||
<constraints>
|
||||
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
|
||||
</constraints>
|
||||
<properties>
|
||||
<labelFor value="ec77c"/>
|
||||
<text value="&Reason"/>
|
||||
</properties>
|
||||
</component>
|
||||
<component id="ec77c" class="javax.swing.JTextField" binding="reasonTextField">
|
||||
<constraints>
|
||||
<grid row="1" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false"/>
|
||||
</constraints>
|
||||
<properties/>
|
||||
</component>
|
||||
</children>
|
||||
</grid>
|
||||
</children>
|
||||
</grid>
|
||||
</form>
|
||||
+81
@@ -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();
|
||||
}
|
||||
}
|
||||
+201
@@ -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<Problem> = 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<SuppressIntentionAction>? {
|
||||
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<String, IncompatibleAPIInspection.Problem> = emptyMap()
|
||||
private set
|
||||
|
||||
private var words: Set<String> = emptySet()
|
||||
|
||||
fun update(problems: List<IncompatibleAPIInspection.Problem>) {
|
||||
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
|
||||
}
|
||||
+106
@@ -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
|
||||
}
|
||||
}
|
||||
+86
@@ -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<String>()
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,10 @@
|
||||
text="Kotlin Project Post-Open Activity" internal="true">
|
||||
<add-to-group group-id="KotlinInternalGroup"/>
|
||||
</action>
|
||||
|
||||
<action id="AddToProblemApiInspection" class="org.jetbrains.kotlin.idea.inspections.api.AddIncompatibleApiAction"
|
||||
text="Report as incompatible API">
|
||||
</action>
|
||||
</actions>
|
||||
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
@@ -118,6 +122,14 @@
|
||||
|
||||
<editorNotificationProvider implementation="org.jetbrains.kotlin.idea.versions.UnsupportedAbiVersionNotificationPanelProvider"/>
|
||||
|
||||
<localInspection
|
||||
groupName="Plugin DevKit"
|
||||
shortName="IncompatibleAPI"
|
||||
enabledByDefault="false"
|
||||
level="ERROR"
|
||||
displayName="Incompatible API usage"
|
||||
implementationClass="org.jetbrains.kotlin.idea.inspections.api.IncompatibleAPIInspection"/>
|
||||
|
||||
<projectService serviceInterface="org.jetbrains.uast.kotlin.KotlinUastBindingContextProviderService"
|
||||
serviceImplementation="org.jetbrains.uast.kotlin.internal.IdeaKotlinUastBindingContextProviderService"/>
|
||||
</extensions>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -0,0 +1,8 @@
|
||||
package problem.api.kotlin.constructors
|
||||
|
||||
import lib.LibConstructor
|
||||
|
||||
fun ktOverloads(lib: LibMethods) {
|
||||
LibConstructor(null, "some")
|
||||
LibConstructor(null)
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<problems>
|
||||
<problem>
|
||||
<file>classes.kt</file>
|
||||
<line>6</line>
|
||||
<description>class example</description>
|
||||
</problem>
|
||||
<problem>
|
||||
<file>classes.kt</file>
|
||||
<line>9</line>
|
||||
<description>class example</description>
|
||||
</problem>
|
||||
|
||||
<problem>
|
||||
<file>overloads.kt</file>
|
||||
<line>6</line>
|
||||
<description>concrete overload example</description>
|
||||
</problem>
|
||||
<problem>
|
||||
<file>overloads.kt</file>
|
||||
<line>9</line>
|
||||
<description>overload with no params example</description>
|
||||
</problem>
|
||||
<problem>
|
||||
<file>overloads.kt</file>
|
||||
<line>10</line>
|
||||
<description>overload with no params example</description>
|
||||
</problem>
|
||||
|
||||
<problem>
|
||||
<file>static.kt</file>
|
||||
<line>6</line>
|
||||
<description>static method example</description>
|
||||
</problem>
|
||||
|
||||
<problem>
|
||||
<file>JavaTest.java</file>
|
||||
<line>10</line>
|
||||
<description>class example</description>
|
||||
</problem>
|
||||
<problem>
|
||||
<file>JavaTest.java</file>
|
||||
<line>11</line>
|
||||
<description>static method example</description>
|
||||
</problem>
|
||||
<problem>
|
||||
<file>JavaTest.java</file>
|
||||
<line>15</line>
|
||||
<description>concrete overload example</description>
|
||||
</problem>
|
||||
<problem>
|
||||
<file>JavaTest.java</file>
|
||||
<line>18</line>
|
||||
<description>overload with no params example</description>
|
||||
</problem>
|
||||
<problem>
|
||||
<file>JavaTest.java</file>
|
||||
<line>19</line>
|
||||
<description>overload with no params example</description>
|
||||
</problem>
|
||||
<problem>
|
||||
<file>JavaTest.java</file>
|
||||
<line>25</line>
|
||||
<description>class example</description>
|
||||
</problem>
|
||||
<problem>
|
||||
<file>JavaTest.java</file>
|
||||
<line>30</line>
|
||||
<description>signature changed example</description>
|
||||
</problem>
|
||||
<problem>
|
||||
<file>JavaTest.java</file>
|
||||
<line>43</line>
|
||||
<description>constructor example</description>
|
||||
</problem>
|
||||
|
||||
<problem>
|
||||
<file>super.kt</file>
|
||||
<line>6</line>
|
||||
<description>signature changed example</description>
|
||||
</problem>
|
||||
|
||||
<problem>
|
||||
<file>constructors.kt</file>
|
||||
<line>7</line>
|
||||
<description>constructor example</description>
|
||||
</problem>
|
||||
</problems>
|
||||
@@ -0,0 +1 @@
|
||||
// INSPECTION_CLASS: org.jetbrains.kotlin.idea.inspections.api.IncompatibleAPIInspection
|
||||
@@ -0,0 +1,12 @@
|
||||
<inspection_tool class="IncompatibleAPI" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="problems">
|
||||
<list>
|
||||
<Problem reference="lib.LibClass" reason="class example" />
|
||||
<Problem reference="lib.LibMethods#staticMethod" reason="static method example" />
|
||||
<Problem reference="lib.LibMethods#overload1(int)" reason="concrete overload example" />
|
||||
<Problem reference="lib.LibMethods#overload2" reason="overload with no params example" />
|
||||
<Problem reference="lib.LibSuper#test" reason="signature changed example" />
|
||||
<Problem reference="lib.LibConstructor#LibConstructor(java.lang.Object)" reason="constructor example" />
|
||||
</list>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
@@ -0,0 +1,4 @@
|
||||
package lib;
|
||||
|
||||
public class LibClass {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package lib;
|
||||
|
||||
public class LibConstructor {
|
||||
public LibConstructor(Object object) {
|
||||
}
|
||||
|
||||
public LibConstructor(Object object, String other) {
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package lib;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class LibSuper {
|
||||
public void test(@Nullable String str) {}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package problem.api.kotlin.statics
|
||||
|
||||
import lib.LibMethods
|
||||
|
||||
fun ktTest() {
|
||||
LibMethods.staticMethod()
|
||||
}
|
||||
@@ -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?) {
|
||||
}
|
||||
}
|
||||
@@ -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()) {
|
||||
|
||||
+5
@@ -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");
|
||||
|
||||
@@ -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<VirtualFile>? = null, withTestDir: String? = null
|
||||
@@ -46,8 +47,12 @@ fun runInspection(
|
||||
}
|
||||
|
||||
fun runInspection(
|
||||
inspectionClass: Class<*>, project: Project, files: List<VirtualFile>? = null, withTestDir: String? = null
|
||||
inspectionClass: Class<*>, project: Project,
|
||||
settings: Element? = null, files: List<VirtualFile>? = null, withTestDir: String? = null
|
||||
): InspectionToolPresentation {
|
||||
val inspection = inspectionClass.newInstance() as LocalInspectionTool
|
||||
if (settings != null) {
|
||||
inspection.readSettings(settings)
|
||||
}
|
||||
return runInspection(inspection, project, files, withTestDir)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user