Skip to content

Commit 57993cf

Browse files
sonar-nigel[bot]Vibe Bot
authored andcommitted
SONARPY-3505 Fix S5953 false positives on PEP 695 new generics syntax (#984)
Co-authored-by: Vibe Bot <vibe-bot@sonarsource.com> GitOrigin-RevId: cf8686af965e1276e1ee6967c2fa5f0878a51a7a
1 parent f1d98b4 commit 57993cf

File tree

8 files changed

+97
-7
lines changed

8 files changed

+97
-7
lines changed

python-checks/src/main/java/org/sonar/python/checks/UndefinedSymbolsCheck.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@
3131
import org.sonar.plugins.python.api.tree.ImportFrom;
3232
import org.sonar.plugins.python.api.tree.Name;
3333
import org.sonar.plugins.python.api.tree.Tree;
34+
import org.sonar.plugins.python.api.tree.TypeAliasStatement;
3435
import org.sonar.plugins.python.api.tree.TypeParams;
36+
import org.sonar.python.semantic.BuiltinSymbols;
3537
import org.sonar.python.tree.TreeUtils;
3638

3739
@Rule(key = "S5953")
@@ -72,19 +74,22 @@ private static class UnresolvedSymbolsVisitor extends BaseTreeVisitor {
7274

7375
@Override
7476
public void visitName(Name name) {
75-
if (name.isVariable() && name.symbol() == null && !name.name().startsWith("_") && !isTypeVar(name)) {
77+
if (name.isVariable() && name.symbolV2() == null && !BuiltinSymbols.all().contains(name.name()) && !name.name().startsWith("_") && !isTypeVar(name)) {
7678
nameIssues.computeIfAbsent(name.name(), k -> new ArrayList<>()).add(name);
7779
}
7880
}
7981

8082
private static boolean isTypeVar(Name name) {
81-
return TreeUtils.firstAncestor(name, tree -> classWithTypeVar(tree, name)) != null;
83+
return TreeUtils.firstAncestor(name, tree -> hasTypeVarInScope(tree, name)) != null;
8284
}
8385

84-
private static boolean classWithTypeVar(Tree tree, Name name) {
86+
private static boolean hasTypeVarInScope(Tree tree, Name name) {
8587
if (tree instanceof ClassDef classDef) {
8688
return hasTypeVar(classDef.typeParams(), name);
8789
}
90+
if (tree instanceof TypeAliasStatement typeAliasStatement) {
91+
return hasTypeVar(typeAliasStatement.typeParams(), name);
92+
}
8893
return false;
8994
}
9095

python-checks/src/test/resources/checks/genericTypeStatement.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,11 @@ def non_compliant_1():
2020
type MyAlias[T] = dict[T, _R] # Noncompliant
2121
# ^^^^^^^ ^^< {{Use of "TypeVar" here.}}
2222

23-
M = TypeVar("M")
24-
# ^^^^^^^^^^^^> {{"TypeVar" is assigned here.}}
25-
type MyAlias[M] = set[M] # Noncompliant
26-
# ^^^^^^^ ^< {{Use of "TypeVar" here.}}
2723

2824

2925
def compliant(AType):
3026
type MyAlias[T] = dict[T, str]
3127
type MyAlias = set[str]
3228
type MyAlias = set[AType]
29+
M = TypeVar("M")
30+
type MyAlias[M] = set[M] # Compliant: M in set[M] refers to the type parameter, not the TypeVar

python-checks/src/test/resources/checks/undefinedSymbols/undefinedSymbols.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,30 @@ def from_class_def(self):
163163
def unknow_generic(self):
164164
a: list[X] = ... # Noncompliant
165165

166+
class GenericClassBuiltinBound[ItemType, ItemTypeId: str]:
167+
pass
168+
169+
class BaseBound:
170+
pass
171+
172+
class GenericClassUserDefinedBound[FooT: BaseBound]:
173+
pass
174+
175+
class UserBase:
176+
pass
177+
178+
class UserGeneric[UBased: UserBase]:
179+
def __init__(self, value: UBased) -> None:
180+
self.value = value
181+
182+
from collections.abc import Callable
183+
type Validator[V] = Callable[[V], V]
184+
185+
class Serializable:
186+
pass
187+
188+
type SerializableList[S: Serializable] = list[S]
189+
166190
# FP with python 2 syntax for exception handling
167191
try:
168192
""

python-frontend/src/main/java/org/sonar/python/semantic/SymbolTableBuilder.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,7 @@ private void createTypeParameters(@Nullable TypeParams typeParams) {
342342
@Override
343343
public void visitTypeAliasStatement(TypeAliasStatement typeAliasStatement) {
344344
addBindingUsage(typeAliasStatement.name(), Usage.Kind.TYPE_ALIAS_DECLARATION);
345+
createTypeParameters(typeAliasStatement.typeParams());
345346
super.visitTypeAliasStatement(typeAliasStatement);
346347
}
347348

@@ -697,6 +698,7 @@ public void visitTypeAnnotation(TypeAnnotation tree) {
697698
public void visitClassDef(ClassDef pyClassDefTree) {
698699
scan(pyClassDefTree.args());
699700
scan(pyClassDefTree.decorators());
701+
scan(pyClassDefTree.typeParams());
700702
enterScope(pyClassDefTree);
701703
scan(pyClassDefTree.name());
702704
resolveTypeHierarchy(pyClassDefTree, pyClassDefTree.name().symbol(), pythonFile, scopesByRootTree.get(fileInput).symbolsByName);

python-frontend/src/main/java/org/sonar/python/semantic/v2/ReadUsagesVisitor.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ public void visitTypeAnnotation(TypeAnnotation tree) {
121121
public void visitClassDef(ClassDef classDef) {
122122
scan(classDef.args());
123123
scan(classDef.decorators());
124+
scan(classDef.typeParams());
124125
enterScope(classDef);
125126
scan(classDef.name());
126127
scan(classDef.body());

python-frontend/src/main/java/org/sonar/python/semantic/v2/WriteUsagesVisitor.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ private void addTupleParamElementsToBindingUsage(TupleParameter param) {
171171
@Override
172172
public void visitTypeAliasStatement(TypeAliasStatement typeAliasStatement) {
173173
currentScope().addBindingUsage(typeAliasStatement.name(), UsageV2.Kind.TYPE_ALIAS_DECLARATION);
174+
createTypeParameters(typeAliasStatement.typeParams());
174175
super.visitTypeAliasStatement(typeAliasStatement);
175176
}
176177

python-frontend/src/test/java/org/sonar/python/semantic/SymbolTableBuilderTest.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.sonar.plugins.python.api.symbols.Usage;
3636
import org.sonar.plugins.python.api.tree.BaseTreeVisitor;
3737
import org.sonar.plugins.python.api.tree.CallExpression;
38+
import org.sonar.plugins.python.api.tree.ClassDef;
3839
import org.sonar.plugins.python.api.tree.ComprehensionExpression;
3940
import org.sonar.plugins.python.api.tree.DictCompExpression;
4041
import org.sonar.plugins.python.api.tree.ExpressionStatement;
@@ -44,6 +45,7 @@
4445
import org.sonar.plugins.python.api.tree.Name;
4546
import org.sonar.plugins.python.api.tree.Tree;
4647
import org.sonar.plugins.python.api.tree.Tuple;
48+
import org.sonar.plugins.python.api.tree.TypeAliasStatement;
4749
import org.sonar.python.PythonTestUtils;
4850
import org.sonar.python.TestPythonVisitorRunner;
4951
import org.sonar.python.tree.TreeUtils;
@@ -636,6 +638,33 @@ void type_alias_declaration() {
636638
assertThat(symbolByName.get("M").usages().get(0).kind()).isEqualTo(Usage.Kind.TYPE_ALIAS_DECLARATION);
637639
}
638640

641+
@Test
642+
void class_type_param_bound_resolved_in_enclosing_scope() {
643+
FileInput tree = PythonTestUtils.parse(
644+
"class BaseBound:",
645+
" pass",
646+
"class Foo[T: BaseBound]:",
647+
" pass"
648+
);
649+
ClassDef fooClass = (ClassDef) PythonTestUtils.getLastDescendant(tree, t -> t.is(Tree.Kind.CLASSDEF));
650+
Name boundName = (Name) fooClass.typeParams().typeParamsList().get(0).typeAnnotation().expression();
651+
assertThat(boundName.name()).isEqualTo("BaseBound");
652+
assertThat(boundName.symbol()).isNotNull();
653+
assertThat(boundName.symbol().name()).isEqualTo("BaseBound");
654+
}
655+
656+
@Test
657+
void type_alias_type_params_registered() {
658+
FileInput tree = PythonTestUtils.parse("type Alias[T] = list[T]");
659+
TypeAliasStatement typeAlias = (TypeAliasStatement) tree.statements().statements().get(0);
660+
Name typeParamName = typeAlias.typeParams().typeParamsList().get(0).name();
661+
assertThat(typeParamName.name()).isEqualTo("T");
662+
assertThat(typeParamName.symbol()).isNotNull();
663+
assertThat(typeParamName.symbol().usages())
664+
.extracting(Usage::kind)
665+
.contains(Usage.Kind.TYPE_PARAM_DECLARATION);
666+
}
667+
639668
private static class TestVisitor extends BaseTreeVisitor {
640669
@Override
641670
public void visitFunctionDef(FunctionDef pyFunctionDefTree) {

python-frontend/src/test/java/org/sonar/python/semantic/v2/SymbolTableBuilderV2Test.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,15 @@
2222
import org.junit.jupiter.api.Disabled;
2323
import org.junit.jupiter.api.Test;
2424
import org.sonar.plugins.python.api.symbols.v2.SymbolV2;
25+
import org.sonar.plugins.python.api.symbols.v2.UsageV2;
2526
import org.sonar.plugins.python.api.tree.AssignmentStatement;
27+
import org.sonar.plugins.python.api.tree.ClassDef;
2628
import org.sonar.plugins.python.api.tree.FileInput;
2729
import org.sonar.plugins.python.api.tree.FunctionDef;
2830
import org.sonar.plugins.python.api.tree.ImportName;
2931
import org.sonar.plugins.python.api.tree.Name;
3032
import org.sonar.plugins.python.api.tree.Tree;
33+
import org.sonar.plugins.python.api.tree.TypeAliasStatement;
3134
import org.sonar.python.PythonTestUtils;
3235
import org.sonar.python.tree.TreeUtils;
3336

@@ -260,6 +263,33 @@ void never_written_variables_have_symbol() {
260263
}
261264

262265

266+
@Test
267+
void class_type_param_bound_is_resolved_in_enclosing_scope() {
268+
FileInput fileInput = PythonTestUtils.parse("""
269+
class BaseBound:
270+
pass
271+
class Foo[T: BaseBound]:
272+
pass
273+
""");
274+
new SymbolTableBuilderV2(fileInput).build();
275+
ClassDef fooClass = (ClassDef) fileInput.statements().statements().get(1);
276+
Name boundName = (Name) fooClass.typeParams().typeParamsList().get(0).typeAnnotation().expression();
277+
Assertions.assertThat(boundName.symbolV2()).isNotNull();
278+
Assertions.assertThat(boundName.symbolV2().name()).isEqualTo("BaseBound");
279+
}
280+
281+
@Test
282+
void type_alias_type_params_are_registered() {
283+
FileInput fileInput = PythonTestUtils.parse("type Alias[T] = list[T]");
284+
new SymbolTableBuilderV2(fileInput).build();
285+
TypeAliasStatement typeAlias = (TypeAliasStatement) fileInput.statements().statements().get(0);
286+
Name typeParamName = typeAlias.typeParams().typeParamsList().get(0).name();
287+
Assertions.assertThat(typeParamName.symbolV2()).isNotNull();
288+
Assertions.assertThat(typeParamName.symbolV2().usages())
289+
.extracting(UsageV2::kind)
290+
.contains(UsageV2.Kind.TYPE_PARAM_DECLARATION);
291+
}
292+
263293
private static void assertNameSymbol(Name name, String expectedSymbolName, int expectedUsagesCount) {
264294
var symbol = name.symbolV2();
265295
Assertions.assertThat(symbol).isNotNull();

0 commit comments

Comments
 (0)