Skip to content

Commit

Permalink
Improve field detections
Browse files Browse the repository at this point in the history
  • Loading branch information
koxudaxi committed Apr 14, 2024
1 parent fafd533 commit 323b6dd
Show file tree
Hide file tree
Showing 7 changed files with 40 additions and 23 deletions.
30 changes: 22 additions & 8 deletions src/com/koxudaxi/pydantic/PydanticInspection.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.koxudaxi.pydantic
import com.intellij.codeInspection.LocalInspectionToolSession
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.diff.comparison.trimEnd
import com.intellij.psi.PsiElementVisitor
import com.jetbrains.python.PyNames
import com.jetbrains.python.codeInsight.stdlib.PyDataclassTypeProvider
Expand All @@ -16,7 +15,10 @@ import com.jetbrains.python.psi.impl.PyCallExpressionImpl
import com.jetbrains.python.psi.impl.PyEvaluator
import com.jetbrains.python.psi.impl.PyTargetExpressionImpl
import com.jetbrains.python.psi.resolve.PyResolveContext
import com.jetbrains.python.psi.types.*
import com.jetbrains.python.psi.types.PyCallableType
import com.jetbrains.python.psi.types.PyClassType
import com.jetbrains.python.psi.types.PyTypeChecker
import com.jetbrains.python.psi.types.TypeEvalContext


class PydanticInspection : PyInspection() {
Expand Down Expand Up @@ -426,21 +428,33 @@ class PydanticInspection : PyInspection() {
private fun inspectModelAttribute(node: PyQualifiedExpression) {
val qualifier = node.qualifier ?: return
val name = node.name ?: return
val pyClass = (myTypeEvalContext.getType(qualifier) as? PyClassType)?.pyClass ?: return
val pyClassType = myTypeEvalContext.getType(qualifier) as? PyClassType ?: return
val pyClass = pyClassType.pyClass
if (!isPydanticModel(pyClass, false, myTypeEvalContext)) return
if (pyClass.findNestedClass(name, true) is PyClass) return
if (pyClass.findProperty(name, true, myTypeEvalContext) != null) return
if (pyClass.findMethodByName(name, true, myTypeEvalContext) != null) return
val field = pyClass.findClassAttribute(name, true, myTypeEvalContext)
if (field is PyAnnotationOwner && PyTypingTypeProvider.isClassVar(field, myTypeEvalContext)) return

val pydanticVersion = PydanticCacheService.getVersion(pyClass.project)

// Check private field or model fields
if (name.startsWith("_") || pydanticVersion.isV2 && name.startsWith(MODEL_FIELD_PREFIX)) return

val config = getConfig(pyClass, myTypeEvalContext, true)
getAncestorPydanticModels(pyClass, true, myTypeEvalContext).forEach {
if (hasAttribute(it, config, pydanticVersion.isV2, name)) return
if (pyClassType.isDefinition) {
if(field == null && node.reference?.resolve() is PyTargetExpression) return
} else {
getAncestorPydanticModels(pyClass, true, myTypeEvalContext).forEach {
if (hasAttribute(it, config, pydanticVersion.isV2, name)) return
}
if (hasAttribute(pyClass, config, pydanticVersion.isV2, name)) return
}
if (hasAttribute(pyClass, config, pydanticVersion.isV2, name)) return
registerProblem(node.node.lastChildNode.psi, "Unresolved attribute reference '${name}' for class '${pyClass.name}' ")
}
private fun hasAttribute(pyClass: PyClass, config: HashMap<String, Any?>, isV2: Boolean, name: String): Boolean =
getPydanticField(pyClass, myTypeEvalContext, config, isV2, false, name)
.any() || pyClass.findNestedClass(name, true) is PyClass
getPydanticField(pyClass, myTypeEvalContext, config, isV2, false, name).any()

}

Expand Down
6 changes: 3 additions & 3 deletions testData/inspection/readOnlyProperty.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ class G(BaseModel):
class Config:
allow_mutation=False
G.abc =<EOLError descr="Expression expected"></EOLError>
G.<warning descr="Unresolved attribute reference 'abc' for class 'G'">abc</warning>.lower()
<error descr="Cannot assign to function call">G.<warning descr="Unresolved attribute reference 'abc' for class 'G'">abc</warning>.lower()</error> = 'efg'

G.abc = "test"
G.abc.lower()
<error descr="Cannot assign to function call">G.abc.lower()</error> = 'efg'

class H:
class Config:
Expand Down
6 changes: 3 additions & 3 deletions testData/inspectionv18/readOnlyProperty.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ class G(BaseModel):
class Config:
allow_mutation=False
G.abc =<EOLError descr="Expression expected"></EOLError>
G.<warning descr="Unresolved attribute reference 'abc' for class 'G'">abc</warning>.lower()
<error descr="Cannot assign to function call">G.<warning descr="Unresolved attribute reference 'abc' for class 'G'">abc</warning>.lower()</error> = 'efg'

G.abc = "test"
G.abc.lower()
<error descr="Cannot assign to function call">G.abc.lower()</error> = 'efg'

class H:
class Config:
Expand Down
6 changes: 3 additions & 3 deletions testData/inspectionv18/readOnlyPropertyFrozen.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ class G(BaseModel):
class Config:
frozen=False
G.abc =<EOLError descr="Expression expected"></EOLError>
G.<warning descr="Unresolved attribute reference 'abc' for class 'G'">abc</warning>.lower()
<error descr="Cannot assign to function call">G.<warning descr="Unresolved attribute reference 'abc' for class 'G'">abc</warning>.lower()</error> = 'efg'

G.abc = "test"
G.abc.lower()
<error descr="Cannot assign to function call">G.abc.lower()</error> = 'efg'

class H:
class Config:
Expand Down
5 changes: 3 additions & 2 deletions testData/inspectionv2/modelAttribute.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ class B(BaseModel):
class Inner(BaseModel):
user: str
a: int = 1
# _url: str = "https://someurl"
_url: str = "https://someurl"
TEST: ClassVar[str] = "Hello World"
def f(self):
self._url
self.Inner
self.<warning descr="Unresolved attribute reference 'fake' for class 'B'">fake</warning>
self.a
# self._url
B.<warning descr="Unresolved attribute reference 'a' for class 'B'">a</warning>
b = B(a=1)
b.<warning descr="Unresolved attribute reference 'fake' for class 'B'">fake</warning>
B.Inner
Expand Down
5 changes: 3 additions & 2 deletions testData/inspectionv2/readOnlyPropertyFrozenConfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ class G(BaseModel):
class Config:
frozen=False
G.abc =<EOLError descr="Expression expected"></EOLError>
G.<warning descr="Unresolved attribute reference 'abc' for class 'G'">abc</warning>.lower()
<error descr="Cannot assign to function call">G.<warning descr="Unresolved attribute reference 'abc' for class 'G'">abc</warning>.lower()</error> = 'efg'
G.abc = "test"
G.abc.lower()
<error descr="Cannot assign to function call">G.abc.lower()</error> = 'efg'


class H:
Expand Down
5 changes: 3 additions & 2 deletions testData/inspectionv2/readOnlyPropertyFrozenConfigDict.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ class F(D):
class G(BaseModel):
model_config = ConfigDict(frozen=False)
G.abc =<EOLError descr="Expression expected"></EOLError>
G.<warning descr="Unresolved attribute reference 'abc' for class 'G'">abc</warning>.lower()
<error descr="Cannot assign to function call">G.<warning descr="Unresolved attribute reference 'abc' for class 'G'">abc</warning>.lower()</error> = 'efg'
G.abc = "test"
G.abc.lower()
<error descr="Cannot assign to function call">G.abc.lower()</error> = 'efg'


class H:
Expand Down

0 comments on commit 323b6dd

Please sign in to comment.