From 352aa2d670ac1afa49631e052fe3acec3eb48e0c Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Thu, 12 Dec 2019 23:23:05 +0900 Subject: [PATCH] inspect untyped fields (#93) * inspect untyped fields --- resources/META-INF/plugin.xml | 7 +++- .../PydanticInspection.html | 5 +++ src/com/koxudaxi/pydantic/Pydantic.kt | 32 +++++++++++-------- .../pydantic/PydanticCompletionContributor.kt | 6 ++-- .../koxudaxi/pydantic/PydanticInspection.kt | 32 ++++++++++++++++--- .../koxudaxi/pydantic/PydanticTypeProvider.kt | 2 +- testData/inspection/warnUntypedFields.py | 21 ++++++++++++ .../inspection/warnUntypedFieldsDisable.py | 22 +++++++++++++ .../pydantic/PydanticInspectionTest.kt | 13 ++++++++ 9 files changed, 116 insertions(+), 24 deletions(-) create mode 100644 resources/inspectionDescriptions/PydanticInspection.html create mode 100644 testData/inspection/warnUntypedFields.py create mode 100644 testData/inspection/warnUntypedFieldsDisable.py diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml index 7993cce5..efa0690a 100644 --- a/resources/META-INF/plugin.xml +++ b/resources/META-INF/plugin.xml @@ -1,9 +1,14 @@ com.koxudaxi.pydantic Pydantic - 0.0.28 + 0.0.29 Koudai Aono @koxudaxi version 0.0.29 +

Features, BugFixes

+
    +
  • Inspect untyped fields [#93]
  • +

version 0.0.28

Features, BugFixes

    diff --git a/resources/inspectionDescriptions/PydanticInspection.html b/resources/inspectionDescriptions/PydanticInspection.html new file mode 100644 index 00000000..65c6a08d --- /dev/null +++ b/resources/inspectionDescriptions/PydanticInspection.html @@ -0,0 +1,5 @@ + + +This inspection checks Pydantic models. + + \ No newline at end of file diff --git a/src/com/koxudaxi/pydantic/Pydantic.kt b/src/com/koxudaxi/pydantic/Pydantic.kt index 92cacd22..1c928de8 100644 --- a/src/com/koxudaxi/pydantic/Pydantic.kt +++ b/src/com/koxudaxi/pydantic/Pydantic.kt @@ -55,7 +55,7 @@ val CONFIG_TYPES = mapOf( "allow_mutation" to Boolean ) -internal fun getPyClassByPyCallExpression(pyCallExpression: PyCallExpression, context: TypeEvalContext): PyClass? { +fun getPyClassByPyCallExpression(pyCallExpression: PyCallExpression, context: TypeEvalContext): PyClass? { val callee = pyCallExpression.callee ?: return null val pyType = when (val type = context.getType(callee)) { is PyClass -> return type @@ -65,16 +65,16 @@ internal fun getPyClassByPyCallExpression(pyCallExpression: PyCallExpression, co return getPyClassTypeByPyTypes(pyType).firstOrNull { isPydanticModel(it.pyClass) }?.pyClass } -internal fun getPyClassByPyKeywordArgument(pyKeywordArgument: PyKeywordArgument, context: TypeEvalContext): PyClass? { +fun getPyClassByPyKeywordArgument(pyKeywordArgument: PyKeywordArgument, context: TypeEvalContext): PyClass? { val pyCallExpression = PsiTreeUtil.getParentOfType(pyKeywordArgument, PyCallExpression::class.java) ?: return null return getPyClassByPyCallExpression(pyCallExpression, context) } -internal fun isPydanticModel(pyClass: PyClass, context: TypeEvalContext? = null): Boolean { +fun isPydanticModel(pyClass: PyClass, context: TypeEvalContext? = null): Boolean { return (isSubClassOfPydanticBaseModel(pyClass, context) || isPydanticDataclass(pyClass)) && !isPydanticBaseModel(pyClass) } -internal fun isPydanticBaseModel(pyClass: PyClass): Boolean { +fun isPydanticBaseModel(pyClass: PyClass): Boolean { return pyClass.qualifiedName == BASE_MODEL_Q_NAME } @@ -154,13 +154,13 @@ private fun getAliasedFieldName(field: PyTargetExpression, context: TypeEvalCont } -internal fun getResolveElements(referenceExpression: PyReferenceExpression, context: TypeEvalContext): Array { +fun getResolveElements(referenceExpression: PyReferenceExpression, context: TypeEvalContext): Array { val resolveContext = PyResolveContext.noImplicits().withTypeEvalContext(context) return referenceExpression.getReference(resolveContext).multiResolve(false) } -internal fun getPyClassTypeByPyTypes(pyType: PyType): List { +fun getPyClassTypeByPyTypes(pyType: PyType): List { return when (pyType) { is PyUnionType -> pyType.members @@ -174,13 +174,13 @@ internal fun getPyClassTypeByPyTypes(pyType: PyType): List { } -internal fun isPydanticSchemaByPsiElement(psiElement: PsiElement, context: TypeEvalContext): Boolean { +fun isPydanticSchemaByPsiElement(psiElement: PsiElement, context: TypeEvalContext): Boolean { PsiTreeUtil.getContextOfType(psiElement, PyClass::class.java) ?.let { return isPydanticSchema(it, context) } return false } -internal fun isPydanticFieldByPsiElement(psiElement: PsiElement): Boolean { +fun isPydanticFieldByPsiElement(psiElement: PsiElement): Boolean { when (psiElement) { is PyFunction -> return isPydanticField(psiElement) else -> PsiTreeUtil.getContextOfType(psiElement, PyFunction::class.java) @@ -189,7 +189,7 @@ internal fun isPydanticFieldByPsiElement(psiElement: PsiElement): Boolean { return false } -internal fun getPydanticVersion(project: Project, context: TypeEvalContext): KotlinVersion? { +fun getPydanticVersion(project: Project, context: TypeEvalContext): KotlinVersion? { val module = project.modules.firstOrNull() ?: return null val pythonSdk = module.pythonSdk val contextAnchor = ModuleBasedContextAnchor(module) @@ -210,11 +210,11 @@ internal fun getPydanticVersion(project: Project, context: TypeEvalContext): Kot }) } -internal fun isValidFieldName(name: String): Boolean { +fun isValidFieldName(name: String): Boolean { return name.first() != '_' } -internal fun getConfigValue(name: String, value: Any?, context: TypeEvalContext): Any? { +fun getConfigValue(name: String, value: Any?, context: TypeEvalContext): Any? { if (value is PyReferenceExpression) { val resolveResults = getResolveElements(value, context) val targetExpression = PyUtil.filterTopPriorityResults(resolveResults).firstOrNull() ?: return null @@ -232,7 +232,7 @@ internal fun getConfigValue(name: String, value: Any?, context: TypeEvalContext) return null } -internal fun getConfig(pyClass: PyClass, context: TypeEvalContext, setDefault: Boolean): HashMap { +fun getConfig(pyClass: PyClass, context: TypeEvalContext, setDefault: Boolean): HashMap { val config = hashMapOf() pyClass.getAncestorClasses(context) .reversed() @@ -265,7 +265,7 @@ internal fun getConfig(pyClass: PyClass, context: TypeEvalContext, setDefault: B return config } -internal fun getFieldName(field: PyTargetExpression, +fun getFieldName(field: PyTargetExpression, context: TypeEvalContext, config: HashMap, pydanticVersion: KotlinVersion?): String? { @@ -286,9 +286,13 @@ internal fun getFieldName(field: PyTargetExpression, } -internal fun getPydanticBaseConfig(project: Project, context: TypeEvalContext): PyClass? { +fun getPydanticBaseConfig(project: Project, context: TypeEvalContext): PyClass? { val module = project.modules.firstOrNull() ?: return null val pythonSdk = module.pythonSdk val contextAnchor = ModuleBasedContextAnchor(module) return BASE_CONFIG_QUALIFIED_NAME.resolveToElement(QNameResolveContext(contextAnchor, pythonSdk, context)) as? PyClass +} + +fun getPyClassByAttribute(pyPsiElement: PsiElement?): PyClass? { + return pyPsiElement?.parent?.parent as? PyClass } \ No newline at end of file diff --git a/src/com/koxudaxi/pydantic/PydanticCompletionContributor.kt b/src/com/koxudaxi/pydantic/PydanticCompletionContributor.kt index 954be2a8..db1f63b9 100644 --- a/src/com/koxudaxi/pydantic/PydanticCompletionContributor.kt +++ b/src/com/koxudaxi/pydantic/PydanticCompletionContributor.kt @@ -240,9 +240,9 @@ class PydanticCompletionContributor : CompletionContributor() { override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) { - val configClass = parameters.position.parent?.parent?.parent?.parent as? PyClass ?: return + val configClass = getPyClassByAttribute(parameters.position.parent?.parent) ?: return if (!isConfigClass(configClass)) return - val pydanticModel = configClass.parent?.parent as? PyClass ?:return + val pydanticModel = getPyClassByAttribute(configClass) ?:return if (!isPydanticModel(pydanticModel)) return val typeEvalContext = parameters.getTypeEvalContext() @@ -264,7 +264,7 @@ class PydanticCompletionContributor : CompletionContributor() { override val icon: Icon = AllIcons.Nodes.Class override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) { - val pydanticModel = parameters.position.parent?.parent?.parent?.parent as? PyClass ?: return + val pydanticModel = getPyClassByAttribute(parameters.position.parent?.parent) ?: return if (!isPydanticModel(pydanticModel)) return if (pydanticModel.findNestedClass("Config", false) != null) return val element = PrioritizedLookupElement.withGrouping( diff --git a/src/com/koxudaxi/pydantic/PydanticInspection.kt b/src/com/koxudaxi/pydantic/PydanticInspection.kt index 620cc7fd..ab461e55 100644 --- a/src/com/koxudaxi/pydantic/PydanticInspection.kt +++ b/src/com/koxudaxi/pydantic/PydanticInspection.kt @@ -3,6 +3,7 @@ package com.koxudaxi.pydantic import com.intellij.codeInspection.LocalInspectionToolSession import com.intellij.codeInspection.ProblemHighlightType import com.intellij.codeInspection.ProblemsHolder +import com.intellij.codeInspection.ui.MultipleCheckboxOptionsPanel import com.intellij.psi.PsiElementVisitor import com.jetbrains.python.PyBundle import com.jetbrains.python.PyNames @@ -16,19 +17,24 @@ import com.jetbrains.python.psi.impl.PyTargetExpressionImpl import com.jetbrains.python.psi.resolve.PyResolveContext import com.jetbrains.python.psi.types.PyClassType import com.jetbrains.python.psi.types.PyClassTypeImpl +import javax.swing.JComponent + +var defaultWarnUntypedFields = false class PydanticInspection : PyInspection() { + var warnUntypedFields = defaultWarnUntypedFields override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor = Visitor(holder, session) - private class Visitor(holder: ProblemsHolder, session: LocalInspectionToolSession) : PyInspectionVisitor(holder, session) { + inner class Visitor(holder: ProblemsHolder, session: LocalInspectionToolSession) : PyInspectionVisitor(holder, session) { override fun visitPyFunction(node: PyFunction?) { super.visitPyFunction(node) - val pyClass = node?.parent?.parent as? PyClass ?: return + if (node == null) return + val pyClass = getPyClassByAttribute(node) ?: return if (!isPydanticModel(pyClass, myTypeEvalContext) || !isValidatorMethod(node)) return val paramList = node.parameterList val params = paramList.parameters @@ -49,7 +55,6 @@ class PydanticInspection : PyInspection() { super.visitPyCallExpression(node) if (node == null) return - inspectPydanticModelCallableExpression(node) inspectFromOrm(node) @@ -59,9 +64,10 @@ class PydanticInspection : PyInspection() { super.visitPyAssignmentStatement(node) if (node == null) return - + if (this@PydanticInspection.warnUntypedFields) { + inspectWarnUntypedFields(node) + } inspectReadOnlyProperty(node) - } private fun inspectPydanticModelCallableExpression(pyCallExpression: PyCallExpression) { @@ -110,5 +116,21 @@ class PydanticInspection : PyInspection() { ProblemHighlightType.GENERIC_ERROR) } + + private fun inspectWarnUntypedFields(node: PyAssignmentStatement){ + val pyClass = getPyClassByAttribute(node) ?: return + if (!isPydanticModel(pyClass, myTypeEvalContext)) return + if (node.annotation != null) return + + registerProblem(node, + "Untyped fields disallowed", ProblemHighlightType.WARNING) + + } + } + + override fun createOptionsPanel(): JComponent? { + val panel = MultipleCheckboxOptionsPanel(this) + panel.addCheckbox( "Warning untyped fields", "warnUntypedFields") + return panel } } \ No newline at end of file diff --git a/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt b/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt index 189a1038..aea5ac3d 100644 --- a/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt +++ b/src/com/koxudaxi/pydantic/PydanticTypeProvider.kt @@ -19,7 +19,7 @@ class PydanticTypeProvider : PyTypeProviderBase() { override fun getReferenceType(referenceTarget: PsiElement, context: TypeEvalContext, anchor: PsiElement?): Ref? { if (referenceTarget is PyTargetExpression) { - val pyClass = referenceTarget.parent?.parent?.parent as? PyClass ?: return null + val pyClass = getPyClassByAttribute(referenceTarget.parent) ?: return null if (!isPydanticModel(pyClass, context)) return null val name = referenceTarget.name ?: return null getRefTypeFromFieldName(name, context, pyClass)?.let { return it } diff --git a/testData/inspection/warnUntypedFields.py b/testData/inspection/warnUntypedFields.py new file mode 100644 index 00000000..1dd01104 --- /dev/null +++ b/testData/inspection/warnUntypedFields.py @@ -0,0 +1,21 @@ +from pydantic import BaseModel + + +class A(BaseModel): + a = '123' + + +class B(BaseModel): + b: str = '123' + + +class C: + c = '123' + +class D: + d + +def e(): + ee = '123' + +f = '123' diff --git a/testData/inspection/warnUntypedFieldsDisable.py b/testData/inspection/warnUntypedFieldsDisable.py new file mode 100644 index 00000000..c6e3c11e --- /dev/null +++ b/testData/inspection/warnUntypedFieldsDisable.py @@ -0,0 +1,22 @@ +from pydantic import BaseModel + + +class A(BaseModel): + a = '123' + + +class B(BaseModel): + b: str = '123' + + +class C: + c = '123' + + +class D: + d + +def e(): + ee = '123' + +f = '123' diff --git a/testSrc/com/koxudaxi/pydantic/PydanticInspectionTest.kt b/testSrc/com/koxudaxi/pydantic/PydanticInspectionTest.kt index f02de282..f8862ed0 100644 --- a/testSrc/com/koxudaxi/pydantic/PydanticInspectionTest.kt +++ b/testSrc/com/koxudaxi/pydantic/PydanticInspectionTest.kt @@ -49,4 +49,17 @@ open class PydanticInspectionTest : PydanticInspectionBase() { fun testReadOnlyProperty() { doTest() } + + fun testWarnUntypedFieldsDisable() { + doTest() + } + + fun testWarnUntypedFields() { + try { + defaultWarnUntypedFields = true + doTest() + } finally { + defaultWarnUntypedFields = false + } + } }