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
This commit is contained in:
evgeny.bovykin
2025-07-28 15:56:48 +00:00
committed by intellij-monorepo-bot
parent 52e44e5541
commit 24079c336d
8 changed files with 147 additions and 1 deletions
@@ -0,0 +1,10 @@
<html>
<body>
<p>Reports redeclarations of type aliases.</p>
<p><b>Example:</b></p>
<pre><code>
type A = int
type A = str
</code></pre>
</body>
</html>
@@ -205,6 +205,7 @@
<localInspection language="Python" shortName="PyNamedTupleInspection" suppressId="PyNamedTuple" bundle="messages.PyPsiBundle" key="INSP.named.tuple" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.inspections.PyNamedTupleInspection"/>
<localInspection language="Python" shortName="PyArgumentListInspection" suppressId="PyArgumentList" bundle="messages.PyPsiBundle" key="INSP.NAME.incorrect.call.arguments" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.inspections.PyArgumentListInspection"/>
<localInspection language="Python" shortName="PyRedeclarationInspection" suppressId="PyRedeclaration" bundle="messages.PyPsiBundle" key="INSP.NAME.redeclaration" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.inspections.PyRedeclarationInspection"/>
<localInspection language="Python" shortName="PyTypeAliasRedeclarationInspection" suppressId="PyTypeAliasRedeclaration" bundle="messages.PyPsiBundle" key="INSP.NAME.type.alias.redeclaration" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.inspections.PyTypeAliasRedeclarationInspection"/>
<localInspection language="Python" shortName="PyMethodParametersInspection" suppressId="PyMethodParameters" bundle="messages.PyPsiBundle" key="INSP.NAME.problematic.first.parameter" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WEAK WARNING" implementationClass="com.jetbrains.python.inspections.PyMethodParametersInspection"/>
<localInspection language="Python" shortName="PyUnreachableCodeInspection" suppressId="PyUnreachableCode" bundle="messages.PyPsiBundle" key="INSP.NAME.unreachable.code" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.inspections.PyUnreachableCodeInspection"/>
<localInspection language="Python" shortName="PyMethodFirstArgAssignmentInspection" suppressId="PyMethodFirstArgAssignment" bundle="messages.PyPsiBundle" key="INSP.NAME.first.arg.assign" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.inspections.PyMethodFirstArgAssignmentInspection"/>
@@ -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}''
@@ -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<LocalQuickFix> = 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())
}
}
}
@@ -7,4 +7,4 @@ def TopLevelBoo():
class <warning descr="Redeclared 'TopLevelBoo' defined above without usage">TopLevelBoo</warning>:
pass
pass
@@ -0,0 +1,6 @@
type UnusedAlias = str
type <warning descr="Name 'UnusedAlias' already defined">UnusedAlias</warning> = str
type UsedAlias = int
print(UsedAlias)
type <warning descr="Name 'UsedAlias' already defined">UsedAlias</warning> = int
@@ -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<out PyInspection?> {
return PyTypeAliasRedeclarationInspection::class.java
}
}
@@ -42,6 +42,7 @@ private val inspections
PyTypeCheckerInspection(),
PyTypeHintsInspection(),
PyUnresolvedReferencesInspection(),
PyTypeAliasRedeclarationInspection(),
)
@RunWith(Parameterized::class)