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:
Daniil Kalinin
2023-11-06 13:57:51 +01:00
committed by intellij-monorepo-bot
parent ac6316198f
commit 646ba00a3d
5 changed files with 315 additions and 0 deletions

View File

@@ -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"/>

View File

@@ -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>

View File

@@ -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

View File

@@ -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)
}
}
}
}

View File

@@ -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;
}
}