diff --git a/README.md b/README.md index 5fe3e08b..87f8b348 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ * Model-specific `__init__`-arguments type-checking for subclasses of `pydantic.BaseModel` * Refactor support for renaming fields for subclasses of `BaseModel` * (If the field name is refactored from the model definition or `__init__` call keyword arguments, PyCharm will present a dialog offering the choice to automatically rename the keyword where it occurs in a model initialization call. +* Search related-fields by class attributes and keyword arguments of `__init__` with `Ctrl+B` and `Cmd+B` ## How to install: diff --git a/build.gradle b/build.gradle index f13d82ba..902bd8ba 100644 --- a/build.gradle +++ b/build.gradle @@ -36,16 +36,16 @@ allprojects { compileKotlin { kotlinOptions { jvmTarget = "1.8" - languageVersion = "1.2" - apiVersion = "1.2" + languageVersion = "1.3" + apiVersion = "1.3" } } compileTestKotlin { kotlinOptions { jvmTarget = "1.8" - languageVersion = "1.2" - apiVersion = "1.2" + languageVersion = "1.3" + apiVersion = "1.23" } } diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml index d4fdab3c..1c733fa8 100644 --- a/resources/META-INF/plugin.xml +++ b/resources/META-INF/plugin.xml @@ -1,9 +1,14 @@ com.koxudaxi.pydantic Pydantic - 0.0.13 + 0.0.14 Koudai Aono @koxudaxi version 0.0.14 +

Features

+

version 0.0.13

Features, BugFixes

  • pydantic.dataclasses.dataclass @@ -50,6 +56,7 @@ enabledByDefault="true" level="WARNING" implementationClass="com.koxudaxi.pydantic.PydanticInspection"/> + diff --git a/src/com/koxudaxi/pydantic/PydanticBaseModel.kt b/src/com/koxudaxi/pydantic/PydanticBaseModel.kt new file mode 100644 index 00000000..720d8da4 --- /dev/null +++ b/src/com/koxudaxi/pydantic/PydanticBaseModel.kt @@ -0,0 +1,11 @@ +package com.koxudaxi.pydantic + +import com.jetbrains.python.psi.PyCallExpression +import com.jetbrains.python.psi.PyClass +import com.jetbrains.python.psi.PyKeywordArgument + + +fun getPyClassByPyKeywordArgument(pyKeywordArgument: PyKeywordArgument) : PyClass? { + val pyCallExpression = pyKeywordArgument.parent?.parent as? PyCallExpression ?: return null + return pyCallExpression.callee?.reference?.resolve() as? PyClass ?: return null +} diff --git a/src/com/koxudaxi/pydantic/PydanticFieldRenameFactory.kt b/src/com/koxudaxi/pydantic/PydanticFieldRenameFactory.kt index 7d266061..26a2254e 100644 --- a/src/com/koxudaxi/pydantic/PydanticFieldRenameFactory.kt +++ b/src/com/koxudaxi/pydantic/PydanticFieldRenameFactory.kt @@ -6,14 +6,12 @@ import com.intellij.psi.util.PsiTreeUtil import com.intellij.refactoring.rename.naming.AutomaticRenamer import com.intellij.refactoring.rename.naming.AutomaticRenamerFactory import com.intellij.usageView.UsageInfo -import com.jetbrains.extensions.python.inherits import com.jetbrains.python.codeInsight.PyCodeInsightSettings import com.jetbrains.python.psi.PyCallExpression import com.jetbrains.python.psi.PyClass import com.jetbrains.python.psi.PyKeywordArgument import com.jetbrains.python.psi.PyTargetExpression import com.jetbrains.python.psi.search.PyClassInheritorsSearch -import com.jetbrains.python.psi.types.TypeEvalContext class PydanticFieldRenameFactory : AutomaticRenamerFactory { @@ -69,7 +67,7 @@ class PydanticFieldRenameFactory : AutomaticRenamerFactory { added.add(pyClass) addClassAttributes(pyClass, elementName) addKeywordArguments(pyClass, elementName) - pyClass.getAncestorClasses(null).forEach { ancestorClass -> + pyClass.getAncestorClasses(null).forEach { ancestorClass -> if (ancestorClass.qualifiedName != "pydantic.main.BaseModel") { if (ancestorClass.isSubclass("pydantic.main.BaseModel", null) && !added.contains(ancestorClass)) { @@ -78,18 +76,15 @@ class PydanticFieldRenameFactory : AutomaticRenamerFactory { } } PyClassInheritorsSearch.search(pyClass, true).forEach { inheritorsPyClass -> - if (inheritorsPyClass.qualifiedName != "pydantic.main.BaseModel" && ! added.contains(inheritorsPyClass)) { + if (inheritorsPyClass.qualifiedName != "pydantic.main.BaseModel" && !added.contains(inheritorsPyClass)) { addAllElement(inheritorsPyClass, elementName, added) } } } private fun addClassAttributes(pyClass: PyClass, elementName: String) { - pyClass.classAttributes.forEach { pyTargetExpression -> - if (pyTargetExpression.name == elementName) { - myElements.add(pyTargetExpression) - } - } + val pyTargetExpression = pyClass.findClassAttribute(elementName, false, null) ?: return + myElements.add(pyTargetExpression) } private fun addKeywordArguments(pyClass: PyClass, elementName: String) { @@ -97,12 +92,13 @@ class PydanticFieldRenameFactory : AutomaticRenamerFactory { val callee = PsiTreeUtil.getParentOfType(psiReference.element, PyCallExpression::class.java) callee?.arguments?.forEach { argument -> if (argument is PyKeywordArgument && argument.name == elementName) { - myElements.add(argument) + myElements.add(argument) } } } } + override fun getDialogTitle(): String { return "Rename Fields" } @@ -121,8 +117,3 @@ class PydanticFieldRenameFactory : AutomaticRenamerFactory { } } - -private fun getPyClassByPyKeywordArgument(pyKeywordArgument: PyKeywordArgument) : PyClass? { - val pyCallExpression = pyKeywordArgument.parent?.parent as? PyCallExpression ?: return null - return pyCallExpression.callee?.reference?.resolve() as? PyClass ?: return null -} \ No newline at end of file diff --git a/src/com/koxudaxi/pydantic/PydanticFieldSearchExecutor.kt b/src/com/koxudaxi/pydantic/PydanticFieldSearchExecutor.kt new file mode 100644 index 00000000..0dffbcf2 --- /dev/null +++ b/src/com/koxudaxi/pydantic/PydanticFieldSearchExecutor.kt @@ -0,0 +1,86 @@ +package com.koxudaxi.pydantic + +import com.intellij.openapi.application.QueryExecutorBase +import com.intellij.openapi.application.ReadAction.run +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiReference +import com.intellij.psi.search.searches.ReferencesSearch +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.util.Processor +import com.jetbrains.python.psi.PyCallExpression +import com.jetbrains.python.psi.PyClass +import com.jetbrains.python.psi.PyKeywordArgument +import com.jetbrains.python.psi.PyTargetExpression +import com.jetbrains.python.psi.search.PyClassInheritorsSearch + +private fun searchField(pyClass: PyClass, elementName: String, consumer: Processor): Boolean { + if (!pyClass.isSubclass("pydantic.main.BaseModel", null)) return false + val pyTargetExpression = pyClass.findClassAttribute(elementName, false, null) ?: return false + consumer.process(pyTargetExpression.reference) + return true +} + +private fun searchKeywordArgument(pyClass: PyClass, elementName: String, consumer: Processor) { + if (!pyClass.isSubclass("pydantic.main.BaseModel", null)) return + ReferencesSearch.search(pyClass as PsiElement).forEach { psiReference -> + val callee = PsiTreeUtil.getParentOfType(psiReference.element, PyCallExpression::class.java) + callee?.arguments?.forEach { argument -> + if (argument is PyKeywordArgument && argument.name == elementName) { + consumer.process(argument.reference) + + } + } + } +} + +private fun searchDirectReferenceField(pyClass: PyClass, elementName: String, consumer: Processor): Boolean { + if (searchField(pyClass, elementName, consumer)) return true + + pyClass.getAncestorClasses(null).forEach { ancestorClass -> + if (ancestorClass.qualifiedName != "pydantic.main.BaseModel") { + if (ancestorClass.isSubclass("pydantic.main.BaseModel", null)) { + if (searchDirectReferenceField(ancestorClass, elementName, consumer)) { + return true + } + } + } + } + return false +} + +private fun searchAllElementReference(pyClass: PyClass?, elementName: String, added: MutableSet, consumer: Processor) { + if (pyClass == null) return + added.add(pyClass) + searchField(pyClass, elementName, consumer) + searchKeywordArgument(pyClass, elementName, consumer) + pyClass.getAncestorClasses(null).forEach { ancestorClass -> + if (ancestorClass.qualifiedName != "pydantic.main.BaseModel" && !added.contains(ancestorClass)){ + searchField(pyClass, elementName, consumer) + } + } + PyClassInheritorsSearch.search(pyClass, true).forEach { inheritorsPyClass -> + if (inheritorsPyClass.qualifiedName != "pydantic.main.BaseModel" && !added.contains(inheritorsPyClass)) { + searchAllElementReference(inheritorsPyClass, elementName, added, consumer) + } + } +} + +class PydanticFieldSearchExecutor : QueryExecutorBase() { + override fun processQuery(queryParameters: ReferencesSearch.SearchParameters, consumer: Processor) { + + when (val element = queryParameters.elementToSearch) { + is PyKeywordArgument -> run { + val elementName = element.name ?: return@run + val pyClass = getPyClassByPyKeywordArgument(element) ?: return@run + if (!pyClass.isSubclass("pydantic.main.BaseModel", null)) return@run + searchDirectReferenceField(pyClass, elementName, consumer) + } + is PyTargetExpression -> run { + val elementName = element.name ?: return@run + val pyClass = element.containingClass ?: return@run + if (!pyClass.isSubclass("pydantic.main.BaseModel", null)) return@run + searchAllElementReference(pyClass, elementName, mutableSetOf(), consumer) + } + } + } +}