Skip to content

Commit 097a259

Browse files
maksim-grebeniuk-sonarsourcesonartech
authored andcommitted
SONARPY-2921 Fix the stack overflow in case of inheritance loops for ClassType.resolveMember (#252)
GitOrigin-RevId: 1639651f5c931a8fb4ab21ab1432b15291cd534a
1 parent cd1306b commit 097a259

2 files changed

Lines changed: 88 additions & 3 deletions

File tree

python-frontend/src/main/java/org/sonar/plugins/python/api/types/v2/ClassType.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.Objects;
2424
import java.util.Optional;
2525
import java.util.Set;
26+
import java.util.function.Predicate;
2627
import java.util.stream.Collectors;
2728
import javax.annotation.Nullable;
2829
import org.sonar.api.Beta;
@@ -127,8 +128,13 @@ public String key() {
127128

128129
@Override
129130
public Optional<PythonType> resolveMember(String memberName) {
131+
return resolveMember(memberName, new HashSet<>());
132+
}
133+
134+
private Optional<PythonType> resolveMember(String memberName, Set<PythonType> visited) {
135+
visited.add(this);
130136
return localMember(memberName)
131-
.or(() -> inheritedMember(memberName));
137+
.or(() -> inheritedMember(memberName, visited));
132138
}
133139

134140
private Optional<PythonType> localMember(String memberName) {
@@ -138,9 +144,17 @@ private Optional<PythonType> localMember(String memberName) {
138144
.findFirst();
139145
}
140146

141-
private Optional<PythonType> inheritedMember(String memberName) {
147+
private Optional<PythonType> inheritedMember(String memberName, Set<PythonType> visited) {
142148
return superClasses().stream()
143-
.map(s -> s.type().resolveMember(memberName))
149+
.map(TypeWrapper::type)
150+
.filter(Predicate.not(visited::contains))
151+
.map(t -> {
152+
visited.add(t);
153+
if (t instanceof ClassType superClassType) {
154+
return superClassType.resolveMember(memberName, visited);
155+
}
156+
return t.resolveMember(memberName);
157+
})
144158
.filter(Optional::isPresent)
145159
.map(Optional::get)
146160
.findFirst();

python-frontend/src/test/java/org/sonar/plugins/python/api/types/v2/ClassTypeTest.java

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
*/
1717
package org.sonar.plugins.python.api.types.v2;
1818

19+
import java.util.HashMap;
1920
import java.util.List;
21+
import java.util.Map;
2022
import org.junit.jupiter.api.Disabled;
2123
import org.junit.jupiter.api.Test;
2224
import org.sonar.plugins.python.api.LocationInFile;
@@ -30,6 +32,7 @@
3032
import org.sonar.python.PythonTestUtils;
3133
import org.sonar.python.semantic.SymbolUtils;
3234
import org.sonar.python.semantic.v2.ClassTypeBuilder;
35+
import org.sonar.python.semantic.v2.ProjectLevelTypeTable;
3336
import org.sonar.python.semantic.v2.SymbolTableBuilderV2;
3437
import org.sonar.python.semantic.v2.SymbolV2;
3538
import org.sonar.python.semantic.v2.TypeInferenceV2;
@@ -38,6 +41,8 @@
3841
import static org.assertj.core.api.Assertions.assertThat;
3942
import static org.sonar.python.PythonTestUtils.parse;
4043
import static org.sonar.python.PythonTestUtils.parseWithoutSymbols;
44+
import static org.sonar.python.PythonTestUtils.pythonFile;
45+
import static org.sonar.python.semantic.ProjectLevelSymbolTable.empty;
4146
import static org.sonar.python.types.v2.TypesTestUtils.PROJECT_LEVEL_TYPE_TABLE;
4247

4348
public class ClassTypeTest {
@@ -109,6 +114,45 @@ void multiple_local_parents() {
109114
assertThat(classB.superClasses()).extracting(TypeWrapper::type).containsExactlyInAnyOrder(classC, classA);
110115
}
111116

117+
@Test
118+
void recursive_inheritance_resolve_member() {
119+
var classTypes = multiFilesClassTypes(Map.ofEntries(
120+
Map.entry(
121+
"a.py",
122+
"""
123+
from b import B
124+
class A(B): ...
125+
"""
126+
),
127+
Map.entry(
128+
"b.py",
129+
"""
130+
from a import A
131+
class B(A): ...
132+
"""
133+
)
134+
)
135+
);
136+
ClassType classA = classTypes.get("a.A");
137+
assertThat(classA.resolveMember("foo")).isEmpty();
138+
}
139+
140+
@Test
141+
void unresolved_inheritance_resolve_member() {
142+
var classTypes = multiFilesClassTypes(Map.ofEntries(
143+
Map.entry(
144+
"a.py",
145+
"""
146+
from b import B
147+
class A(B): ...
148+
"""
149+
)
150+
)
151+
);
152+
ClassType classA = classTypes.get("a.A");
153+
assertThat(classA.resolveMember("foo")).containsInstanceOf(UnknownType.UnresolvedImportType.class);
154+
}
155+
112156
@Test
113157
void unknown_parent() {
114158
List<ClassType> classTypes = classTypes(
@@ -611,4 +655,31 @@ public static List<ClassType> classTypes(String... code) {
611655
.map(ClassType.class::cast)
612656
.toList();
613657
}
658+
659+
public static Map<String, ClassType> multiFilesClassTypes(Map<String, String> filesCodes) {
660+
var projectSymbolTable = empty();
661+
var result = new HashMap<String, ClassType>();
662+
663+
filesCodes.forEach((fileName, code) -> {
664+
var fileInput = parseWithoutSymbols(code);
665+
projectSymbolTable.addModule(fileInput, "", pythonFile(fileName));
666+
});
667+
668+
filesCodes.forEach((fileName, code) -> {
669+
FileInput fileInput = parseWithoutSymbols(code);
670+
var symbolTable = new SymbolTableBuilderV2(fileInput)
671+
.build();
672+
673+
new TypeInferenceV2(new ProjectLevelTypeTable(projectSymbolTable), pythonFile(fileName), symbolTable, "").inferTypes(fileInput);
674+
PythonTestUtils.getAllDescendant(fileInput, t -> t.is(Tree.Kind.CLASSDEF))
675+
.stream()
676+
.map(ClassDef.class::cast)
677+
.map(ClassDef::name)
678+
.map(Name::typeV2)
679+
.map(ClassType.class::cast)
680+
.forEach(ct -> result.put(ct.fullyQualifiedName(), ct));
681+
});
682+
683+
return result;
684+
}
614685
}

0 commit comments

Comments
 (0)