PY-50401 Impl checking name argument in ParamSpec

GitOrigin-RevId: 655026dceea03b1697e5d39ce897f2ef1f25948c
This commit is contained in:
andrey.matveev
2022-05-06 12:15:27 +00:00
committed by intellij-monorepo-bot
parent eb7114c66a
commit a66dea2997
3 changed files with 84 additions and 29 deletions
@@ -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
@@ -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<PyExpression?>()
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
@@ -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(<warning descr=\"'ParamSpec()' expects a string literal as first argument\">name</warning>)\n" +
"T1 = ParamSpec('T1')");
}
// PY-50401
public void testParamSpecNameAndTargetNameEquality() {
doTestByText("from typing import ParamSpec\n" +
"\n" +
"T0 = ParamSpec(<warning descr=\"The argument to 'ParamSpec()' must be a string equal to the variable name to which it is assigned\">'T1'</warning>)\n" +
"T1 = ParamSpec('T1')");
}
// PY-50930
public void testNoInspectionInCallableParameterParamSpecFromTypingExpressions() {
doTestByText("from typing import Callable, TypeVar\n" +