mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
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:
committed by
intellij-monorepo-bot
parent
041ec1f6e2
commit
eb3eeb0fca
@@ -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
|
||||
|
||||
+27
-22
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-2
@@ -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"
|
||||
|
||||
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
+2
-2
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user