From 24079c336df0ea186ebd16eb7f950e7449aa3d30 Mon Sep 17 00:00:00 2001 From: "evgeny.bovykin" Date: Tue, 20 May 2025 12:03:48 +0200 Subject: [PATCH] PY-76851 Introduce PyTypeAliasRedeclarationInspection Similar to PyRedeclarationInspection but specifically for type aliases. The difference is that for type aliases, we should report them as redelclarations even if they were used before GitOrigin-RevId: 735c7be53bf7625cf0dccb83d8a3157665f65a96 --- .../PyTypeAliasRedeclarationInspection.html | 10 ++ .../resources/intellij.python.psi.impl.xml | 1 + .../resources/messages/PyPsiBundle.properties | 4 + .../PyTypeAliasRedeclarationInspection.kt | 95 +++++++++++++++++++ .../redeclaredTopLevel.py | 2 +- .../typeAliasRedeclaration.py | 6 ++ .../PyTypeAliasRedeclarationInspectionTest.kt | 29 ++++++ .../inspections/PyTypingConformanceTest.kt | 1 + 8 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 python/python-psi-impl/resources/inspectionDescriptions/PyTypeAliasRedeclarationInspection.html create mode 100644 python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeAliasRedeclarationInspection.kt create mode 100644 python/testData/inspections/PyTypeAliasRedeclarationInspection/typeAliasRedeclaration.py create mode 100644 python/testSrc/com/jetbrains/python/inspections/PyTypeAliasRedeclarationInspectionTest.kt diff --git a/python/python-psi-impl/resources/inspectionDescriptions/PyTypeAliasRedeclarationInspection.html b/python/python-psi-impl/resources/inspectionDescriptions/PyTypeAliasRedeclarationInspection.html new file mode 100644 index 000000000000..a81b16744bbe --- /dev/null +++ b/python/python-psi-impl/resources/inspectionDescriptions/PyTypeAliasRedeclarationInspection.html @@ -0,0 +1,10 @@ + + +

Reports redeclarations of type aliases.

+

Example:

+

+type A = int
+type A = str
+
+ + diff --git a/python/python-psi-impl/resources/intellij.python.psi.impl.xml b/python/python-psi-impl/resources/intellij.python.psi.impl.xml index 6b2cbe9f9e5d..80092c77feb9 100644 --- a/python/python-psi-impl/resources/intellij.python.psi.impl.xml +++ b/python/python-psi-impl/resources/intellij.python.psi.impl.xml @@ -205,6 +205,7 @@ + diff --git a/python/python-psi-impl/resources/messages/PyPsiBundle.properties b/python/python-psi-impl/resources/messages/PyPsiBundle.properties index b4b281cb7a5e..c66d14466b89 100644 --- a/python/python-psi-impl/resources/messages/PyPsiBundle.properties +++ b/python/python-psi-impl/resources/messages/PyPsiBundle.properties @@ -594,6 +594,10 @@ INSP.decorator.receives.unexpected.builtin=This decorator will not receive the c INSP.NAME.redeclaration=Redeclared names without usages INSP.redeclared.name=Redeclared ''{0}'' defined above without usage +# PyTypeAliasRedeclarationInspection +INSP.NAME.type.alias.redeclaration=Redeclared type alias +INSP.redeclared.type.alias=Name ''{0}'' already defined + # PyInterpreterInspection INSP.NAME.invalid.interpreter=An invalid interpreter INSP.interpreter.pipenv.interpreter.associated.with.another.project=Pipenv interpreter is associated with another project: ''{0}'' diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeAliasRedeclarationInspection.kt b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeAliasRedeclarationInspection.kt new file mode 100644 index 000000000000..56150f3583a1 --- /dev/null +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeAliasRedeclarationInspection.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jetbrains.python.inspections + +import com.intellij.codeInsight.controlflow.ControlFlowUtil +import com.intellij.codeInsight.controlflow.Instruction +import com.intellij.codeInspection.LocalInspectionToolSession +import com.intellij.codeInspection.LocalQuickFix +import com.intellij.codeInspection.ProblemHighlightType +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiElementVisitor +import com.intellij.psi.PsiNameIdentifierOwner +import com.jetbrains.python.PyPsiBundle +import com.jetbrains.python.PythonUiService +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.psi.PyTypeAliasStatement +import com.jetbrains.python.psi.types.TypeEvalContext + +/** + * Annotates type alias re-declarations + */ +class PyTypeAliasRedeclarationInspection : PyInspection() { + override fun buildVisitor( + holder: ProblemsHolder, + isOnTheFly: Boolean, + session: LocalInspectionToolSession + ): PsiElementVisitor { + return Visitor(holder, PyInspectionVisitor.getContext(session)) + } + + private class Visitor(holder: ProblemsHolder?, context: TypeEvalContext) : PyInspectionVisitor(holder, context) { + override fun visitPyTypeAliasStatement(node: PyTypeAliasStatement) { + reportRedeclaration(node) + } + + fun reportRedeclaration(element: PsiNameIdentifierOwner) { + val name = element.getName() + var writeElement: PsiElement? = null + if (name != null) { + val owner = ScopeUtil.getScopeOwner(element) + if (owner != null) { + val instructions = ControlFlowCache.getControlFlow(owner).getInstructions() + val startInstruction = ControlFlowUtil.findInstructionNumberByElement(instructions, element) + if (startInstruction >= 0) { + ControlFlowUtil.iteratePrev(startInstruction, instructions) { instruction: Instruction? -> + if (instruction is ReadWriteInstruction && instruction.num() != startInstruction) { + if (name == instruction.name) { + val originalElement = instruction.element + if (originalElement != null) { + if (instruction.access.isWriteAccess && originalElement !== element) { + writeElement = originalElement + return@iteratePrev ControlFlowUtil.Operation.BREAK + } + } + } + } + ControlFlowUtil.Operation.NEXT + } + } + } + } + + if (writeElement == null) { + return + } + val quickFixes: MutableList = ArrayList() + val quickFix = PythonUiService.getInstance().createPyRenameElementQuickFix(element) + if (quickFix != null) { + quickFixes.add(quickFix) + } + val identifier = element.getNameIdentifier() + registerProblem(identifier ?: element, + PyPsiBundle.message("INSP.redeclared.type.alias", name), + ProblemHighlightType.GENERIC_ERROR_OR_WARNING, + null, + *quickFixes.toTypedArray()) + } + } +} diff --git a/python/testData/inspections/PyRedeclarationInspection/redeclaredTopLevel.py b/python/testData/inspections/PyRedeclarationInspection/redeclaredTopLevel.py index c5f73954cc89..f251bb5daf78 100644 --- a/python/testData/inspections/PyRedeclarationInspection/redeclaredTopLevel.py +++ b/python/testData/inspections/PyRedeclarationInspection/redeclaredTopLevel.py @@ -7,4 +7,4 @@ def TopLevelBoo(): class TopLevelBoo: - pass \ No newline at end of file + pass diff --git a/python/testData/inspections/PyTypeAliasRedeclarationInspection/typeAliasRedeclaration.py b/python/testData/inspections/PyTypeAliasRedeclarationInspection/typeAliasRedeclaration.py new file mode 100644 index 000000000000..695d4c9000d7 --- /dev/null +++ b/python/testData/inspections/PyTypeAliasRedeclarationInspection/typeAliasRedeclaration.py @@ -0,0 +1,6 @@ +type UnusedAlias = str +type UnusedAlias = str + +type UsedAlias = int +print(UsedAlias) +type UsedAlias = int \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/inspections/PyTypeAliasRedeclarationInspectionTest.kt b/python/testSrc/com/jetbrains/python/inspections/PyTypeAliasRedeclarationInspectionTest.kt new file mode 100644 index 000000000000..0d322c7922c6 --- /dev/null +++ b/python/testSrc/com/jetbrains/python/inspections/PyTypeAliasRedeclarationInspectionTest.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jetbrains.python.inspections + +import com.jetbrains.python.fixtures.PyInspectionTestCase + +class PyTypeAliasRedeclarationInspectionTest : PyInspectionTestCase() { + // PY-76851 + fun testTypeAliasRedeclaration() { + doTest() + } + + override fun getInspectionClass(): Class { + return PyTypeAliasRedeclarationInspection::class.java + } +} diff --git a/python/testSrc/com/jetbrains/python/inspections/PyTypingConformanceTest.kt b/python/testSrc/com/jetbrains/python/inspections/PyTypingConformanceTest.kt index 5c990067b5af..77038f9e0814 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyTypingConformanceTest.kt +++ b/python/testSrc/com/jetbrains/python/inspections/PyTypingConformanceTest.kt @@ -42,6 +42,7 @@ private val inspections PyTypeCheckerInspection(), PyTypeHintsInspection(), PyUnresolvedReferencesInspection(), + PyTypeAliasRedeclarationInspection(), ) @RunWith(Parameterized::class)