diff --git a/python/resources/inspectionDescriptions/PyTypeHintsInspection.html b/python/resources/inspectionDescriptions/PyTypeHintsInspection.html new file mode 100644 index 000000000000..d5f597548c75 --- /dev/null +++ b/python/resources/inspectionDescriptions/PyTypeHintsInspection.html @@ -0,0 +1,5 @@ + + +This inspection detects invalid usages of type hints. + + \ No newline at end of file diff --git a/python/src/META-INF/python-core-common.xml b/python/src/META-INF/python-core-common.xml index 02dbacbf2c83..7b1180d154a7 100644 --- a/python/src/META-INF/python-core-common.xml +++ b/python/src/META-INF/python-core-common.xml @@ -424,6 +424,7 @@ + diff --git a/python/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java b/python/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java index 7d50c0ddae3d..c5f9864f1b98 100644 --- a/python/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java +++ b/python/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java @@ -82,7 +82,7 @@ public class PyTypingTypeProvider extends PyTypeProviderBase { private static final String DEQUE = "typing.Deque"; private static final String TUPLE = "typing.Tuple"; private static final String CLASS_VAR = "typing.ClassVar"; - private static final String TYPE_VAR = "typing.TypeVar"; + public static final String TYPE_VAR = "typing.TypeVar"; private static final String CHAIN_MAP = "typing.ChainMap"; private static final String UNION = "typing.Union"; private static final String OPTIONAL = "typing.Optional"; diff --git a/python/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt b/python/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt new file mode 100644 index 000000000000..09671c1f854d --- /dev/null +++ b/python/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt @@ -0,0 +1,82 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.jetbrains.python.inspections + +import com.intellij.codeInsight.controlflow.ControlFlowUtil +import com.intellij.codeInspection.LocalInspectionToolSession +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.PsiElementVisitor +import com.intellij.psi.util.QualifiedName +import com.jetbrains.python.codeInsight.controlflow.ControlFlowCache +import com.jetbrains.python.codeInsight.controlflow.ReadWriteInstruction +import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil +import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider +import com.jetbrains.python.psi.* +import com.jetbrains.python.psi.resolve.PyResolveUtil + +class PyTypeHintsInspection : PyInspection() { + + override fun buildVisitor(holder: ProblemsHolder, + isOnTheFly: Boolean, + session: LocalInspectionToolSession): PsiElementVisitor = Visitor(holder, session) + + private class Visitor(holder: ProblemsHolder, session: LocalInspectionToolSession) : PyInspectionVisitor(holder, session) { + + override fun visitPyCallExpression(node: PyCallExpression?) { + super.visitPyCallExpression(node) + + if (node != null) { + val callee = node.callee as? PyReferenceExpression + if (callee != null && + QualifiedName.fromDottedString(PyTypingTypeProvider.TYPE_VAR) in PyResolveUtil.resolveImportedElementQNameLocally(callee)) { + val target = (node.parent as? PyAssignmentStatement)?.targetsToValuesMapping?.firstOrNull { it.second == node }?.first + + checkTypeVarPlacement(node, target) + checkTypeVarRedefinition(target) + + if (target != null) { + checkTypeVarName(node, target) + } + } + } + } + + private fun checkTypeVarPlacement(call: PyCallExpression, target: PyExpression?) { + if (target == null) { + registerProblem(call, "A 'TypeVar()' expression must always directly be assigned to a variable") + } + } + + private fun checkTypeVarName(call: PyCallExpression, target: PyExpression) { + val name = call.getArgument(0, "name", PyStringLiteralExpression::class.java) + + if (name != null && name.stringValue != target.name) { + registerProblem(name, "The argument to 'TypeVar()' must be a string equal to the variable name to which it is assigned") + } + } + + private fun checkTypeVarRedefinition(target: PyExpression?) { + val scopeOwner = ScopeUtil.getScopeOwner(target) ?: return + val name = target?.name ?: return + + val instructions = ControlFlowCache.getControlFlow(scopeOwner).instructions + val startInstruction = ControlFlowUtil.findInstructionNumberByElement(instructions, target) + + ControlFlowUtil.iteratePrev( + startInstruction, + instructions, + { instruction -> + if (instruction is ReadWriteInstruction && + instruction.num() != startInstruction && + name == instruction.name && + instruction.access.isWriteAccess) { + registerProblem(target, "Type variables must not be redefined") + ControlFlowUtil.Operation.BREAK + } + else { + ControlFlowUtil.Operation.NEXT + } + } + ) + } + } +} \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java new file mode 100644 index 000000000000..255a14666bd1 --- /dev/null +++ b/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java @@ -0,0 +1,44 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.jetbrains.python.inspections; + +import com.jetbrains.python.fixtures.PyInspectionTestCase; +import com.jetbrains.python.psi.LanguageLevel; +import org.jetbrains.annotations.NotNull; + +public class PyTypeHintsInspectionTest extends PyInspectionTestCase { + + // PY-28243 + public void testTypeVarAndTargetName() { + doTestByText("from typing import TypeVar\n" + + "\n" + + "T0 = TypeVar('T0')\n" + + "T1 = TypeVar('T2')"); + } + + // PY-28243 + public void testTypeVarPlacement() { + runWithLanguageLevel( + LanguageLevel.PYTHON36, + () -> doTestByText("from typing import List, TypeVar\n" + + "\n" + + "T0 = TypeVar('T0')\n" + + "a: List[T0]\n" + + "b: List[TypeVar('T1')]") + ); + } + + // PY-28243 + public void testTypeVarRedefinition() { + doTestByText("from typing import TypeVar\n" + + "\n" + + "T0 = TypeVar('T0')\n" + + "print(T0)\n" + + "T0 = TypeVar('T0')"); + } + + @NotNull + @Override + protected Class getInspectionClass() { + return PyTypeHintsInspection.class; + } +}