Skip to content

Commit 899af47

Browse files
SONARPY-2064 Propagate imported names from import from (#1911)
1 parent 3d0deee commit 899af47

File tree

8 files changed

+87
-9
lines changed

8 files changed

+87
-9
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def member_access():
7676
def types_from_typeshed(foo):
7777
from math import acos
7878
from functools import wraps
79-
acos(42)() # FN: declared return type of Typeshed
79+
acos(42)() # Noncompliant
8080
wraps(func)(foo) # OK, wraps returns a Callable
8181

8282
def with_metaclass():

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ public class SymbolV2Utils {
2323

2424
private SymbolV2Utils() {}
2525

26-
public static boolean isFunctionOrClassDeclaration(UsageV2 usageV2) {
27-
return usageV2.kind() == UsageV2.Kind.FUNC_DECLARATION || usageV2.kind() == UsageV2.Kind.CLASS_DECLARATION;
26+
public static boolean isDeclaration(UsageV2 usageV2) {
27+
return usageV2.kind() == UsageV2.Kind.FUNC_DECLARATION
28+
|| usageV2.kind() == UsageV2.Kind.CLASS_DECLARATION
29+
|| usageV2.kind() == UsageV2.Kind.IMPORT;
2830
}
2931
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.sonar.plugins.python.api.tree.Expression;
3030
import org.sonar.plugins.python.api.tree.ForStatement;
3131
import org.sonar.plugins.python.api.tree.FunctionDef;
32+
import org.sonar.plugins.python.api.tree.ImportFrom;
3233
import org.sonar.plugins.python.api.tree.ImportName;
3334
import org.sonar.plugins.python.api.tree.Name;
3435
import org.sonar.plugins.python.api.tree.Parameter;
@@ -93,6 +94,8 @@ public void updateProgramState(Tree element, ProgramState programState) {
9394
handleDefinitions(functionDef, state);
9495
} else if (element instanceof ImportName importName) {
9596
handleDefinitions(importName, state);
97+
} else if (element instanceof ImportFrom importFrom) {
98+
handleDefinitions(importFrom, state);
9699
} else if (element instanceof Parameter parameter) {
97100
handleParameter(parameter, state);
98101
} else if (isForLoopAssignment(element)) {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public static Stream<Tree> getSymbolNonDeclarationUsageTrees(SymbolV2 symbol) {
9292
.stream()
9393
// Function and class definition names will always have FunctionType and ClassType respectively
9494
// so they are filtered out of type propagation
95-
.filter(u -> !SymbolV2Utils.isFunctionOrClassDeclaration(u))
95+
.filter(u -> !SymbolV2Utils.isDeclaration(u))
9696
.map(UsageV2::tree);
9797
}
9898

@@ -111,7 +111,7 @@ Set<Propagation> dependents() {
111111
static PythonType currentType(Name lhsName) {
112112
return lhsName.symbolV2().usages()
113113
.stream()
114-
.filter(u -> !SymbolV2Utils.isFunctionOrClassDeclaration(u))
114+
.filter(u -> !SymbolV2Utils.isDeclaration(u))
115115
.map(UsageV2::tree)
116116
.filter(Expression.class::isInstance)
117117
.map(Expression.class::cast)

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.sonar.plugins.python.api.tree.Expression;
3434
import org.sonar.plugins.python.api.tree.ForStatement;
3535
import org.sonar.plugins.python.api.tree.FunctionDef;
36+
import org.sonar.plugins.python.api.tree.ImportFrom;
3637
import org.sonar.plugins.python.api.tree.ImportName;
3738
import org.sonar.plugins.python.api.tree.Name;
3839
import org.sonar.plugins.python.api.tree.Parameter;
@@ -140,6 +141,22 @@ public void visitImportName(ImportName importName) {
140141
});
141142
}
142143

144+
@Override
145+
public void visitImportFrom(ImportFrom importFrom) {
146+
super.visitImportFrom(importFrom);
147+
importFrom.importedNames()
148+
.forEach(aliasedName -> aliasedName.dottedName().names().forEach(
149+
name -> {
150+
SymbolV2 symbolV2 = name.symbolV2();
151+
if (symbolV2 != null) {
152+
Definition definition = new Definition(symbolV2, name);
153+
definitionsByDefinitionStatement.computeIfAbsent(importFrom, k -> new HashSet<>()).add(definition);
154+
propagationsByLhs.computeIfAbsent(symbolV2, s -> new HashSet<>()).add(definition);
155+
}
156+
}
157+
));
158+
}
159+
143160
@Override
144161
public void visitForStatement(ForStatement forStatement) {
145162
scan(forStatement.testExpressions());

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

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.sonar.plugins.python.api.tree.ExpressionStatement;
2525
import org.sonar.plugins.python.api.tree.FileInput;
2626
import org.sonar.plugins.python.api.tree.FunctionDef;
27+
import org.sonar.plugins.python.api.tree.ImportName;
2728
import org.sonar.plugins.python.api.tree.Name;
2829

2930
import static org.assertj.core.api.Assertions.assertThat;
@@ -38,22 +39,32 @@ def foo(): ...
3839
foo
3940
class MyClass: ...
4041
MyClass
42+
import bar
43+
bar
4144
""");
4245
new SymbolTableBuilderV2(fileInput).build();
4346

4447
FunctionDef functionDef = (FunctionDef) fileInput.statements().statements().get(0);
4548
Name fooName = (Name) ((ExpressionStatement) fileInput.statements().statements().get(1)).expressions().get(0);
4649
ClassDef classDef = (ClassDef) fileInput.statements().statements().get(2);
4750
Name myClassName = (Name) ((ExpressionStatement) fileInput.statements().statements().get(3)).expressions().get(0);
51+
ImportName importName = (ImportName) fileInput.statements().statements().get(4);
52+
Name barName = (Name) ((ExpressionStatement) fileInput.statements().statements().get(5)).expressions().get(0);
4853

4954
UsageV2 functionDefUsage = functionDef.name().symbolV2().usages().stream().filter(u -> functionDef.name().equals(u.tree())).findFirst().get();
5055
UsageV2 nameUsage = fooName.symbolV2().usages().stream().filter(u -> fooName.equals(u.tree())).findFirst().get();
5156
UsageV2 classDefUsage = classDef.name().symbolV2().usages().stream().filter(u -> classDef.name().equals(u.tree())).findFirst().get();
5257
UsageV2 myClassaNameUsage = myClassName.symbolV2().usages().stream().filter(u -> myClassName.equals(u.tree())).findFirst().get();
58+
UsageV2 barImportUsage = importName.modules().get(0).dottedName().names().get(0).symbolV2().usages()
59+
.stream().filter(u -> importName.modules().get(0).dottedName().names().get(0).equals(u.tree())).findFirst().get();
60+
UsageV2 barNameUsage = barName.symbolV2().usages().stream().filter(u -> barName.equals(u.tree())).findFirst().get();
5361

54-
assertThat(SymbolV2Utils.isFunctionOrClassDeclaration(functionDefUsage)).isTrue();
55-
assertThat(SymbolV2Utils.isFunctionOrClassDeclaration(nameUsage)).isFalse();
56-
assertThat(SymbolV2Utils.isFunctionOrClassDeclaration(classDefUsage)).isTrue();
57-
assertThat(SymbolV2Utils.isFunctionOrClassDeclaration(myClassaNameUsage)).isFalse();
62+
63+
assertThat(SymbolV2Utils.isDeclaration(functionDefUsage)).isTrue();
64+
assertThat(SymbolV2Utils.isDeclaration(nameUsage)).isFalse();
65+
assertThat(SymbolV2Utils.isDeclaration(classDefUsage)).isTrue();
66+
assertThat(SymbolV2Utils.isDeclaration(myClassaNameUsage)).isFalse();
67+
assertThat(SymbolV2Utils.isDeclaration(barImportUsage)).isTrue();
68+
assertThat(SymbolV2Utils.isDeclaration(barNameUsage)).isFalse();
5869
}
5970
}

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
import static org.sonar.python.PythonTestUtils.parseWithoutSymbols;
7272
import static org.sonar.python.PythonTestUtils.pythonFile;
7373
import static org.sonar.python.types.v2.TypesTestUtils.DICT_TYPE;
74+
import static org.sonar.python.types.v2.TypesTestUtils.FLOAT_TYPE;
7475
import static org.sonar.python.types.v2.TypesTestUtils.FROZENSET_TYPE;
7576
import static org.sonar.python.types.v2.TypesTestUtils.INT_TYPE;
7677
import static org.sonar.python.types.v2.TypesTestUtils.LIST_TYPE;
@@ -1858,6 +1859,49 @@ void return_type_of_call_expression_2() {
18581859
).typeV2().unwrappedType()).isEqualTo(NONE_TYPE);
18591860
}
18601861

1862+
@Test
1863+
void imported_ambiguous_symbol() {
1864+
FileInput fileInput = inferTypes("""
1865+
from math import acos, atan
1866+
acos
1867+
atan
1868+
""");
1869+
UnionType acosType = (UnionType) ((ExpressionStatement) fileInput.statements().statements().get(1)).expressions().get(0).typeV2();
1870+
assertThat(acosType.candidates()).allMatch(p -> p instanceof FunctionType);
1871+
assertThat(acosType.candidates()).extracting(PythonType::name).containsExactly("acos", "acos");
1872+
assertThat(acosType.candidates()).map(FunctionType.class::cast).extracting(FunctionType::returnType).containsExactly(FLOAT_TYPE, FLOAT_TYPE);
1873+
1874+
UnionType atanType = (UnionType) ((ExpressionStatement) fileInput.statements().statements().get(2)).expressions().get(0).typeV2();
1875+
assertThat(atanType.candidates()).allMatch(p -> p instanceof FunctionType);
1876+
assertThat(atanType.candidates()).extracting(PythonType::name).containsExactly("atan", "atan");
1877+
assertThat(atanType.candidates()).map(FunctionType.class::cast).extracting(FunctionType::returnType).containsExactly(FLOAT_TYPE, FLOAT_TYPE);
1878+
}
1879+
1880+
@Test
1881+
void imported_ambiguous_symbol_try_except() {
1882+
FileInput fileInput = inferTypes("""
1883+
try:
1884+
from math import acos
1885+
acos
1886+
except:
1887+
...
1888+
acos
1889+
""");
1890+
Expression acosExpr1 = TreeUtils.firstChild(fileInput.statements().statements().get(0), ExpressionStatement.class::isInstance)
1891+
.map(ExpressionStatement.class::cast)
1892+
.map(ExpressionStatement::expressions)
1893+
.map(expressions -> expressions.get(0))
1894+
.map(Expression.class::cast)
1895+
.get();
1896+
UnionType acosType1 = (UnionType) acosExpr1.typeV2();
1897+
assertThat(acosType1.candidates()).allMatch(p -> p instanceof FunctionType);
1898+
assertThat(acosType1.candidates()).map(FunctionType.class::cast).extracting(FunctionType::returnType).containsExactly(FLOAT_TYPE, FLOAT_TYPE);
1899+
1900+
UnionType acosType2 = (UnionType) ((ExpressionStatement) fileInput.statements().statements().get(1)).expressions().get(0).typeV2();
1901+
assertThat(acosType2.candidates()).allMatch(p -> p instanceof FunctionType);
1902+
assertThat(acosType2.candidates()).map(FunctionType.class::cast).extracting(FunctionType::returnType).containsExactly(FLOAT_TYPE, FLOAT_TYPE);
1903+
}
1904+
18611905
@Test
18621906
void return_type_of_call_of_locally_defined_function() {
18631907
var type = lastExpression("""

python-frontend/src/test/java/org/sonar/python/types/v2/TypesTestUtils.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public class TypesTestUtils {
3333
public static final ModuleType BUILTINS = PROJECT_LEVEL_TYPE_TABLE.getBuiltinsModule();
3434

3535
public static final PythonType INT_TYPE = BUILTINS.resolveMember("int").get();
36+
public static final PythonType FLOAT_TYPE = BUILTINS.resolveMember("float").get();
3637
public static final PythonType BOOL_TYPE = BUILTINS.resolveMember("bool").get();
3738
public static final PythonType STR_TYPE = BUILTINS.resolveMember("str").get();
3839
public static final PythonType LIST_TYPE = BUILTINS.resolveMember("list").get();

0 commit comments

Comments
 (0)