Skip to content

Commit 411a7ec

Browse files
Fix StackOverFlowError when method parameters have declared type of parent class (#1033)
1 parent 0e57db0 commit 411a7ec

File tree

6 files changed

+133
-40
lines changed

6 files changed

+133
-40
lines changed

python-frontend/src/main/java/org/sonar/python/index/DescriptorUtils.java

Lines changed: 78 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
*/
2020
package org.sonar.python.index;
2121

22+
import java.util.Collections;
2223
import java.util.List;
24+
import java.util.Map;
2325
import java.util.Objects;
2426
import java.util.Set;
2527
import java.util.stream.Collectors;
@@ -28,11 +30,16 @@
2830
import org.sonar.plugins.python.api.symbols.ClassSymbol;
2931
import org.sonar.plugins.python.api.symbols.FunctionSymbol;
3032
import org.sonar.plugins.python.api.symbols.Symbol;
33+
import org.sonar.plugins.python.api.types.InferredType;
3134
import org.sonar.python.semantic.AmbiguousSymbolImpl;
3235
import org.sonar.python.semantic.ClassSymbolImpl;
3336
import org.sonar.python.semantic.FunctionSymbolImpl;
3437
import org.sonar.python.semantic.ProjectLevelSymbolTable;
3538
import org.sonar.python.semantic.SymbolImpl;
39+
import org.sonar.python.types.DeclaredType;
40+
41+
import static org.sonar.python.semantic.SymbolUtils.typeshedSymbolWithFQN;
42+
import static org.sonar.python.types.InferredTypes.anyType;
3643

3744
public class DescriptorUtils {
3845

@@ -88,7 +95,7 @@ private static AmbiguousDescriptor ambiguousDescriptor(AmbiguousSymbol ambiguous
8895
}
8996

9097
public static AmbiguousDescriptor ambiguousDescriptor(AmbiguousSymbol ambiguousSymbol, @Nullable String overriddenFQN) {
91-
String fullyQualifiedName = overriddenFQN != null ? overriddenFQN : ambiguousSymbol.fullyQualifiedName();
98+
String fullyQualifiedName = overriddenFQN != null ? overriddenFQN : ambiguousSymbol.fullyQualifiedName();
9299
Set<Descriptor> alternatives = ambiguousSymbol.alternatives().stream()
93100
.map(DescriptorUtils::descriptor)
94101
.collect(Collectors.toSet());
@@ -107,27 +114,89 @@ private static List<FunctionDescriptor.Parameter> parameters(List<FunctionSymbol
107114
)).collect(Collectors.toList());
108115
}
109116

110-
public static Symbol symbolFromDescriptor(Descriptor descriptor, ProjectLevelSymbolTable projectLevelSymbolTable) {
111-
return symbolFromDescriptor(descriptor, projectLevelSymbolTable, null);
112-
}
113-
114-
public static Symbol symbolFromDescriptor(Descriptor descriptor, ProjectLevelSymbolTable projectLevelSymbolTable, @Nullable String localSymbolName) {
117+
public static Symbol symbolFromDescriptor(Descriptor descriptor, ProjectLevelSymbolTable projectLevelSymbolTable,
118+
@Nullable String localSymbolName, Map<String, Symbol> createdSymbols) {
115119
// The symbol generated from the descriptor will not have the descriptor name if an alias (localSymbolName) is defined
120+
if (createdSymbols.containsKey(descriptor.fullyQualifiedName())) {
121+
return createdSymbols.get(descriptor.fullyQualifiedName());
122+
}
116123
String symbolName = localSymbolName != null ? localSymbolName : descriptor.name();
117124
switch (descriptor.kind()) {
118125
case CLASS:
119-
return new ClassSymbolImpl((ClassDescriptor) descriptor, projectLevelSymbolTable, symbolName);
126+
return createClassSymbol(descriptor, projectLevelSymbolTable, createdSymbols, symbolName);
120127
case FUNCTION:
121-
return new FunctionSymbolImpl((FunctionDescriptor) descriptor, projectLevelSymbolTable, symbolName);
128+
return createFunctionSymbol((FunctionDescriptor) descriptor, projectLevelSymbolTable, createdSymbols, symbolName);
122129
case VARIABLE:
123130
return new SymbolImpl(symbolName, descriptor.fullyQualifiedName());
124131
case AMBIGUOUS:
125132
Set<Symbol> alternatives = ((AmbiguousDescriptor) descriptor).alternatives().stream()
126-
.map(a -> DescriptorUtils.symbolFromDescriptor(a, projectLevelSymbolTable, symbolName))
133+
.map(a -> DescriptorUtils.symbolFromDescriptor(a, projectLevelSymbolTable, symbolName, createdSymbols))
127134
.collect(Collectors.toSet());
128135
return new AmbiguousSymbolImpl(symbolName, descriptor.fullyQualifiedName(), alternatives);
129136
default:
130137
throw new IllegalStateException(String.format("Error while creating a Symbol from a Descriptor: Unexpected descriptor kind: %s", descriptor.kind()));
131138
}
132139
}
140+
141+
private static ClassSymbolImpl createClassSymbol(Descriptor descriptor, ProjectLevelSymbolTable projectLevelSymbolTable, Map<String, Symbol> createdSymbols, String symbolName) {
142+
ClassDescriptor classDescriptor = (ClassDescriptor) descriptor;
143+
ClassSymbolImpl classSymbol = new ClassSymbolImpl((ClassDescriptor) descriptor, symbolName);
144+
createdSymbols.put(descriptor.fullyQualifiedName(), classSymbol);
145+
addSuperClasses(classSymbol, classDescriptor, projectLevelSymbolTable, createdSymbols);
146+
addMembers(classSymbol, classDescriptor, projectLevelSymbolTable, createdSymbols);
147+
return classSymbol;
148+
}
149+
150+
private static void addMembers(ClassSymbolImpl classSymbol, ClassDescriptor classDescriptor,
151+
ProjectLevelSymbolTable projectLevelSymbolTable, Map<String, Symbol> createdSymbols) {
152+
classSymbol.addMembers(classDescriptor.members().stream()
153+
.map(memberFqn -> DescriptorUtils.symbolFromDescriptor(memberFqn, projectLevelSymbolTable, null, createdSymbols))
154+
.map(member -> {
155+
if (member instanceof FunctionSymbolImpl) {
156+
((FunctionSymbolImpl) member).setOwner(classSymbol);
157+
}
158+
return member;
159+
})
160+
.collect(Collectors.toList()));
161+
}
162+
163+
private static void addSuperClasses(ClassSymbolImpl classSymbol, ClassDescriptor classDescriptor,
164+
ProjectLevelSymbolTable projectLevelSymbolTable, Map<String, Symbol> createdSymbols) {
165+
classDescriptor.superClasses().stream()
166+
.map(superClassFqn -> {
167+
if (createdSymbols.containsKey(superClassFqn)) {
168+
return createdSymbols.get(superClassFqn);
169+
}
170+
Symbol symbol = projectLevelSymbolTable.getSymbol(superClassFqn);
171+
symbol = symbol != null ? symbol : typeshedSymbolWithFQN(superClassFqn);
172+
createdSymbols.put(superClassFqn, symbol);
173+
return symbol;
174+
}
175+
)
176+
.forEach(classSymbol::addSuperClass);
177+
}
178+
179+
private static FunctionSymbolImpl createFunctionSymbol(FunctionDescriptor functionDescriptor, ProjectLevelSymbolTable projectLevelSymbolTable,
180+
Map<String, Symbol> createdSymbols, String symbolName) {
181+
FunctionSymbolImpl functionSymbol = new FunctionSymbolImpl(functionDescriptor, symbolName);
182+
addParameters(functionSymbol, functionDescriptor, projectLevelSymbolTable, createdSymbols);
183+
return functionSymbol;
184+
}
185+
186+
private static void addParameters(FunctionSymbolImpl functionSymbol, FunctionDescriptor functionDescriptor,
187+
ProjectLevelSymbolTable projectLevelSymbolTable, Map<String, Symbol> createdSymbols) {
188+
functionDescriptor.parameters().stream().map(p -> {
189+
FunctionSymbolImpl.ParameterImpl parameter = new FunctionSymbolImpl.ParameterImpl(p);
190+
Symbol existingSymbol = createdSymbols.get(p.annotatedType());
191+
Symbol typeSymbol = existingSymbol != null ? existingSymbol : projectLevelSymbolTable.getSymbol(p.annotatedType());
192+
String annotatedTypeName = parameter.annotatedTypeName();
193+
if (typeSymbol == null && annotatedTypeName != null) {
194+
typeSymbol = typeshedSymbolWithFQN(annotatedTypeName);
195+
}
196+
// TODO: SONARPY-951 starred parameters should be mapped to the appropriate runtime type
197+
InferredType declaredType = typeSymbol == null ? anyType() : new DeclaredType(typeSymbol, Collections.emptyList());
198+
parameter.setDeclaredType(declaredType);
199+
return parameter;
200+
}).forEach(functionSymbol::addParameter);
201+
}
133202
}

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

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,10 @@
4242
import org.sonar.plugins.python.api.symbols.Symbol;
4343
import org.sonar.plugins.python.api.tree.ClassDef;
4444
import org.sonar.python.index.ClassDescriptor;
45-
import org.sonar.python.index.DescriptorUtils;
4645
import org.sonar.python.types.TypeShed;
4746
import org.sonar.python.types.protobuf.SymbolsProtos;
4847

4948
import static org.sonar.python.semantic.SymbolUtils.pathOf;
50-
import static org.sonar.python.semantic.SymbolUtils.typeshedSymbolWithFQN;
5149
import static org.sonar.python.tree.TreeUtils.locationInFile;
5250
import static org.sonar.python.types.TypeShed.isValidForProjectPythonVersion;
5351
import static org.sonar.python.types.TypeShed.normalizedFqn;
@@ -103,19 +101,9 @@ public ClassSymbolImpl(String name, @Nullable String fullyQualifiedName, Locatio
103101
setKind(Kind.CLASS);
104102
}
105103

106-
public ClassSymbolImpl(ClassDescriptor classDescriptor, ProjectLevelSymbolTable projectLevelSymbolTable, String symbolName) {
104+
public ClassSymbolImpl(ClassDescriptor classDescriptor, String symbolName) {
107105
super(symbolName, classDescriptor.fullyQualifiedName());
108106
setKind(Kind.CLASS);
109-
superClasses.addAll(classDescriptor.superClasses().stream().map(superClassFqn -> {
110-
Symbol symbol = projectLevelSymbolTable.getSymbol(superClassFqn);
111-
return symbol != null ? symbol : typeshedSymbolWithFQN(superClassFqn);
112-
}).collect(Collectors.toList()));
113-
members.addAll(classDescriptor.members().stream().map(m -> DescriptorUtils.symbolFromDescriptor(m, projectLevelSymbolTable)).collect(Collectors.toList()));
114-
members.forEach(m -> {
115-
if (m instanceof FunctionSymbolImpl) {
116-
((FunctionSymbolImpl) m).setOwner(this);
117-
}
118-
});
119107
classDefinitionLocation = classDescriptor.definitionLocation();
120108
hasDecorators = classDescriptor.hasDecorators();
121109
hasMetaClass = classDescriptor.hasMetaClass();

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

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,10 @@
2121

2222
import java.nio.file.Path;
2323
import java.util.ArrayList;
24-
import java.util.Collections;
2524
import java.util.HashSet;
2625
import java.util.List;
2726
import java.util.Objects;
2827
import java.util.Optional;
29-
import java.util.stream.Collectors;
3028
import javax.annotation.CheckForNull;
3129
import javax.annotation.Nullable;
3230
import org.sonar.plugins.python.api.LocationInFile;
@@ -44,14 +42,12 @@
4442
import org.sonar.plugins.python.api.types.InferredType;
4543
import org.sonar.python.index.FunctionDescriptor;
4644
import org.sonar.python.tree.TreeUtils;
47-
import org.sonar.python.types.DeclaredType;
4845
import org.sonar.python.types.InferredTypes;
4946
import org.sonar.python.types.TypeShed;
5047
import org.sonar.python.types.protobuf.SymbolsProtos;
5148

5249
import static org.sonar.python.semantic.SymbolUtils.isTypeShedFile;
5350
import static org.sonar.python.semantic.SymbolUtils.pathOf;
54-
import static org.sonar.python.semantic.SymbolUtils.typeshedSymbolWithFQN;
5551
import static org.sonar.python.tree.TreeUtils.locationInFile;
5652
import static org.sonar.python.types.InferredTypes.anyType;
5753
import static org.sonar.python.types.InferredTypes.fromTypeAnnotation;
@@ -127,7 +123,7 @@ public FunctionSymbolImpl(SymbolsProtos.FunctionSymbol functionSymbolProto, bool
127123
this.validForPythonVersions = new HashSet<>(validFor);
128124
}
129125

130-
public FunctionSymbolImpl(FunctionDescriptor functionDescriptor, ProjectLevelSymbolTable projectLevelSymbolTable, String symbolName) {
126+
public FunctionSymbolImpl(FunctionDescriptor functionDescriptor, String symbolName) {
131127
super(symbolName, functionDescriptor.fullyQualifiedName());
132128
setKind(Kind.FUNCTION);
133129
isInstanceMethod = functionDescriptor.isInstanceMethod();
@@ -136,8 +132,6 @@ public FunctionSymbolImpl(FunctionDescriptor functionDescriptor, ProjectLevelSym
136132
decorators = functionDescriptor.decorators();
137133
annotatedReturnTypeName = functionDescriptor.annotatedReturnTypeName();
138134
functionDefinitionLocation = functionDescriptor.definitionLocation();
139-
parameters.addAll(functionDescriptor.parameters().stream().map(p -> new FunctionSymbolImpl.ParameterImpl(p, projectLevelSymbolTable)).collect(Collectors.toList()));
140-
hasVariadicParameter = parameters.stream().anyMatch(FunctionSymbol.Parameter::isVariadic);
141135
// TODO: Will no longer be true once SONARPY-647 is fixed
142136
isStub = false;
143137
}
@@ -147,6 +141,13 @@ public void setParametersWithType(ParameterList parametersList) {
147141
createParameterNames(parametersList.all(), functionDefinitionLocation == null ? null : functionDefinitionLocation.fileId());
148142
}
149143

144+
public void addParameter(ParameterImpl parameter) {
145+
this.parameters.add(parameter);
146+
if (parameter.isVariadic()) {
147+
this.hasVariadicParameter = true;
148+
}
149+
}
150+
150151
FunctionSymbolImpl(String name, FunctionSymbol functionSymbol) {
151152
super(name, functionSymbol.fullyQualifiedName());
152153
setKind(Kind.FUNCTION);
@@ -372,20 +373,14 @@ public static class ParameterImpl implements Parameter {
372373
this.annotatedTypeName = annotatedTypeName;
373374
}
374375

375-
public ParameterImpl(FunctionDescriptor.Parameter parameterDescriptor, ProjectLevelSymbolTable projectLevelSymbolTable) {
376+
public ParameterImpl(FunctionDescriptor.Parameter parameterDescriptor) {
376377
this.name = parameterDescriptor.name();
377378
this.hasDefaultValue = parameterDescriptor.hasDefaultValue();
378379
this.isVariadic = parameterDescriptor.isVariadic();
379380
this.isKeywordOnly = parameterDescriptor.isKeywordOnly();
380381
this.isPositionalOnly = parameterDescriptor.isPositionalOnly();
381382
this.location = parameterDescriptor.location();
382383
this.annotatedTypeName = parameterDescriptor.annotatedType();
383-
Symbol typeSymbol = projectLevelSymbolTable.getSymbol(parameterDescriptor.annotatedType());
384-
if (typeSymbol == null && annotatedTypeName != null) {
385-
typeSymbol = typeshedSymbolWithFQN(annotatedTypeName);
386-
}
387-
// TODO: SONARPY-951 starred parameters should be mapped to the appropriate runtime type
388-
this.declaredType = typeSymbol == null ? anyType() : new DeclaredType(typeSymbol, Collections.emptyList());
389384
}
390385

391386
@Override
@@ -403,6 +398,10 @@ public InferredType declaredType() {
403398
return declaredType;
404399
}
405400

401+
public void setDeclaredType(InferredType type) {
402+
this.declaredType = type;
403+
}
404+
406405
@CheckForNull
407406
public String annotatedTypeName() {
408407
return annotatedTypeName;

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ public Symbol getSymbol(@Nullable String fullyQualifiedName, @Nullable String lo
152152
return null;
153153
} else {
154154
queriedSymbolNames.add(fullyQualifiedName);
155-
Symbol symbol = DescriptorUtils.symbolFromDescriptor(descriptor, this, localSymbolName);
155+
Symbol symbol = DescriptorUtils.symbolFromDescriptor(descriptor, this, localSymbolName, new HashMap<>());
156156
queriedSymbolNames = new HashSet<>();
157157
return symbol;
158158
}
@@ -161,11 +161,12 @@ public Symbol getSymbol(@Nullable String fullyQualifiedName, @Nullable String lo
161161
@CheckForNull
162162
public Set<Symbol> getSymbolsFromModule(@Nullable String moduleName) {
163163
Set<Descriptor> descriptors = globalDescriptorsByModuleName.get(moduleName);
164+
Map<String, Symbol> createdSymbols = new HashMap<>();
164165
if (descriptors == null) {
165166
return null;
166167
}
167168
return descriptors.stream()
168-
.map(desc -> DescriptorUtils.symbolFromDescriptor(desc, this)).collect(Collectors.toSet());
169+
.map(desc -> DescriptorUtils.symbolFromDescriptor(desc, this, null, createdSymbols)).collect(Collectors.toSet());
169170
}
170171

171172
public boolean isDjangoView(@Nullable String fqn) {

python-frontend/src/main/java/org/sonar/python/types/DeclaredType.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,8 @@ public String typeName() {
154154
return str.toString();
155155
}
156156

157-
Symbol getTypeClass() {
157+
// Visible for testing
158+
public Symbol getTypeClass() {
158159
if (typeClass == null) {
159160
// the value is recomputed each time instead of storing it to avoid consistency problem when 'sonar.python.version' property is changed
160161
return TypeShed.typeShedClass(builtinFullyQualifiedName);

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

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@
3939
import org.sonar.plugins.python.api.tree.ImportFrom;
4040
import org.sonar.plugins.python.api.tree.QualifiedExpression;
4141
import org.sonar.plugins.python.api.tree.Tree;
42+
import org.sonar.plugins.python.api.types.InferredType;
4243
import org.sonar.python.PythonTestUtils;
44+
import org.sonar.python.types.DeclaredType;
4345
import org.sonar.python.types.InferredTypes;
4446

4547
import static org.assertj.core.api.Assertions.assertThat;
@@ -612,7 +614,7 @@ public void class_having_itself_as_superclass_should_not_trigger_error() {
612614
FileInput fileInput = parseWithoutSymbols("class A(A): pass");
613615
Set<Symbol> globalSymbols = globalSymbols(fileInput, "mod");
614616
ClassSymbol a = (ClassSymbol) globalSymbols.iterator().next();
615-
assertThat(a.superClasses()).extracting("name").containsExactly("A");
617+
assertThat(a.superClasses()).containsExactly(a);
616618
}
617619

618620
@Test
@@ -697,6 +699,22 @@ public void annotated_parameter_is_translated_correctly() {
697699
assertThat(globalSymbols).extracting(Symbol::usages).allSatisfy(usages -> assertThat(usages).isEmpty());
698700
}
699701

702+
@Test
703+
public void symbols_from_module_should_be_the_same() {
704+
FileInput tree = parseWithoutSymbols(
705+
"class A: ...",
706+
"class B(A): ..."
707+
);
708+
ProjectLevelSymbolTable projectLevelSymbolTable = new ProjectLevelSymbolTable();
709+
projectLevelSymbolTable.addModule(tree, "", pythonFile("mod.py"));
710+
Set<Symbol> mod = projectLevelSymbolTable.getSymbolsFromModule("mod");
711+
assertThat(mod).extracting(Symbol::name).containsExactlyInAnyOrder("A", "B");
712+
ClassSymbol classSymbolA = (ClassSymbol) mod.stream().filter(s -> s.fullyQualifiedName().equals("mod.A")).findFirst().get();
713+
ClassSymbol classSymbolB = (ClassSymbol) mod.stream().filter(s -> s.fullyQualifiedName().equals("mod.B")).findFirst().get();
714+
ClassSymbol superClass = (ClassSymbol) classSymbolB.superClasses().get(0);
715+
assertThat(superClass).isSameAs(classSymbolA);
716+
}
717+
700718
@Test
701719
public void imported_typeshed_symbols_are_not_exported() {
702720
FileInput tree = parseWithoutSymbols(
@@ -706,6 +724,23 @@ public void imported_typeshed_symbols_are_not_exported() {
706724
assertThat(globalSymbols).isEmpty();
707725
}
708726

727+
@Test
728+
public void class_with_method_parameter_of_same_type() {
729+
FileInput tree = parseWithoutSymbols(
730+
"class Document:",
731+
" def my_method(param: Document): ..."
732+
);
733+
Set<Symbol> globalSymbols = globalSymbols(tree, "");
734+
assertThat(globalSymbols).hasSize(1);
735+
ClassSymbol classSymbol = (ClassSymbol) globalSymbols.stream().findFirst().get();
736+
assertThat(classSymbol.declaredMembers()).hasSize(1);
737+
FunctionSymbol functionSymbol = (FunctionSymbol) classSymbol.declaredMembers().stream().findFirst().get();
738+
assertThat(functionSymbol.parameters()).hasSize(1);
739+
FunctionSymbol.Parameter parameter = functionSymbol.parameters().get(0);
740+
DeclaredType declaredType = (DeclaredType) parameter.declaredType();
741+
assertThat(declaredType.getTypeClass()).isSameAs(classSymbol);
742+
}
743+
709744
@Test
710745
public void django_views() {
711746
String[] urls = {

0 commit comments

Comments
 (0)