Introduce inspection that checks stub packages compatibility (PY-31244)

This commit is contained in:
Semyon Proshev
2018-10-11 18:02:19 +03:00
parent 9886b0f741
commit 8fb784c13c
3 changed files with 93 additions and 0 deletions
@@ -0,0 +1,8 @@
<html>
<body>
Checks that a stub package supports the version of the corresponding runtime package.
<p>
Stub package is a package that contains type information for some runtime package.
See <a href="https://www.python.org/dev/peps/pep-0561/">PEP 561</a> for more details.
</body>
</html>
@@ -434,6 +434,7 @@
<localInspection language="Python" shortName="PyTypeHintsInspection" suppressId="PyTypeHints" displayName="Type hints definitions and usages" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.inspections.PyTypeHintsInspection"/>
<localInspection language="Python" shortName="PyAsyncCallInspection" suppressId="PyAsyncCall" displayName="Coroutine is not awaited" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.inspections.PyAsyncCallInspection"/>
<localInspection language="Python" shortName="PyStubPackagesAdvertiser" suppressId="PyStubPackagesAdvertiser" displayName="Stub packages advertiser" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.codeInsight.typing.PyStubPackagesAdvertiser"/>
<localInspection language="Python" shortName="PyStubPackagesCompatibilityInspection" suppressId="PyStubPackagesCompatibility" displayName="Stub packages compatibility inspection" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.codeInsight.typing.PyStubPackagesCompatibilityInspection"/>
<defaultLiveTemplatesProvider implementation="com.jetbrains.python.codeInsight.liveTemplates.PyDefaultLiveTemplatesProvider"/>
<liveTemplateContext implementation="com.jetbrains.python.codeInsight.liveTemplates.PythonTemplateContextType$General"/>
@@ -0,0 +1,84 @@
// Copyright 2000-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 file.
package com.jetbrains.python.codeInsight.typing
import com.intellij.codeInspection.LocalInspectionToolSession
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.codeInspection.ui.ListEditForm
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.text.StringUtil
import com.intellij.profile.codeInspection.ProjectInspectionProfileManager
import com.intellij.psi.PsiElementVisitor
import com.jetbrains.python.inspections.PyInspection
import com.jetbrains.python.inspections.PyInspectionVisitor
import com.jetbrains.python.inspections.PyInterpreterInspection
import com.jetbrains.python.packaging.PyPackage
import com.jetbrains.python.packaging.PyPackageManager
import com.jetbrains.python.packaging.requirement.PyRequirementRelation
import com.jetbrains.python.psi.PyFile
import com.jetbrains.python.sdk.PythonSdkType
import javax.swing.JComponent
class PyStubPackagesCompatibilityInspection : PyInspection() {
@Suppress("MemberVisibilityCanBePrivate")
var ignoredStubPackages: MutableList<String> = mutableListOf()
override fun createOptionsPanel(): JComponent = ListEditForm("Ignored stub packages", ignoredStubPackages).contentPanel
override fun buildVisitor(holder: ProblemsHolder,
isOnTheFly: Boolean,
session: LocalInspectionToolSession): PsiElementVisitor {
return Visitor(ignoredStubPackages, holder, session)
}
private class Visitor(val ignoredStubPackages: MutableList<String>,
holder: ProblemsHolder,
session: LocalInspectionToolSession) : PyInspectionVisitor(holder, session) {
override fun visitPyFile(node: PyFile) {
val module = ModuleUtilCore.findModuleForFile(node) ?: return
val sdk = PythonSdkType.findPythonSdk(module) ?: return
val installedPackages = PyPackageManager.getInstance(sdk).packages ?: emptyList()
if (installedPackages.isEmpty()) return
val nameToPkg = mutableMapOf<String, PyPackage>()
installedPackages.forEach { nameToPkg[it.name] = it }
installedPackages
.asSequence()
.filter { pkg -> pkg.name.let { it.endsWith(STUBS_SUFFIX) && it !in ignoredStubPackages } }
.mapNotNull { stubPkg -> nameToPkg[stubPkg.name.removeSuffix(STUBS_SUFFIX)]?.let { it to stubPkg } }
.forEach { (runtimePkg, stubPkg) ->
val runtimePkgName = runtimePkg.name
val requirement = stubPkg.requirements.firstOrNull { it.name == runtimePkgName } ?: return@forEach
if (requirement.match(listOf(runtimePkg)) == null) {
val stubPkgName = stubPkg.name
val specsToString = StringUtil.join(requirement.versionSpecs, { it.presentableText }, ", ")
registerProblem(node,
"'$stubPkgName${PyRequirementRelation.EQ.presentableText}${stubPkg.version}' " +
"is incompatible with " +
"'$runtimePkgName${PyRequirementRelation.EQ.presentableText}${runtimePkg.version}'. " +
"Expected '$runtimePkgName' version: [$specsToString]",
PyInterpreterInspection.ConfigureInterpreterFix(),
createIgnoreStubPackageQuickFix(stubPkgName, ignoredStubPackages))
}
}
}
private fun createIgnoreStubPackageQuickFix(stubPkgName: String, ignoredStubPkgs: MutableList<String>): LocalQuickFix {
return object : LocalQuickFix {
override fun getFamilyName() = "Ignore '$stubPkgName' compatibility"
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
if (ignoredStubPkgs.add(stubPkgName)) ProjectInspectionProfileManager.getInstance(project).fireProfileChanged()
}
}
}
}
}