PY-57582 dataclasses: field with default value preceding KW_ONLY shows false-positive warning

- support marker field typed with KW_ONLY
- adjust other inspection message

GitOrigin-RevId: 28f30ffcc3400bc719c63eacd76e432a547a1a9c
This commit is contained in:
Marcus Mews
2025-09-26 07:22:47 +00:00
committed by intellij-monorepo-bot
parent 041ec1f6e2
commit eb3eeb0fca
12 changed files with 99 additions and 44 deletions
@@ -523,6 +523,10 @@ INSP.GROUP.python=Python
INSP.abstract.class.set.as.metaclass=Set ''{0}'' as metaclass
INSP.abstract.class.add.to.superclasses=Add ''{0}'' to superclasses
INSP.named.tuple=Invalid definition of 'typing.NamedTuple'
INSP.named.tuple.default.value.order.local=Fields with a default value must come after any fields without a default.
INSP.named.tuple.default.value.order.superclass=Non-default argument(s) follows default argument(s) defined in {0}
INSP.named.tuple.default.value.order.inherited=Inherited non-default argument(s) defined in {0} follows inherited default argument defined in {1}
INSP.shadows.name.from.outer.scope=Shadows name ''{0}'' from outer scope
INSP.trailing.semicolon=Trailing semicolon in the statement
INSP.protected.member.ignore.annotations=Ignore annotations
@@ -9,11 +9,15 @@ import com.intellij.psi.PsiElement
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.ResolveState
import com.intellij.psi.scope.PsiScopeProcessor
import com.jetbrains.python.PyPsiBundle
import com.jetbrains.python.codeInsight.PyDataclassNames.Dataclasses
import com.jetbrains.python.codeInsight.parseDataclassParameters
import com.jetbrains.python.codeInsight.stdlib.PyNamedTupleTypeProvider
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.psi.PyClass
import com.jetbrains.python.psi.PyTargetExpression
import com.jetbrains.python.psi.impl.PyPsiUtils
import com.jetbrains.python.psi.types.PyClassType
import com.jetbrains.python.psi.types.TypeEvalContext
class PyNamedTupleInspection : PyInspection() {
@@ -26,7 +30,7 @@ class PyNamedTupleInspection : PyInspection() {
callback: (PsiElement, @InspectionMessage String, ProblemHighlightType) -> Unit,
fieldsFilter: (PyTargetExpression) -> Boolean = { true },
hasAssignedValue: (PyTargetExpression) -> Boolean = PyTargetExpression::hasAssignedValue) {
val fieldsProcessor = if (classFieldsFilter(cls)) processFields(cls, fieldsFilter, hasAssignedValue) else null
val fieldsProcessor = if (classFieldsFilter(cls)) processFields(cls, fieldsFilter, hasAssignedValue, context) else null
if ((fieldsProcessor == null || fieldsProcessor.fieldsWithoutDefaultValue.isEmpty()) && !checkInheritedOrder) return
@@ -36,7 +40,7 @@ class PyNamedTupleInspection : PyInspection() {
Ancestor.FILTERED
}
else {
val processor = processFields(it, fieldsFilter, hasAssignedValue)
val processor = processFields(it, fieldsFilter, hasAssignedValue, context)
if (processor.fieldsWithDefaultValue.isNotEmpty()) {
Ancestor.HAS_FIELD_WITH_DEFAULT_VALUE
}
@@ -56,12 +60,8 @@ class PyNamedTupleInspection : PyInspection() {
seenAncestorHavingFieldWithDefaultValue = ancestor
}
else if (ancestorKind == Ancestor.HAS_FIELD_WITHOUT_DEFAULT_VALUE && seenAncestorHavingFieldWithDefaultValue != null) {
callback(
cls.superClassExpressionList!!,
"Inherited non-default argument(s) defined in ${ancestor.name} follows " +
"inherited default argument defined in ${seenAncestorHavingFieldWithDefaultValue.name}",
ProblemHighlightType.GENERIC_ERROR
)
val msg = PyPsiBundle.message("INSP.named.tuple.default.value.order.inherited", ancestor.name, seenAncestorHavingFieldWithDefaultValue.name)
callback(cls.superClassExpressionList!!, msg, ProblemHighlightType.GENERIC_ERROR)
break
}
}
@@ -80,26 +80,22 @@ class PyNamedTupleInspection : PyInspection() {
.filter { it.second == Ancestor.HAS_FIELD_WITH_DEFAULT_VALUE }
.joinToString { "'${it.first.name}'" }
callback(classNameElement,
"Non-default argument(s) follows default argument(s) defined in $ancestorNames",
ProblemHighlightType.GENERIC_ERROR)
val msg = PyPsiBundle.message("INSP.named.tuple.default.value.order.superclass", ancestorNames)
callback(fieldsWithoutDefaultNotOverriden.first(), msg, ProblemHighlightType.GENERIC_ERROR)
}
}
val lastFieldWithoutDefault = fieldsWithoutDefaultNotOverriden.last()
fieldsProcessor.fieldsWithDefaultValue
.takeWhile { PyPsiUtils.isBefore(it, lastFieldWithoutDefault) }
.forEach {
callback(it,
"Fields with a default value must come after any fields without a default.",
ProblemHighlightType.GENERIC_ERROR)
}
.takeWhile { PyPsiUtils.isBefore(it, fieldsWithoutDefaultNotOverriden.last()) }
.forEach { callback(it, PyPsiBundle.message("INSP.named.tuple.default.value.order.local"), ProblemHighlightType.GENERIC_ERROR) }
}
}
private fun processFields(cls: PyClass,
filter: (PyTargetExpression) -> Boolean,
hasAssignedValue: (PyTargetExpression) -> Boolean): LocalFieldsProcessor {
val fieldsProcessor = LocalFieldsProcessor(filter, hasAssignedValue)
hasAssignedValue: (PyTargetExpression) -> Boolean,
context: TypeEvalContext): LocalFieldsProcessor {
val isDataclass = parseDataclassParameters(cls, context) != null
val fieldsProcessor = LocalFieldsProcessor(filter, hasAssignedValue, isDataclass, context)
cls.processClassLevelDeclarations(fieldsProcessor)
return fieldsProcessor
}
@@ -127,13 +123,18 @@ class PyNamedTupleInspection : PyInspection() {
}
private class LocalFieldsProcessor(private val filter: (PyTargetExpression) -> Boolean,
private val hasAssignedValue: (PyTargetExpression) -> Boolean) : PsiScopeProcessor {
private val hasAssignedValue: (PyTargetExpression) -> Boolean,
private val isDataclass: Boolean,
private val context: TypeEvalContext) : PsiScopeProcessor {
val fieldsWithDefaultValue = mutableListOf<PyTargetExpression>()
val fieldsWithoutDefaultValue = mutableListOf<PyTargetExpression>()
var kwOnlyMarkerVisited: Boolean = false
override fun execute(element: PsiElement, state: ResolveState): Boolean {
if (element is PyTargetExpression && filter(element)) {
if (hasAssignedValue(element)) {
if (isDataclass && isKwOnlyMarker(element, context)) {
kwOnlyMarkerVisited = true
} else if (hasAssignedValue(element) || kwOnlyMarkerVisited) {
fieldsWithDefaultValue.add(element)
}
else {
@@ -142,5 +143,9 @@ class PyNamedTupleInspection : PyInspection() {
}
return true
}
private fun isKwOnlyMarker(field: PyTargetExpression, context: TypeEvalContext): Boolean {
return (context.getType(field) as? PyClassType)?.classQName == Dataclasses.DATACLASSES_KW_ONLY
}
}
}
@@ -4,5 +4,5 @@ from mod import Base
@dataclasses.dataclass
class <error descr="Non-default argument(s) follows default argument(s) defined in 'Base'">Sub</error>(Base):
field_no_default: int
class Sub(Base):
<error descr="Non-default argument(s) follows default argument(s) defined in 'Base'">field_no_default</error>: int
@@ -3,8 +3,8 @@ from decorator import my_dataclass
@my_dataclass()
class <error descr="Non-default argument(s) follows default argument(s) defined in 'A1'">B1</error>(A1):
y1: str
class B1(A1):
<error descr="Non-default argument(s) follows default argument(s) defined in 'A1'">y1</error>: str
y2: str = "1"
@@ -54,8 +54,8 @@ class BaseNotKwOnlyDefault:
@my_dataclass()
class <error descr="Non-default argument(s) follows default argument(s) defined in 'BaseNotKwOnlyDefault'">SubNotKwOnly</error>(BaseNotKwOnlyDefault):
not_kw_only_no_default: int
class SubNotKwOnly(BaseNotKwOnlyDefault):
<error descr="Non-default argument(s) follows default argument(s) defined in 'BaseNotKwOnlyDefault'">not_kw_only_no_default</error>: int
@my_dataclass(kw_only=True)
@@ -4,5 +4,5 @@ from mod import Base
@dataclasses.dataclass
class <error descr="Non-default argument(s) follows default argument(s) defined in 'Base'">Sub</error>(Base):
field_no_default: int
class Sub(Base):
<error descr="Non-default argument(s) follows default argument(s) defined in 'Base'">field_no_default</error>: int
@@ -7,8 +7,8 @@ class A1:
x2: int = 1
@dataclasses.dataclass
class <error descr="Non-default argument(s) follows default argument(s) defined in 'A1'">B1</error>(A1):
y1: str
class B1(A1):
<error descr="Non-default argument(s) follows default argument(s) defined in 'A1'">y1</error>: str
y2: str = "1"
@attr.dataclass
@@ -17,8 +17,8 @@ class A2:
x2: int = 1
@attr.dataclass
class <error descr="Non-default argument(s) follows default argument(s) defined in 'A2'">B2</error>(A2):
y1: str
class B2(A2):
<error descr="Non-default argument(s) follows default argument(s) defined in 'A2'">y1</error>: str
y2: str = "1"
@dataclasses.dataclass
@@ -0,0 +1,28 @@
from dataclasses import dataclass, KW_ONLY
from typing import NamedTuple
@dataclass
class A1:
bar1: int = 1 # no error
_: KW_ONLY
bar2: int
@dataclass
class Base:
x: int = 0
@dataclass
class Child1(Base):
<error descr="Non-default argument(s) follows default argument(s) defined in 'Base'">y</error>: int
_: KW_ONLY
z: int = 1
@dataclass
class Child2(Base):
_: KW_ONLY
y: int # no error
z: int = 1
@@ -12,16 +12,16 @@ class A1(B):
b: int
@dataclass
class <error descr="Non-default argument(s) follows default argument(s) defined in 'B'">A2</error>(B):
class A2(B):
a: int
c: int
<error descr="Non-default argument(s) follows default argument(s) defined in 'B'">c</error>: int
b: int
@dataclass
class <error descr="Non-default argument(s) follows default argument(s) defined in 'B'">A3</error>(B):
class A3(B):
a: int
b: int
c: int
<error descr="Non-default argument(s) follows default argument(s) defined in 'B'">c</error>: int
@dataclass
class A4(B):
@@ -30,25 +30,25 @@ class A4(B):
c: int = 1
@dataclass
class <error descr="Non-default argument(s) follows default argument(s) defined in 'B'">A5</error>(B):
class A5(B):
a: int
<error descr="Fields with a default value must come after any fields without a default.">c</error>: int = 1
<error descr="Fields with a default value must come after any fields without a default.">d</error>: int = 1
b: int
e: int
<error descr="Non-default argument(s) follows default argument(s) defined in 'B'">e</error>: int
@dataclass
class <error descr="Non-default argument(s) follows default argument(s) defined in 'B'">A6</error>(B):
class A6(B):
a: int
<error descr="Fields with a default value must come after any fields without a default.">c</error>: int = 1
d: int
<error descr="Non-default argument(s) follows default argument(s) defined in 'B'">d</error>: int
e: int
b: int = 1
@dataclass
class <error descr="Non-default argument(s) follows default argument(s) defined in 'B'">A7</error>(B):
class A7(B):
a: int
b: int
<error descr="Fields with a default value must come after any fields without a default.">c</error>: int = 1
<error descr="Fields with a default value must come after any fields without a default.">d</error>: int = 1
e: int
<error descr="Non-default argument(s) follows default argument(s) defined in 'B'">e</error>: int
@@ -0,0 +1,9 @@
from typing import NamedTuple
from dataclasses import KW_ONLY
class NoDataclass(NamedTuple):
y: int
_: KW_ONLY # no effect
<error descr="Fields with a default value must come after any fields without a default.">z</error>: int = 1
bar2: int
@@ -400,6 +400,11 @@ public class PyDataclassInspectionTest extends PyInspectionTestCase {
doMultiFileTest();
}
// PY-57582
public void testFieldsOrderKwOnly() {
doTest();
}
// PY-49946
public void testFieldsOrderOverridden() {
doTest();
@@ -25,6 +25,10 @@ public class PyNamedTupleInspectionTest extends PyInspectionTestCase {
runWithLanguageLevel(LanguageLevel.PYTHON36, this::doTest);
}
public void testTypingNTFieldsOrderKWOnly() {
doTest();
}
@NotNull
@Override
protected Class<? extends PyInspection> getInspectionClass() {