Skip to content

Commit 4ae019f

Browse files
SONARPY-2010 Avoid creating duplicate types when the corresponding symbols are duplicated (#1882)
1 parent 4721b07 commit 4ae019f

File tree

16 files changed

+469
-74
lines changed

16 files changed

+469
-74
lines changed

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,5 @@ def aliased_utcnow():
3636
# FN because we lose the FQN in the assignment
3737
reassigned()
3838

39-
from datetime.datetime import utcnow as aliased
40-
aliased() # Noncompliant
41-
42-
from datetime.datetime import utcnow
43-
utcnow() # Noncompliant
39+
from datetime import datetime as aliased
40+
aliased.utcnow() # Noncompliant
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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.HashMap;
23+
import java.util.Map;
24+
import org.sonar.python.types.v2.LazyType;
25+
import org.sonar.python.types.v2.PythonType;
26+
27+
public class LazyTypesContext {
28+
private final Map<String, LazyType> lazyTypes;
29+
private final ProjectLevelTypeTable projectLevelTypeTable;
30+
31+
public LazyTypesContext(ProjectLevelTypeTable projectLevelTypeTable) {
32+
this.lazyTypes = new HashMap<>();
33+
this.projectLevelTypeTable = projectLevelTypeTable;
34+
}
35+
36+
public LazyType getOrCreateLazyType(String fullyQualifiedName) {
37+
if (lazyTypes.containsKey(fullyQualifiedName)) {
38+
return lazyTypes.get(fullyQualifiedName);
39+
}
40+
var lazyType = new LazyType(fullyQualifiedName, this);
41+
lazyTypes.put(fullyQualifiedName, lazyType);
42+
return lazyType;
43+
}
44+
45+
public PythonType resolveLazyType(LazyType lazyType) {
46+
PythonType resolved = projectLevelTypeTable.getType(lazyType.fullyQualifiedName());
47+
lazyType.resolve(resolved);
48+
lazyTypes.remove(lazyType.fullyQualifiedName());
49+
return resolved;
50+
}
51+
}

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

Lines changed: 14 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -20,47 +20,30 @@
2020
package org.sonar.python.semantic.v2;
2121

2222
import java.util.List;
23+
import java.util.Optional;
2324
import java.util.stream.IntStream;
2425
import org.sonar.python.semantic.ProjectLevelSymbolTable;
2526
import org.sonar.python.types.v2.ModuleType;
2627
import org.sonar.python.types.v2.PythonType;
27-
import org.sonar.python.types.v2.TriBool;
2828

2929
public class ProjectLevelTypeTable {
3030

3131
private final SymbolsModuleTypeProvider symbolsModuleTypeProvider;
3232
private final ModuleType rootModule;
33+
private final LazyTypesContext lazyTypesContext;
3334

3435
public ProjectLevelTypeTable(ProjectLevelSymbolTable projectLevelSymbolTable) {
3536
this(projectLevelSymbolTable, new TypeShed(projectLevelSymbolTable));
3637
}
3738

3839
public ProjectLevelTypeTable(ProjectLevelSymbolTable projectLevelSymbolTable, TypeShed typeShed) {
39-
this.symbolsModuleTypeProvider = new SymbolsModuleTypeProvider(projectLevelSymbolTable, typeShed);
40+
this.lazyTypesContext = new LazyTypesContext(this);
41+
this.symbolsModuleTypeProvider = new SymbolsModuleTypeProvider(projectLevelSymbolTable, typeShed, lazyTypesContext);
4042
this.rootModule = this.symbolsModuleTypeProvider.createBuiltinModule();
4143
}
4244

43-
public ModuleType getModule(String... moduleName) {
44-
return getModule(List.of(moduleName));
45-
}
46-
47-
public ModuleType getModule(List<String> moduleNameParts) {
48-
var parent = rootModule;
49-
for (int i = 0; i < moduleNameParts.size(); i++) {
50-
var existing = parent.resolveMember(moduleNameParts.get(i)).orElse(PythonType.UNKNOWN);
51-
52-
if (existing instanceof ModuleType existingModule) {
53-
parent = existingModule;
54-
continue;
55-
}
56-
57-
var moduleFqn = IntStream.rangeClosed(0, i)
58-
.mapToObj(moduleNameParts::get)
59-
.toList();
60-
61-
parent = symbolsModuleTypeProvider.createModuleType(moduleFqn, parent);
62-
}
63-
return parent;
45+
public ModuleType getBuiltinsModule() {
46+
return rootModule;
6447
}
6548

6649
public PythonType getType(String typeFqn) {
@@ -75,17 +58,22 @@ public PythonType getType(List<String> typeFqnParts) {
7558
var parent = (PythonType) rootModule;
7659
for (int i = 0; i < typeFqnParts.size(); i++) {
7760
var part = typeFqnParts.get(i);
78-
if (parent.hasMember(part) == TriBool.TRUE) {
79-
parent = parent.resolveMember(part).orElse(PythonType.UNKNOWN);
61+
Optional<PythonType> resolvedMember = parent.resolveMember(part);
62+
if (resolvedMember.isPresent()) {
63+
parent = resolvedMember.get();
8064
} else if (parent instanceof ModuleType module) {
8165
var moduleFqn = IntStream.rangeClosed(0, i)
8266
.mapToObj(typeFqnParts::get)
8367
.toList();
84-
parent = symbolsModuleTypeProvider.createModuleType(moduleFqn, module);
68+
parent = symbolsModuleTypeProvider.convertModuleType(moduleFqn, module);
8569
} else {
8670
return PythonType.UNKNOWN;
8771
}
8872
}
8973
return parent;
9074
}
75+
76+
public LazyTypesContext lazyTypesContext() {
77+
return lazyTypesContext;
78+
}
9179
}

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

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

22+
import java.util.ArrayDeque;
23+
import java.util.Arrays;
2224
import java.util.Collection;
2325
import java.util.HashMap;
2426
import java.util.List;
@@ -40,6 +42,7 @@
4042
import org.sonar.python.semantic.SymbolImpl;
4143
import org.sonar.python.types.v2.ClassType;
4244
import org.sonar.python.types.v2.FunctionType;
45+
import org.sonar.python.types.v2.LazyType;
4346
import org.sonar.python.types.v2.Member;
4447
import org.sonar.python.types.v2.ModuleType;
4548
import org.sonar.python.types.v2.ParameterV2;
@@ -50,23 +53,28 @@ public class SymbolsModuleTypeProvider {
5053
private final ProjectLevelSymbolTable projectLevelSymbolTable;
5154
private final TypeShed typeShed;
5255
private ModuleType rootModule;
56+
private LazyTypesContext lazyTypesContext;
5357

54-
public SymbolsModuleTypeProvider(ProjectLevelSymbolTable projectLevelSymbolTable, TypeShed typeShed) {
58+
public SymbolsModuleTypeProvider(ProjectLevelSymbolTable projectLevelSymbolTable, TypeShed typeShed, LazyTypesContext lazyTypeContext) {
5559
this.projectLevelSymbolTable = projectLevelSymbolTable;
5660
this.typeShed = typeShed;
61+
this.lazyTypesContext = lazyTypeContext;
5762
this.rootModule = createModuleFromSymbols(null, null, typeShed.builtinSymbols().values());
5863
}
5964

6065
public ModuleType createBuiltinModule() {
6166
return rootModule;
6267
}
6368

64-
public ModuleType createModuleType(List<String> moduleFqn, ModuleType parent) {
69+
public PythonType convertModuleType(List<String> moduleFqn, ModuleType parent) {
6570
var moduleName = moduleFqn.get(moduleFqn.size() - 1);
6671
var moduleFqnString = getModuleFqnString(moduleFqn);
67-
return createModuleTypeFromProjectLevelSymbolTable(moduleName, moduleFqnString, parent)
68-
.or(() -> createModuleTypeFromTypeShed(moduleName, moduleFqnString, parent))
69-
.orElseGet(() -> createEmptyModule(moduleName, parent));
72+
Optional<ModuleType> result = createModuleTypeFromProjectLevelSymbolTable(moduleName, moduleFqnString, parent)
73+
.or(() -> createModuleTypeFromTypeShed(moduleName, moduleFqnString, parent));
74+
if (result.isEmpty()) {
75+
return PythonType.UNKNOWN;
76+
}
77+
return result.get();
7078
}
7179

7280
private static String getModuleFqnString(List<String> moduleFqn) {
@@ -84,12 +92,6 @@ private Optional<ModuleType> createModuleTypeFromTypeShed(String moduleName, Str
8492
.map(typeShedModuleSymbols -> createModuleFromSymbols(moduleName, parent, typeShedModuleSymbols.values()));
8593
}
8694

87-
private static ModuleType createEmptyModule(String moduleName, ModuleType parent) {
88-
var emptyModule = new ModuleType(moduleName, parent);
89-
parent.members().put(moduleName, emptyModule);
90-
return emptyModule;
91-
}
92-
9395
private ModuleType createModuleFromSymbols(@Nullable String name, @Nullable ModuleType parent, Collection<Symbol> symbols) {
9496
var members = new HashMap<String, PythonType>();
9597
Map<Symbol, PythonType> createdTypesBySymbol = new HashMap<>();
@@ -132,6 +134,9 @@ private PythonType convertToFunctionType(FunctionSymbol symbol, Map<Symbol, Pyth
132134
.withHasVariadicParameter(symbol.hasVariadicParameter())
133135
.withDefinitionLocation(symbol.definitionLocation());
134136
FunctionType functionType = functionTypeBuilder.build();
137+
if (returnType instanceof LazyType lazyType) {
138+
lazyType.addConsumer(functionType::resolveLazyReturnType);
139+
}
135140
createdTypesBySymbol.put(symbol, functionType);
136141
return functionType;
137142
}
@@ -140,16 +145,41 @@ private PythonType resolveReturnType(Map<Symbol, PythonType> createdTypesBySymbo
140145
if (classSymbol == null) {
141146
return PythonType.UNKNOWN;
142147
}
143-
if (rootModule == null) {
148+
if (createdTypesBySymbol.containsKey(classSymbol)) {
149+
return createdTypesBySymbol.get(classSymbol);
150+
}
151+
String fullyQualifiedName = classSymbol.fullyQualifiedName();
152+
if (fullyQualifiedName == null) {
144153
return convertToType(classSymbol, createdTypesBySymbol);
145154
}
146-
// If the symbol is a built-in symbol and the root module already exists, we search for it instead of recreating the symbol
147-
// This is necessary to avoid duplicated symbols in the original ProjectSymbolTable to translate into duplicated PythonType
148-
// TODO: SONARPY-2010 to fix this
149-
return Optional.ofNullable(classSymbol.fullyQualifiedName())
150-
.filter(fqn -> !fqn.contains("."))
151-
.flatMap(f -> rootModule.resolveMember(f))
152-
.orElseGet(() -> convertToType(classSymbol, createdTypesBySymbol));
155+
return resolvePossibleLazyType(fullyQualifiedName);
156+
}
157+
158+
PythonType resolvePossibleLazyType(String fullyQualifiedName) {
159+
if (rootModule == null) {
160+
// If root module has not been created yet, return lazy type
161+
return lazyTypesContext.getOrCreateLazyType(fullyQualifiedName);
162+
}
163+
PythonType currentType = rootModule;
164+
String[] fqnParts = fullyQualifiedName.split("\\.");
165+
var fqnPartsQueue = new ArrayDeque<>(Arrays.asList(fqnParts));
166+
while (!fqnPartsQueue.isEmpty()) {
167+
var memberName = fqnPartsQueue.poll();
168+
var memberOpt = currentType.resolveMember(memberName);
169+
if (memberOpt.isEmpty()) {
170+
if (currentType instanceof ModuleType) {
171+
// The type is part of an unresolved submodule
172+
// Create a lazy type for it
173+
return lazyTypesContext.getOrCreateLazyType(fullyQualifiedName);
174+
} else {
175+
// The type is an unknown member of an already resolved type
176+
// Default to UNKNOWN
177+
return PythonType.UNKNOWN;
178+
}
179+
}
180+
currentType = memberOpt.get();
181+
}
182+
return currentType;
153183
}
154184

155185
private PythonType convertToClassType(ClassSymbol symbol, Map<Symbol, PythonType> createdTypesBySymbol) {

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

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ public void visitFileInput(FileInput fileInput) {
107107

108108
@Override
109109
public void visitStringLiteral(StringLiteral stringLiteral) {
110-
ModuleType builtins = this.projectLevelTypeTable.getModule();
110+
ModuleType builtins = this.projectLevelTypeTable.getBuiltinsModule();
111111
// TODO: SONARPY-1867 multiple object types to represent str instance?
112112
PythonType strType = builtins.resolveMember("str").orElse(PythonType.UNKNOWN);
113113
((StringLiteralImpl) stringLiteral).typeV2(new ObjectType(strType, new ArrayList<>(), new ArrayList<>()));
@@ -121,30 +121,30 @@ public void visitTuple(Tuple tuple) {
121121
if (contentTypes.size() == 1 && !contentTypes.get(0).equals(PythonType.UNKNOWN)) {
122122
attributes = contentTypes;
123123
}
124-
ModuleType builtins = this.projectLevelTypeTable.getModule();
124+
ModuleType builtins = this.projectLevelTypeTable.getBuiltinsModule();
125125
PythonType tupleType = builtins.resolveMember("tuple").orElse(PythonType.UNKNOWN);
126126
((TupleImpl) tuple).typeV2(new ObjectType(tupleType, attributes, new ArrayList<>()));
127127
}
128128

129129
@Override
130130
public void visitDictionaryLiteral(DictionaryLiteral dictionaryLiteral) {
131131
super.visitDictionaryLiteral(dictionaryLiteral);
132-
ModuleType builtins = this.projectLevelTypeTable.getModule();
132+
ModuleType builtins = this.projectLevelTypeTable.getBuiltinsModule();
133133
PythonType dictType = builtins.resolveMember("dict").orElse(PythonType.UNKNOWN);
134134
((DictionaryLiteralImpl) dictionaryLiteral).typeV2(new ObjectType(dictType, new ArrayList<>(), new ArrayList<>()));
135135
}
136136

137137
@Override
138138
public void visitSetLiteral(SetLiteral setLiteral) {
139139
super.visitSetLiteral(setLiteral);
140-
ModuleType builtins = this.projectLevelTypeTable.getModule();
140+
ModuleType builtins = this.projectLevelTypeTable.getBuiltinsModule();
141141
PythonType setType = builtins.resolveMember("set").orElse(PythonType.UNKNOWN);
142142
((SetLiteralImpl) setLiteral).typeV2(new ObjectType(setType, new ArrayList<>(), new ArrayList<>()));
143143
}
144144

145145
@Override
146146
public void visitNumericLiteral(NumericLiteral numericLiteral) {
147-
ModuleType builtins = this.projectLevelTypeTable.getModule();
147+
ModuleType builtins = this.projectLevelTypeTable.getBuiltinsModule();
148148
InferredType type = numericLiteral.type();
149149
String memberName = ((RuntimeType) type).getTypeClass().fullyQualifiedName();
150150
if (memberName != null) {
@@ -155,15 +155,15 @@ public void visitNumericLiteral(NumericLiteral numericLiteral) {
155155

156156
@Override
157157
public void visitNone(NoneExpression noneExpression) {
158-
ModuleType builtins = this.projectLevelTypeTable.getModule();
158+
ModuleType builtins = this.projectLevelTypeTable.getBuiltinsModule();
159159
// TODO: SONARPY-1867 multiple object types to represent str instance?
160160
PythonType noneType = builtins.resolveMember("NoneType").orElse(PythonType.UNKNOWN);
161161
((NoneExpressionImpl) noneExpression).typeV2(new ObjectType(noneType, new ArrayList<>(), new ArrayList<>()));
162162
}
163163

164164
@Override
165165
public void visitListLiteral(ListLiteral listLiteral) {
166-
var builtins = this.projectLevelTypeTable.getModule();
166+
var builtins = this.projectLevelTypeTable.getBuiltinsModule();
167167
scan(listLiteral.elements());
168168

169169
var candidateTypes = listLiteral.elements()
@@ -184,7 +184,7 @@ public void visitListLiteral(ListLiteral listLiteral) {
184184
@Override
185185
public void visitPyListOrSetCompExpression(ComprehensionExpression comprehensionExpression) {
186186
super.visitPyListOrSetCompExpression(comprehensionExpression);
187-
var builtins = this.projectLevelTypeTable.getModule();
187+
var builtins = this.projectLevelTypeTable.getBuiltinsModule();
188188
var pythonType = switch (comprehensionExpression.getKind()) {
189189
case LIST_COMPREHENSION -> builtins.resolveMember("list").orElse(PythonType.UNKNOWN);
190190
case SET_COMPREHENSION -> builtins.resolveMember("set").orElse(PythonType.UNKNOWN);
@@ -196,7 +196,7 @@ public void visitPyListOrSetCompExpression(ComprehensionExpression comprehension
196196
@Override
197197
public void visitDictCompExpression(DictCompExpression dictCompExpression) {
198198
super.visitDictCompExpression(dictCompExpression);
199-
var builtins = this.projectLevelTypeTable.getModule();
199+
var builtins = this.projectLevelTypeTable.getBuiltinsModule();
200200
var dictType = builtins.resolveMember("dict").orElse(PythonType.UNKNOWN);
201201
((DictCompExpressionImpl) dictCompExpression).typeV2(new ObjectType(dictType, new ArrayList<>(), new ArrayList<>()));
202202
}
@@ -298,8 +298,11 @@ public void visitImportName(ImportName importName) {
298298
var fqn = names
299299
.stream().map(Name::name)
300300
.toList();
301-
var module = projectLevelTypeTable.getModule(fqn);
301+
var resolvedType = projectLevelTypeTable.getType(fqn);
302302

303+
if (!(resolvedType instanceof ModuleType module)) {
304+
return;
305+
}
303306
if (aliasedName.alias() != null) {
304307
setTypeToName(aliasedName.alias(), module);
305308
} else {
@@ -323,7 +326,7 @@ public void visitImportFrom(ImportFrom importFrom) {
323326
.stream().map(Name::name)
324327
.toList();
325328

326-
var module = projectLevelTypeTable.getModule(fqn);
329+
var module = projectLevelTypeTable.getType(fqn);
327330
importFrom.importedNames().forEach(aliasedName -> aliasedName
328331
.dottedName()
329332
.names()
@@ -412,7 +415,7 @@ public void visitName(Name name) {
412415
SymbolV2 symbolV2 = name.symbolV2();
413416
if (symbolV2 == null) {
414417
// This part could be affected by SONARPY-1802
415-
projectLevelTypeTable.getModule().resolveMember(name.name())
418+
projectLevelTypeTable.getBuiltinsModule().resolveMember(name.name())
416419
.ifPresent(type -> setTypeToName(name, type));
417420
return;
418421
}

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,11 @@ public PythonType typeV2() {
191191
return new ObjectType(classType);
192192
}
193193
if (callee().typeV2() instanceof FunctionType functionType) {
194-
return functionType.returnType();
194+
PythonType returnType = functionType.returnType();
195+
if (returnType.equals(PythonType.UNKNOWN)) {
196+
return PythonType.UNKNOWN;
197+
}
198+
return new ObjectType(returnType);
195199
}
196200
if (callee().typeV2() instanceof UnionType unionType) {
197201
PythonType result = null;

0 commit comments

Comments
 (0)