Skip to content

Commit cb812de

Browse files
SONARPY-1758 Create basic flow insensitive engine to populate the new type model
1 parent 9a7397e commit cb812de

23 files changed

+505
-29
lines changed

python-frontend/src/main/java/org/sonar/plugins/python/api/tree/Expression.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ default InferredType type() {
3232
}
3333

3434
@Beta
35-
default PythonType pythonType() {
35+
default PythonType typeV2() {
3636
return PythonType.UNKNOWN;
3737
}
3838

python-frontend/src/main/java/org/sonar/plugins/python/api/tree/QualifiedExpression.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ default Usage usage() {
5959
}
6060

6161
@Override
62-
default PythonType pythonType() {
63-
return name().pythonType();
62+
default PythonType typeV2() {
63+
return name().typeV2();
6464
}
6565
}
Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,105 @@
1-
package org.sonar.python.semantic.v2;public class ProjectLevelTypeTable {
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2024 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonar.python.semantic.v2;
21+
22+
import java.util.Collections;
23+
import java.util.HashMap;
24+
import java.util.List;
25+
import java.util.Map;
26+
import java.util.Set;
27+
import java.util.function.Function;
28+
import java.util.stream.Collectors;
29+
import org.sonar.plugins.python.api.symbols.AmbiguousSymbol;
30+
import org.sonar.plugins.python.api.symbols.ClassSymbol;
31+
import org.sonar.plugins.python.api.symbols.FunctionSymbol;
32+
import org.sonar.plugins.python.api.symbols.Symbol;
33+
import org.sonar.python.semantic.ProjectLevelSymbolTable;
34+
import org.sonar.python.types.TypeShed;
35+
import org.sonar.python.types.v2.ClassType;
36+
import org.sonar.python.types.v2.FunctionType;
37+
import org.sonar.python.types.v2.Member;
38+
import org.sonar.python.types.v2.ModuleType;
39+
import org.sonar.python.types.v2.PythonType;
40+
import org.sonar.python.types.v2.UnionType;
41+
42+
public class ProjectLevelTypeTable {
43+
44+
private final ProjectLevelSymbolTable projectLevelSymbolTable;
45+
private final Map<String, PythonType> builtinTypes;
46+
private final Map<String, ModuleType> modules;
47+
48+
public ProjectLevelTypeTable(ProjectLevelSymbolTable projectLevelSymbolTable) {
49+
this.projectLevelSymbolTable = projectLevelSymbolTable;
50+
this.modules = new HashMap<>();
51+
Map<String, Symbol> builtinSymbols = TypeShed.builtinSymbols();
52+
this.builtinTypes = new HashMap<>();
53+
builtinSymbols.forEach((key, value) -> builtinTypes.put(key, convertToType(value)));
54+
modules.put("builtins", new ModuleType("builtins", builtinTypes));
55+
}
56+
57+
public ModuleType getModule(String moduleName) {
58+
if (modules.containsKey(moduleName)) {
59+
return modules.get(moduleName);
60+
}
61+
Set<Symbol> symbolsFromModule = projectLevelSymbolTable.getSymbolsFromModule(moduleName);
62+
Map<String, PythonType> children;
63+
if (symbolsFromModule != null) {
64+
children = symbolsFromModule.stream()
65+
.map(this::convertToType)
66+
.collect(Collectors.toMap(PythonType::name, Function.identity(), (a, b) -> PythonType.UNKNOWN));
67+
} else {
68+
// FIXME: what to do here?
69+
children = Collections.emptyMap();
70+
}
71+
72+
ModuleType moduleType = new ModuleType(moduleName, children);
73+
modules.put(moduleName, moduleType);
74+
return moduleType;
75+
}
76+
77+
private PythonType convertToType(Symbol symbol) {
78+
return switch (symbol.kind()) {
79+
case CLASS -> converToClassType((ClassSymbol) symbol);
80+
case FUNCTION -> convertToFunctionType((FunctionSymbol) symbol);
81+
case AMBIGUOUS -> convertToUnionType((AmbiguousSymbol) symbol);
82+
case OTHER -> converToObjectType(symbol);
83+
};
84+
}
85+
86+
private PythonType converToObjectType(Symbol symbol) {
87+
// What should we have here?
88+
return PythonType.UNKNOWN;
89+
}
90+
91+
private PythonType convertToFunctionType(FunctionSymbol symbol) {
92+
return new FunctionType(symbol.name(), List.of(), List.of(), PythonType.UNKNOWN);
93+
}
94+
95+
private PythonType converToClassType(ClassSymbol symbol) {
96+
Set<Member> members = symbol.declaredMembers().stream().map(m -> new Member(m.name(), convertToType(m))).collect(Collectors.toSet());
97+
List<PythonType> superClasses = symbol.superClasses().stream().map(this::convertToType).toList();
98+
return new ClassType(symbol.name(), members, List.of(), superClasses);
99+
}
100+
101+
private PythonType convertToUnionType(AmbiguousSymbol ambiguousSymbol) {
102+
List<PythonType> pythonTypes = ambiguousSymbol.alternatives().stream().map(this::convertToType).toList();
103+
return new UnionType(pythonTypes);
104+
}
2105
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,8 @@ void addUsage(Name name, UsageV2.Kind kind) {
4242
((NameImpl) tree).setUsage(usage);
4343
}*/
4444
}
45+
46+
boolean hasSingleBindingUsage() {
47+
return usages.stream().filter(UsageV2::isBindingUsage).toList().size() == 1;
48+
}
4549
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,193 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2024 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
120
package org.sonar.python.semantic.v2;
221

22+
import java.util.ArrayDeque;
23+
import java.util.ArrayList;
24+
import java.util.Collection;
25+
import java.util.Deque;
26+
import java.util.HashMap;
27+
import java.util.List;
28+
import java.util.Optional;
29+
import org.sonar.plugins.python.api.tree.AssignmentStatement;
330
import org.sonar.plugins.python.api.tree.BaseTreeVisitor;
31+
import org.sonar.plugins.python.api.tree.CallExpression;
32+
import org.sonar.plugins.python.api.tree.ClassDef;
33+
import org.sonar.plugins.python.api.tree.Expression;
34+
import org.sonar.plugins.python.api.tree.ExpressionList;
35+
import org.sonar.plugins.python.api.tree.FileInput;
36+
import org.sonar.plugins.python.api.tree.FunctionDef;
37+
import org.sonar.plugins.python.api.tree.ImportName;
38+
import org.sonar.plugins.python.api.tree.ListLiteral;
39+
import org.sonar.plugins.python.api.tree.Name;
40+
import org.sonar.plugins.python.api.tree.NumericLiteral;
41+
import org.sonar.plugins.python.api.tree.QualifiedExpression;
42+
import org.sonar.plugins.python.api.tree.StringLiteral;
43+
import org.sonar.plugins.python.api.types.InferredType;
44+
import org.sonar.python.tree.ListLiteralImpl;
45+
import org.sonar.python.tree.NameImpl;
46+
import org.sonar.python.tree.NumericLiteralImpl;
47+
import org.sonar.python.tree.StringLiteralImpl;
48+
import org.sonar.python.types.RuntimeType;
49+
import org.sonar.python.types.v2.ClassType;
50+
import org.sonar.python.types.v2.FunctionType;
51+
import org.sonar.python.types.v2.Member;
52+
import org.sonar.python.types.v2.ModuleType;
53+
import org.sonar.python.types.v2.ObjectType;
54+
import org.sonar.python.types.v2.ObjectTypeBuilder;
55+
import org.sonar.python.types.v2.PythonType;
456

557
public class TypeInferenceV2 extends BaseTreeVisitor {
658

59+
private final ProjectLevelTypeTable projectLevelTypeTable;
60+
61+
private final Deque<PythonType> typeStack = new ArrayDeque<>();
62+
63+
public TypeInferenceV2(ProjectLevelTypeTable projectLevelTypeTable) {
64+
this.projectLevelTypeTable = projectLevelTypeTable;
65+
}
66+
67+
@Override
68+
public void visitFileInput(FileInput fileInput) {
69+
var type = new ModuleType("somehow get its name", new HashMap<>());
70+
inTypeScope(type, () -> super.visitFileInput(fileInput));
71+
}
72+
73+
@Override
74+
public void visitStringLiteral(StringLiteral stringLiteral) {
75+
ModuleType builtins = this.projectLevelTypeTable.getModule("builtins");
76+
// TODO: multiple object types to represent str instance?
77+
((StringLiteralImpl) stringLiteral).typeV2(new ObjectType(builtins.resolveMember("str"), List.of(), List.of()));
78+
}
79+
80+
@Override
81+
public void visitNumericLiteral(NumericLiteral numericLiteral) {
82+
ModuleType builtins = this.projectLevelTypeTable.getModule("builtins");
83+
InferredType type = numericLiteral.type();
84+
String memberName = ((RuntimeType) type).getTypeClass().fullyQualifiedName();
85+
if (memberName != null) {
86+
((NumericLiteralImpl) numericLiteral).typeV2(new ObjectType(builtins.resolveMember(memberName), List.of(), List.of()));
87+
}
88+
}
89+
90+
@Override
91+
public void visitListLiteral(ListLiteral listLiteral) {
92+
ModuleType builtins = this.projectLevelTypeTable.getModule("builtins");
93+
scan(listLiteral.elements());
94+
List<PythonType> pythonTypes = listLiteral.elements().expressions().stream().map(Expression::typeV2).distinct().toList();
95+
// TODO: cleanly reduce attributes
96+
((ListLiteralImpl) listLiteral).typeV2(new ObjectType(builtins.resolveMember("list"), pythonTypes, List.of()));
97+
}
98+
99+
@Override
100+
public void visitClassDef(ClassDef classDef) {
101+
scan(classDef.args());
102+
Name name = classDef.name();
103+
ClassType type = new ClassType(name.name());
104+
((NameImpl) name).typeV2(type);
105+
106+
inTypeScope(type, () -> scan(classDef.body()));
107+
}
108+
109+
@Override
110+
public void visitFunctionDef(FunctionDef functionDef) {
111+
scan(functionDef.decorators());
112+
FunctionType functionType = new FunctionType(functionDef.name().name(), new ArrayList<>(), new ArrayList<>(), PythonType.UNKNOWN);
113+
if (currentType() instanceof ClassType classType) {
114+
if (functionDef.name().symbolV2().hasSingleBindingUsage()) {
115+
classType.members().add(new Member(functionType.name(), functionType));
116+
} else {
117+
// TODO: properly infer type in case of multiple assignments
118+
classType.members().add(new Member(functionType.name(), PythonType.UNKNOWN));
119+
}
120+
}
121+
((NameImpl) functionDef.name()).typeV2(functionType);
122+
inTypeScope(functionType, () -> {
123+
// TODO: check scope accuracy
124+
scan(functionDef.typeParams());
125+
scan(functionDef.parameters());
126+
scan(functionDef.returnTypeAnnotation());
127+
scan(functionDef.body());
128+
});
129+
}
130+
131+
@Override
132+
public void visitImportName(ImportName importName) {
133+
//createImportedNames(importName.modules(), null, Collections.emptyList());
134+
super.visitImportName(importName);
135+
}
136+
137+
@Override
138+
public void visitAssignmentStatement(AssignmentStatement assignmentStatement) {
139+
scan(assignmentStatement.assignedValue());
140+
Optional.of(assignmentStatement)
141+
.map(AssignmentStatement::lhsExpressions)
142+
.filter(lhs -> lhs.size() == 1)
143+
.map(lhs -> lhs.get(0))
144+
.map(ExpressionList::expressions)
145+
.filter(lhs -> lhs.size() == 1)
146+
.map(lhs -> lhs.get(0))
147+
.filter(NameImpl.class::isInstance)
148+
.map(NameImpl.class::cast)
149+
.ifPresent(lhsName -> {
150+
var assignedValueType = assignmentStatement.assignedValue().typeV2();
151+
lhsName.typeV2(assignedValueType);
152+
});
153+
}
154+
155+
@Override
156+
public void visitQualifiedExpression(QualifiedExpression qualifiedExpression) {
157+
scan(qualifiedExpression.qualifier());
158+
if (qualifiedExpression.name() instanceof NameImpl name) {
159+
var nameType = qualifiedExpression.qualifier().typeV2().resolveMember(qualifiedExpression.name().name());
160+
name.typeV2(nameType);
161+
}
162+
}
163+
164+
@Override
165+
public void visitName(Name name) {
166+
var type = Optional.of(name)
167+
.map(Name::symbolV2)
168+
.map(SymbolV2::usages)
169+
.stream()
170+
.flatMap(Collection::stream)
171+
.filter(UsageV2::isBindingUsage)
172+
.map(UsageV2::tree)
173+
.filter(Expression.class::isInstance)
174+
.map(Expression.class::cast)
175+
.map(Expression::typeV2)
176+
.toList();
177+
178+
if (type.size() == 1 && name instanceof NameImpl nameImpl) {
179+
nameImpl.typeV2(type.get(0));
180+
}
181+
}
182+
183+
private PythonType currentType() {
184+
return typeStack.peek();
185+
}
186+
187+
private void inTypeScope(PythonType pythonType, Runnable runnable) {
188+
this.typeStack.push(pythonType);
189+
runnable.run();
190+
this.typeStack.poll();
191+
}
7192

8193
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,15 @@
1919
*/
2020
package org.sonar.python.semantic.v2;
2121

22+
import org.sonar.plugins.python.api.symbols.Usage;
2223
import org.sonar.plugins.python.api.tree.Tree;
2324

2425
public record UsageV2(Tree tree, Kind kind) {
2526

27+
public boolean isBindingUsage() {
28+
return kind() != UsageV2.Kind.OTHER && kind() != UsageV2.Kind.GLOBAL_DECLARATION;
29+
}
30+
2631
enum Kind {
2732
ASSIGNMENT_LHS,
2833
COMPOUND_ASSIGNMENT_LHS,

python-frontend/src/main/java/org/sonar/python/tree/CallExpressionImpl.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@
4646
import org.sonar.python.types.DeclaredType;
4747
import org.sonar.python.types.HasTypeDependencies;
4848
import org.sonar.python.types.InferredTypes;
49+
import org.sonar.python.types.v2.ClassType;
50+
import org.sonar.python.types.v2.FunctionType;
51+
import org.sonar.python.types.v2.ObjectType;
52+
import org.sonar.python.types.v2.PythonType;
4953

5054
import static org.sonar.plugins.python.api.symbols.Symbol.Kind.CLASS;
5155
import static org.sonar.plugins.python.api.tree.Tree.Kind.SUBSCRIPTION;
@@ -179,4 +183,15 @@ private static InferredType getType(Symbol symbol) {
179183
public List<Expression> typeDependencies() {
180184
return Collections.singletonList(callee);
181185
}
186+
187+
@Override
188+
public PythonType typeV2() {
189+
if (callee().typeV2() instanceof ClassType classType) {
190+
return new ObjectType(classType);
191+
}
192+
if (callee().typeV2() instanceof FunctionType functionType) {
193+
return new ObjectType(functionType.returnType());
194+
}
195+
return PythonType.UNKNOWN;
196+
}
182197
}

0 commit comments

Comments
 (0)