diff --git a/python/python-psi-impl/resources/messages/PyPsiBundle.properties b/python/python-psi-impl/resources/messages/PyPsiBundle.properties index 1d3db21206bb..553ab3c383d6 100644 --- a/python/python-psi-impl/resources/messages/PyPsiBundle.properties +++ b/python/python-psi-impl/resources/messages/PyPsiBundle.properties @@ -1127,7 +1127,9 @@ INSP.type.hints.type.specified.both.in.type.comment.and.annotation=Types specifi INSP.type.hints.typevar.expression.must.be.always.directly.assigned.to.variable=A 'TypeVar()' expression must always directly be assigned to a variable INSP.type.hints.type.variables.must.not.be.redefined=Type variables must not be redefined INSP.type.hints.typevar.expects.string.literal.as.first.argument='TypeVar()' expects a string literal as first argument +INSP.type.hints.paramspec.expects.string.literal.as.first.argument='ParamSpec()' expects a string literal as first argument INSP.type.hints.argument.to.typevar.must.be.string.equal.to.variable.name=The argument to 'TypeVar()' must be a string equal to the variable name to which it is assigned +INSP.type.hints.argument.to.paramspec.must.be.string.equal.to.variable.name=The argument to 'ParamSpec()' must be a string equal to the variable name to which it is assigned INSP.type.hints.bivariant.type.variables.are.not.supported=Bivariant type variables are not supported INSP.type.hints.typevar.constraints.cannot.be.combined.with.bound=Constraints cannot be combined with bound=\u2026 INSP.type.hints.single.typevar.constraint.not.allowed=A single constraint is not allowed diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt index 7492226df8c5..b625b625c2e8 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt @@ -3,6 +3,7 @@ package com.jetbrains.python.inspections import com.intellij.codeInsight.controlflow.ControlFlowUtil import com.intellij.codeInspection.* +import com.intellij.codeInspection.util.InspectionMessage import com.intellij.codeInspection.util.IntentionFamilyName import com.intellij.openapi.module.ModuleUtilCore import com.intellij.openapi.project.Project @@ -52,18 +53,29 @@ class PyTypeHintsInspection : PyInspection() { val calleeQName = callee?.let { PyResolveUtil.resolveImportedElementQNameLocally(it) } ?: emptyList() if (QualifiedName.fromDottedString(PyTypingTypeProvider.TYPE_VAR) in calleeQName) { - val target = (node.parent as? PyAssignmentStatement)?.targetsToValuesMapping?.firstOrNull { it.second == node }?.first + val target = getTargetFromAssignment(node) checkTypeVarPlacement(node, target) checkTypeVarArguments(node, target) checkTypeVarRedefinition(target) } + if (QualifiedName.fromDottedString(PyTypingTypeProvider.TYPING_PARAM_SPEC) in calleeQName) { + val target = getTargetFromAssignment(node) + checkParamSpecArguments(node, target) + } + checkInstanceAndClassChecks(node) checkParenthesesOnGenerics(node) } + private fun getTargetFromAssignment(node: PyCallExpression): PyExpression? { + val assignmentStatement = node.parent as? PyAssignmentStatement + if (assignmentStatement == null) return null + return assignmentStatement.targetsToValuesMapping.firstOrNull { it.second == node }?.first + } + override fun visitPyClass(node: PyClass) { super.visitPyClass(node) @@ -210,41 +222,34 @@ class PyTypeHintsInspection : PyInspection() { } } + private fun checkParamSpecArguments(call: PyCallExpression, target: PyExpression?) { + processMatchedArgument(call) { name, argument -> + if (name == "name") { + checkNameIsTheSameAsTarget(argument, target, + PyPsiBundle.message("INSP.type.hints.paramspec.expects.string.literal.as.first.argument"), + PyPsiBundle.message("INSP.type.hints.argument.to.paramspec.must.be.string.equal.to.variable.name")) + } + } + } + private fun checkTypeVarArguments(call: PyCallExpression, target: PyExpression?) { - val resolveContext = PyResolveContext.defaultContext(myTypeEvalContext) var covariant = false var contravariant = false var bound: PyExpression? = null val constraints = mutableListOf() - call - .multiMapArguments(resolveContext) - .firstOrNull { it.unmappedArguments.isEmpty() && it.unmappedParameters.isEmpty() } - ?.let { mapping -> - mapping.mappedParameters.entries.forEach { - val name = it.value.name - val argument = PyUtil.peelArgument(it.key) - - when (name) { - "name" -> - if (argument !is PyStringLiteralExpression) { - registerProblem(argument, PyPsiBundle.message("INSP.type.hints.typevar.expects.string.literal.as.first.argument")) - } - else { - val targetName = target?.name - if (targetName != null && targetName != argument.stringValue) { - registerProblem(argument, - PyPsiBundle.message("INSP.type.hints.argument.to.typevar.must.be.string.equal.to.variable.name"), - ReplaceWithTargetNameQuickFix(targetName)) - } - } - "covariant" -> covariant = PyEvaluator.evaluateAsBoolean(argument, false) - "contravariant" -> contravariant = PyEvaluator.evaluateAsBoolean(argument, false) - "bound" -> bound = argument - "constraints" -> constraints.add(argument) - } - } + processMatchedArgument(call) { name, argument -> + when (name) { + "name" -> + checkNameIsTheSameAsTarget(argument, target, + PyPsiBundle.message("INSP.type.hints.typevar.expects.string.literal.as.first.argument"), + PyPsiBundle.message("INSP.type.hints.argument.to.typevar.must.be.string.equal.to.variable.name")) + "covariant" -> covariant = PyEvaluator.evaluateAsBoolean(argument, false) + "contravariant" -> contravariant = PyEvaluator.evaluateAsBoolean(argument, false) + "bound" -> bound = argument + "constraints" -> constraints.add(argument) } + } if (covariant && contravariant) { registerProblem(call, PyPsiBundle.message("INSP.type.hints.bivariant.type.variables.are.not.supported"), @@ -272,6 +277,37 @@ class PyTypeHintsInspection : PyInspection() { } } + private fun checkNameIsTheSameAsTarget(argument: PyExpression?, target: PyExpression?, + @InspectionMessage notStringLiteralMessage: String, + @InspectionMessage notEqualMessage: String) { + if (argument !is PyStringLiteralExpression) { + registerProblem(argument, notStringLiteralMessage) + } + else { + val targetName = target?.name + if (targetName != null && targetName != argument.stringValue) { + registerProblem(argument, + notEqualMessage, + ReplaceWithTargetNameQuickFix(targetName)) + } + } + } + + private fun processMatchedArgument(call: PyCallExpression, + processor: (name: String?, argument: PyExpression?) -> Unit) { + val resolveContext = PyResolveContext.defaultContext(myTypeEvalContext) + call + .multiMapArguments(resolveContext) + .firstOrNull { it.unmappedArguments.isEmpty() && it.unmappedParameters.isEmpty() } + ?.let { mapping -> + mapping.mappedParameters.entries.forEach { + val name = it.value.name + val argument = PyUtil.peelArgument(it.key) + processor(name, argument) + } + } + } + private fun checkInstanceAndClassChecks(call: PyCallExpression) { if (call.isCalleeText(PyNames.ISINSTANCE, PyNames.ISSUBCLASS)) { val base = call.arguments.getOrNull(1) ?: return diff --git a/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java index 665fea0cb1bc..d6f118e26636 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java @@ -1111,6 +1111,23 @@ public class PyTypeHintsInspectionTest extends PyInspectionTestCase { }); } + // PY-50401 + public void testParamSpecNameAsLiteral() { + doTestByText("from typing import ParamSpec\n" + + "\n" + + "name = 'T0'\n" + + "T0 = ParamSpec(name)\n" + + "T1 = ParamSpec('T1')"); + } + + // PY-50401 + public void testParamSpecNameAndTargetNameEquality() { + doTestByText("from typing import ParamSpec\n" + + "\n" + + "T0 = ParamSpec('T1')\n" + + "T1 = ParamSpec('T1')"); + } + // PY-50930 public void testNoInspectionInCallableParameterParamSpecFromTypingExpressions() { doTestByText("from typing import Callable, TypeVar\n" +