mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-01-06 03:21:12 +07:00
PY-61857 Implement PEP 695 Type Parameter Syntax usage inspection:
Inspection covers such cases: * Extending typing.Generic in new-style generic classes * Extending parameterized typing.Protocol in new-style generic classes * Using generic upper bounds and constraints with type parameters for ParamSpec and TypeVarTuple * Mixing traditional and new-style type variables * Using traditional type variables in new-style type aliases GitOrigin-RevId: 8812959f64d2d87e1b72f713405edb86936220b9
This commit is contained in:
committed by
intellij-monorepo-bot
parent
ac6316198f
commit
646ba00a3d
@@ -232,6 +232,7 @@
|
||||
<localInspection language="Python" shortName="PyChainedComparisonsInspection" suppressId="PyChainedComparisons" bundle="messages.PyPsiBundle" key="INSP.NAME.chained.comparisons" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WEAK WARNING" implementationClass="com.jetbrains.python.inspections.PyChainedComparisonsInspection"/>
|
||||
<localInspection language="Python" shortName="PyPep8NamingInspection" suppressId="PyPep8Naming" bundle="messages.PyPsiBundle" key="INSP.NAME.pep8.naming" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WEAK WARNING" implementationClass="com.jetbrains.python.inspections.PyPep8NamingInspection"/>
|
||||
<localInspection language="Python" shortName="PyShadowingBuiltinsInspection" suppressId="PyShadowingBuiltins" bundle="messages.PyPsiBundle" key="INSP.NAME.shadowing.builtins" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WEAK WARNING" implementationClass="com.jetbrains.python.inspections.PyShadowingBuiltinsInspection"/>
|
||||
<localInspection language="Python" shortName="PyNewStyleGenericSyntaxInspection" suppressId="PyNewStyleGenericSyntax" bundle="messages.PyPsiBundle" key="INSP.NAME.new.style.generics.type.param.syntax" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.inspections.PyNewStyleGenericSyntaxInspection"/>
|
||||
|
||||
<codeInsight.parameterNameHints language="Python"
|
||||
implementationClass="com.jetbrains.python.inlayHints.PythonInlayParameterHintsProvider"/>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<html>
|
||||
<body>
|
||||
<p>Reports invalid usage of <a href="https://www.python.org/dev/peps/pep-0695/">PEP 695</a> type parameter syntax
|
||||
<p>
|
||||
Finds the following problems in function and class definitions and new-style type alias statements:
|
||||
<ul>
|
||||
<li>Extending typing.Generic in new-style generic classes</li>
|
||||
<li>Extending parameterized typing.Protocol in new-style generic classes</li>
|
||||
<li>Using generic upper bounds and constraints with type parameters for ParamSpec and TypeVarTuple</li>
|
||||
<li>Mixing traditional and new-style type variables</li>
|
||||
<li>Using traditional type variables in new-style type aliases</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
Examples:
|
||||
</p>
|
||||
<pre><code>
|
||||
from typing import Generic
|
||||
|
||||
class Example[T](Generic[T]): ... # Classes with type parameter list should not extend 'Generic'
|
||||
</code></pre>
|
||||
|
||||
<pre><code>
|
||||
class Example[T: (list[S], str)]: ... # Generic types are not allowed inside constraints and bounds of type parameters
|
||||
</code></pre>
|
||||
|
||||
<pre><code>
|
||||
from typing import TypeVar
|
||||
|
||||
K = TypeVar("K")
|
||||
|
||||
class ClassC[V]:
|
||||
def method2[M](self, a: M, b: K) -> M | K: ... # Mixing traditional and new-style TypeVars is not allowed
|
||||
</code></pre>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1202,3 +1202,17 @@ INSP.class.var.can.not.include.type.variables='ClassVar' parameter cannot includ
|
||||
# Pandas-Specific inspections and quick fixes
|
||||
INSP.pandas.series.values.replace.with.tolist=Method Series.to_list() is recommended
|
||||
QFIX.pandas.series.values.replace.with.tolist=Replace list(Series.values) with Series.to_list()
|
||||
|
||||
# PyTypeParameterListAnnotator
|
||||
type.param.list.annotator.type.parameter.already.defined=Type parameter with name ''{0}'' is already defined in this type parameter list
|
||||
type.param.list.annotator.two.or.more.types.required=Two or more types required
|
||||
type.param.list.annotator.type.var.tuple.and.param.spec.can.not.have.bounds=ParamSpec and TypeVarTuple cannot have constraints and upper bounds
|
||||
|
||||
# PyNewStyleGenericSyntaxInspection
|
||||
INSP.NAME.new.style.generics.type.param.syntax=Invalid usage of new-style type parameters and type aliases
|
||||
INSP.new.style.generics.are.not.allowed.inside.type.param.bounds=Generic types are not allowed inside constraints and bounds of type parameters
|
||||
INSP.new.style.generics.old.style.type.vars.not.allowed.in.new.style.type.aliases=Traditional TypeVars are not allowed inside new-style type alias statements
|
||||
INSP.new.style.generics.classes.with.type.param.list.should.not.extend.generic=Classes with an explicit type parameter list should not extend 'Generic'
|
||||
INSP.new.style.generics.extending.protocol.does.not.need.parameterization=Extending 'Protocol' does not need parameterization in classes with a type parameter list
|
||||
INSP.new.style.generics.mixing.old.style.and.new.style.type.vars.not.allowed=Mixing traditional and new-style type variables is not allowed
|
||||
INSP.new.style.generics.assignment.expressions.not.allowed=Assignment expressions are not allowed inside declarations of classes, functions and type aliases having type parameter list
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package com.jetbrains.python.inspections
|
||||
|
||||
import com.intellij.codeInspection.LocalInspectionToolSession
|
||||
import com.intellij.codeInspection.ProblemHighlightType
|
||||
import com.intellij.codeInspection.ProblemsHolder
|
||||
import com.intellij.codeInspection.util.InspectionMessage
|
||||
import com.intellij.openapi.util.Ref
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.PsiElementVisitor
|
||||
import com.intellij.psi.util.PsiTreeUtil
|
||||
import com.jetbrains.python.PyPsiBundle
|
||||
import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider
|
||||
import com.jetbrains.python.psi.*
|
||||
import com.jetbrains.python.psi.types.PyClassLikeType
|
||||
import com.jetbrains.python.psi.types.PyTypeVarType
|
||||
import com.jetbrains.python.psi.types.TypeEvalContext
|
||||
|
||||
class PyNewStyleGenericSyntaxInspection : PyInspection() {
|
||||
|
||||
override fun buildVisitor(holder: ProblemsHolder,
|
||||
isOnTheFly: Boolean,
|
||||
session: LocalInspectionToolSession): PsiElementVisitor = Visitor(holder,
|
||||
PyInspectionVisitor.getContext(session))
|
||||
|
||||
private class Visitor(holder: ProblemsHolder, context: TypeEvalContext) : PyInspectionVisitor(holder, context) {
|
||||
|
||||
override fun visitPyTypeParameter(typeParameter: PyTypeParameter) {
|
||||
val boundExpression = typeParameter.boundExpression
|
||||
if (boundExpression != null) {
|
||||
val boundElementsToProcess =
|
||||
PsiTreeUtil.findChildrenOfAnyType(boundExpression, false, PyReferenceExpression::class.java, PyLiteralExpression::class.java)
|
||||
|
||||
boundElementsToProcess
|
||||
.filterIsInstance<PyReferenceExpression>()
|
||||
.associateWith { Ref.deref(PyTypingTypeProvider.getType(it, myTypeEvalContext)) }
|
||||
.filter { (_, v) -> v is PyTypeVarType }
|
||||
.forEach { (k, _) ->
|
||||
registerProblem(k,
|
||||
PyPsiBundle.message("INSP.new.style.generics.are.not.allowed.inside.type.param.bounds"),
|
||||
ProblemHighlightType.GENERIC_ERROR)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun visitPyTypeAliasStatement(node: PyTypeAliasStatement) {
|
||||
val typeExpression = node.typeExpression
|
||||
if (typeExpression != null) {
|
||||
reportOldStyleTypeVarsUsage(typeExpression,
|
||||
PyPsiBundle.message(
|
||||
"INSP.new.style.generics.old.style.type.vars.not.allowed.in.new.style.type.aliases"))
|
||||
reportAssignmentExpressions(typeExpression,
|
||||
PyPsiBundle.message("INSP.new.style.generics.assignment.expressions.not.allowed"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun visitPyFunction(node: PyFunction) {
|
||||
val returnTypeAnnotation = node.annotation
|
||||
val typeParameterList = node.typeParameterList
|
||||
|
||||
if (typeParameterList != null) {
|
||||
reportOldStyleTypeVarsUsage(node.parameterList,
|
||||
PyPsiBundle.message("INSP.new.style.generics.mixing.old.style.and.new.style.type.vars.not.allowed"))
|
||||
node.parameterList
|
||||
.parameters
|
||||
.filterIsInstance<PyNamedParameter>()
|
||||
.mapNotNull { it.annotation }
|
||||
.forEach { annotation ->
|
||||
reportAssignmentExpressions(annotation, PyPsiBundle.message("INSP.new.style.generics.assignment.expressions.not.allowed"))
|
||||
}
|
||||
if (returnTypeAnnotation != null) {
|
||||
reportOldStyleTypeVarsUsage(returnTypeAnnotation,
|
||||
PyPsiBundle.message("INSP.new.style.generics.mixing.old.style.and.new.style.type.vars.not.allowed"))
|
||||
reportAssignmentExpressions(returnTypeAnnotation,
|
||||
PyPsiBundle.message("INSP.new.style.generics.assignment.expressions.not.allowed"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun visitPyClass(node: PyClass) {
|
||||
if (node.typeParameterList != null) {
|
||||
val superClassExpressionList = node.superClassExpressionList
|
||||
if (superClassExpressionList != null) {
|
||||
node.superClassExpressions.forEach { ancestor ->
|
||||
val reference = if (ancestor is PySubscriptionExpression) ancestor.operand else ancestor
|
||||
val type = myTypeEvalContext.getType(reference)
|
||||
if (type is PyClassLikeType) {
|
||||
val qName = type.classQName
|
||||
if (PyTypingTypeProvider.GENERIC == qName) {
|
||||
registerProblem(ancestor,
|
||||
PyPsiBundle.message("INSP.new.style.generics.classes.with.type.param.list.should.not.extend.generic"),
|
||||
ProblemHighlightType.GENERIC_ERROR_OR_WARNING)
|
||||
}
|
||||
if (PyTypingTypeProvider.PROTOCOL == qName || PyTypingTypeProvider.PROTOCOL_EXT == qName) {
|
||||
if (ancestor is PySubscriptionExpression) {
|
||||
registerProblem(ancestor.indexExpression,
|
||||
PyPsiBundle.message("INSP.new.style.generics.extending.protocol.does.not.need.parameterization"),
|
||||
ProblemHighlightType.GENERIC_ERROR_OR_WARNING)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
reportOldStyleTypeVarsUsage(superClassExpressionList,
|
||||
PyPsiBundle.message("INSP.new.style.generics.mixing.old.style.and.new.style.type.vars.not.allowed"))
|
||||
reportAssignmentExpressions(superClassExpressionList,
|
||||
PyPsiBundle.message("INSP.new.style.generics.assignment.expressions.not.allowed"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun reportOldStyleTypeVarsUsage(element: PsiElement, @InspectionMessage message: String) {
|
||||
PsiTreeUtil.findChildrenOfAnyType(element, false, PyReferenceExpression::class.java)
|
||||
.associateWith { Ref.deref(PyTypingTypeProvider.getType(it, myTypeEvalContext)) }
|
||||
// if declarationElement a.k.a target expression is null then it most likely resolves to type parameter
|
||||
.filter { (_, v) -> v is PyTypeVarType && v.declarationElement != null }
|
||||
.forEach { (k, _) ->
|
||||
registerProblem(k, message,
|
||||
ProblemHighlightType.GENERIC_ERROR)
|
||||
}
|
||||
}
|
||||
|
||||
private fun reportAssignmentExpressions(element: PsiElement, @InspectionMessage message: String) {
|
||||
PsiTreeUtil.findChildrenOfAnyType(element, false, PyAssignmentExpression::class.java)
|
||||
.forEach { assignmentExpr ->
|
||||
registerProblem(assignmentExpr, message,
|
||||
ProblemHighlightType.GENERIC_ERROR)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.inspections;
|
||||
|
||||
import com.jetbrains.python.fixtures.PyInspectionTestCase;
|
||||
import com.jetbrains.python.psi.LanguageLevel;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class PyNewStyleGenericSyntaxInspectionTest extends PyInspectionTestCase {
|
||||
|
||||
public void testGenericTypeReportedInTypeVarBound() {
|
||||
runWithLanguageLevel(LanguageLevel.PYTHON312,
|
||||
() -> doTestByText("""
|
||||
class ClassC[V]:
|
||||
class ClassD[T: dict[str, <error descr="Generic types are not allowed inside constraints and bounds of type parameters">V</error>]]: ...
|
||||
"""));
|
||||
}
|
||||
|
||||
public void testOldStyleTypeVarReportedInSuperClass() {
|
||||
runWithLanguageLevel(LanguageLevel.PYTHON312,
|
||||
() -> doTestByText("""
|
||||
from typing import TypeVar
|
||||
K = TypeVar("K")
|
||||
|
||||
class ClassA[V](dict[<error descr="Mixing traditional and new-style type variables is not allowed">K</error>, V]): ...
|
||||
"""));
|
||||
}
|
||||
|
||||
public void testExtendingGenericReportedInClassWithTypeParameterList() {
|
||||
runWithLanguageLevel(LanguageLevel.PYTHON312,
|
||||
() -> doTestByText("""
|
||||
from typing import TypeVar, Generic
|
||||
class ClassA[T](<warning descr="Classes with an explicit type parameter list should not extend 'Generic'">Generic[T]</warning>): ...\s
|
||||
"""));
|
||||
}
|
||||
|
||||
public void testParameterizingExtendedProtocolReportedInClassWithTypeParameterList() {
|
||||
runWithLanguageLevel(LanguageLevel.PYTHON312,
|
||||
() -> doTestByText("""
|
||||
from typing import Protocol
|
||||
class ClassA[T](Protocol[<warning descr="Extending 'Protocol' does not need parameterization in classes with a type parameter list">T</warning>]): ...
|
||||
"""));
|
||||
}
|
||||
|
||||
public void testStringLiteralNotReportedInTypeParameterBound() {
|
||||
runWithLanguageLevel(LanguageLevel.PYTHON312,
|
||||
() -> doTestByText("""
|
||||
class ClassB[T: "ForwardReference"]: ... # OK
|
||||
"""));
|
||||
}
|
||||
|
||||
public void testOldStyleTypeVarReportedInTypeAliasStatement() {
|
||||
runWithLanguageLevel(LanguageLevel.PYTHON312,
|
||||
() -> doTestByText("""
|
||||
from typing import TypeVar
|
||||
T = TypeVar('T')
|
||||
|
||||
type m = list[<error descr="Traditional TypeVars are not allowed inside new-style type alias statements">T</error>]
|
||||
"""));
|
||||
}
|
||||
|
||||
public void testOldStyleTypeVarReportedInTypeAliasStatementWithTypeParameterList() {
|
||||
runWithLanguageLevel(LanguageLevel.PYTHON312,
|
||||
() -> doTestByText("""
|
||||
from typing import TypeVar
|
||||
T = TypeVar('T')
|
||||
|
||||
type m[K] = dict[K, <error descr="Traditional TypeVars are not allowed inside new-style type alias statements">T</error>]
|
||||
"""));
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void testOldStyleTypeVarReportedInParameterListOfFunctionWithTypeParameterList() {
|
||||
runWithLanguageLevel(LanguageLevel.PYTHON312,
|
||||
() -> doTestByText("""
|
||||
from typing import TypeVar
|
||||
K = TypeVar("K")
|
||||
|
||||
class ClassC[V]:
|
||||
|
||||
def method2[M](self, a: M, b: <error descr="Mixing traditional and new-style type variables is not allowed">K</error>): ...
|
||||
"""));
|
||||
}
|
||||
|
||||
public void testMixingOldStyleAndNewStyleTypeParametersIsOkInFunctionWithoutTypeParameterList() {
|
||||
runWithLanguageLevel(LanguageLevel.PYTHON312,
|
||||
() -> doTestByText("""
|
||||
from typing import TypeVar
|
||||
K = TypeVar("K")
|
||||
|
||||
class ClassC[V]:
|
||||
def method1(self, a: V, b: K) -> V | K: ...
|
||||
"""));
|
||||
}
|
||||
|
||||
public void testAssignmentExpressionReportedInsideClassDeclarationWithTypeParameterList() {
|
||||
runWithLanguageLevel(LanguageLevel.PYTHON312,
|
||||
() -> doTestByText("""
|
||||
class ClassA[T]((<error descr="Assignment expressions are not allowed inside declarations of classes, functions and type aliases having type parameter list">x := Sequence[T]</error>)): ...
|
||||
"""));
|
||||
}
|
||||
|
||||
public void testAssignmentExpressionReportedInsideFunctionDeclarationWithTypeParameterList() {
|
||||
runWithLanguageLevel(LanguageLevel.PYTHON312,
|
||||
() -> doTestByText("""
|
||||
def func1[T](val: (<error descr="Assignment expressions are not allowed inside declarations of classes, functions and type aliases having type parameter list">x := int</error>)): ...
|
||||
"""));
|
||||
}
|
||||
|
||||
public void testAssignmentExpressionReportedInsideFunctionReturnTypeAnnotationWithTypeParameterList() {
|
||||
runWithLanguageLevel(LanguageLevel.PYTHON312,
|
||||
() -> doTestByText("""
|
||||
def func1[T](val: (<error descr="Assignment expressions are not allowed inside declarations of classes, functions and type aliases having type parameter list">x := int</error>)): ...
|
||||
"""));
|
||||
}
|
||||
|
||||
public void testAssignmentExpressionReportedInsideNewStyleTypeAliasDeclaration() {
|
||||
runWithLanguageLevel(LanguageLevel.PYTHON312,
|
||||
() -> doTestByText("""
|
||||
type Alias1[T] = (<error descr="Assignment expressions are not allowed inside declarations of classes, functions and type aliases having type parameter list">x := list[T]</error>)
|
||||
"""));
|
||||
}
|
||||
|
||||
public void testAssignmentExpressionNotReportedInFunctionParamDefaultValue() {
|
||||
runWithLanguageLevel(LanguageLevel.PYTHON312,
|
||||
() -> doTestByText("""
|
||||
def f[T](x: int = (foo := 42)): ...
|
||||
"""));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NotNull Class<? extends PyInspection> getInspectionClass() {
|
||||
return PyNewStyleGenericSyntaxInspection.class;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user