diff --git a/python-frontend/src/main/java/org/sonar/python/tree/TreeUtils.java b/python-frontend/src/main/java/org/sonar/python/tree/TreeUtils.java index e8d42cc51e..c97a3af969 100644 --- a/python-frontend/src/main/java/org/sonar/python/tree/TreeUtils.java +++ b/python-frontend/src/main/java/org/sonar/python/tree/TreeUtils.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; +import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -145,21 +146,22 @@ public static ClassSymbol getClassSymbolFromDef(@Nullable ClassDef classDef) { } public static List getParentClassesFQN(ClassDef classDef) { - return getParentClasses(TreeUtils.getClassSymbolFromDef(classDef)).stream() + return getParentClasses(TreeUtils.getClassSymbolFromDef(classDef), new HashSet<>()).stream() .map(Symbol::fullyQualifiedName) .filter(Objects::nonNull) .collect(Collectors.toList()); } - private static List getParentClasses(@Nullable ClassSymbol classSymbol) { + private static List getParentClasses(@Nullable ClassSymbol classSymbol, Set visitedSymbols) { List superClasses = new ArrayList<>(); - if (classSymbol == null) { + if (classSymbol == null || visitedSymbols.contains(classSymbol)) { return superClasses; } + visitedSymbols.add(classSymbol); for (Symbol symbol : classSymbol.superClasses()) { superClasses.add(symbol); if (symbol instanceof ClassSymbol) { - superClasses.addAll(getParentClasses((ClassSymbol) symbol)); + superClasses.addAll(getParentClasses((ClassSymbol) symbol, visitedSymbols)); } } return superClasses; diff --git a/python-frontend/src/test/java/org/sonar/python/semantic/ProjectLevelSymbolTableTest.java b/python-frontend/src/test/java/org/sonar/python/semantic/ProjectLevelSymbolTableTest.java index cc636b8542..f5b6a22df6 100644 --- a/python-frontend/src/test/java/org/sonar/python/semantic/ProjectLevelSymbolTableTest.java +++ b/python-frontend/src/test/java/org/sonar/python/semantic/ProjectLevelSymbolTableTest.java @@ -38,15 +38,18 @@ import org.sonar.plugins.python.api.symbols.Symbol; import org.sonar.plugins.python.api.symbols.Usage; import org.sonar.plugins.python.api.tree.CallExpression; +import org.sonar.plugins.python.api.tree.ClassDef; import org.sonar.plugins.python.api.tree.FileInput; import org.sonar.plugins.python.api.tree.FunctionDef; import org.sonar.plugins.python.api.tree.ImportFrom; import org.sonar.plugins.python.api.tree.QualifiedExpression; +import org.sonar.plugins.python.api.tree.Statement; import org.sonar.plugins.python.api.tree.Tree; import org.sonar.python.PythonTestUtils; import org.sonar.python.index.Descriptor; import org.sonar.python.index.DescriptorUtils; import org.sonar.python.index.VariableDescriptor; +import org.sonar.python.tree.TreeUtils; import org.sonar.python.types.DeclaredType; import org.sonar.python.types.InferredTypes; @@ -697,7 +700,25 @@ public void class_having_itself_as_superclass_should_not_trigger_error() { FileInput fileInput = parseWithoutSymbols("class A(A): pass"); Set globalSymbols = globalSymbols(fileInput, "mod"); ClassSymbol a = (ClassSymbol) globalSymbols.iterator().next(); + // SONARPY-1350: The parent "A" is not yet defined at the time it is read, so this is actually not correct assertThat(a.superClasses()).containsExactly(a); + ClassDef classDef = (ClassDef) fileInput.statements().statements().get(0); + assertThat(TreeUtils.getParentClassesFQN(classDef)).containsExactly("mod.mod.A"); + } + + + @Test + public void class_having_another_class_with_same_name_should_not_trigger_error() { + FileInput fileInput = parseWithoutSymbols( + "from external import B", + "class A:", + " class B(B): pass" + ); + globalSymbols(fileInput, "mod"); + ClassDef outerClassDef = (ClassDef) fileInput.statements().statements().get(1); + ClassDef innerClassDef = (ClassDef) outerClassDef.body().statements().get(0); + // SONARPY-1350: Parent should be external.B + assertThat(TreeUtils.getParentClassesFQN(innerClassDef)).containsExactly("mod.mod.A.B"); } @Test