Skip to content

Commit 62604db

Browse files
Seppli11sonartech
authored andcommitted
SONARPY-3194 Collect the CallGraph (#405)
GitOrigin-RevId: 41829c761a1e27f48b06cb01a0d20619b70dc397
1 parent da74ffc commit 62604db

File tree

4 files changed

+409
-3
lines changed

4 files changed

+409
-3
lines changed

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.sonar.python.semantic.v2.TypeInferenceV2;
3939
import org.sonar.python.semantic.v2.callgraph.CallGraph;
4040
import org.sonar.python.semantic.v2.TypeTable;
41+
import org.sonar.python.semantic.v2.callgraph.CallGraphCollector;
4142
import org.sonar.python.types.v2.TypeChecker;
4243

4344
public class PythonVisitorContext extends PythonInputFileContext {
@@ -152,7 +153,7 @@ public Builder projectLevelSymbolTable(ProjectLevelSymbolTable projectLevelSymbo
152153
}
153154

154155
public Builder typeTable(TypeTable typeTable) {
155-
this.typeTable = Optional.ofNullable(typeTable);
156+
this.typeTable = Optional.of(typeTable);
156157
return this;
157158
}
158159

@@ -177,7 +178,7 @@ public Builder moduleType(ModuleType moduleType) {
177178
}
178179

179180
public Builder callGraph(CallGraph callGraph) {
180-
this.callGraph = Optional.ofNullable(callGraph);
181+
this.callGraph = Optional.of(callGraph);
181182
return this;
182183
}
183184

@@ -192,6 +193,8 @@ public PythonVisitorContext build() {
192193
return new TypeInferenceV2(finalTypeTable, pythonFile, symbolTableV2, pkgName).inferModuleType(rootTree);
193194
});
194195

196+
var finalCallGraph = callGraph.orElseGet(() -> CallGraphCollector.collectCallGraph(rootTree));
197+
195198
return new PythonVisitorContext(
196199
rootTree,
197200
pythonFile,
@@ -202,7 +205,7 @@ public PythonVisitorContext build() {
202205
projectConfiguration.orElse(new ProjectConfiguration()),
203206
mt,
204207
new TypeChecker(finalTypeTable),
205-
callGraph.orElse(CallGraph.EMPTY)
208+
finalCallGraph
206209
);
207210
}
208211

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2025 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 Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.python.semantic.v2.callgraph;
18+
19+
import java.util.Optional;
20+
import java.util.Set;
21+
import java.util.stream.Collectors;
22+
import org.sonar.plugins.python.api.tree.BaseTreeVisitor;
23+
import org.sonar.plugins.python.api.tree.CallExpression;
24+
import org.sonar.plugins.python.api.tree.FileInput;
25+
import org.sonar.plugins.python.api.tree.FunctionDef;
26+
import org.sonar.plugins.python.api.tree.Tree;
27+
import org.sonar.plugins.python.api.types.v2.FunctionType;
28+
import org.sonar.plugins.python.api.types.v2.ModuleType;
29+
import org.sonar.plugins.python.api.types.v2.PythonType;
30+
import org.sonar.plugins.python.api.types.v2.UnionType;
31+
import org.sonar.plugins.python.api.types.v2.UnknownType.UnresolvedImportType;
32+
import org.sonar.python.tree.TreeUtils;
33+
34+
public class CallGraphCollector {
35+
private CallGraphCollector() {
36+
}
37+
38+
public static CallGraph collectCallGraph(FileInput rootTree) {
39+
var visitor = new Visitor();
40+
rootTree.accept(visitor);
41+
return visitor.build();
42+
}
43+
44+
private static class Visitor extends BaseTreeVisitor {
45+
private final CallGraph.Builder callGraphBuilder = new CallGraph.Builder();
46+
47+
@Override
48+
public void visitCallExpression(CallExpression callExpr) {
49+
super.visitCallExpression(callExpr);
50+
getCalledFunctionFqn(callExpr).ifPresent(calledFunctionFqn ->
51+
getEnclosedFunctionFqn(callExpr).ifPresent(enclosedFunctionFqn ->
52+
callGraphBuilder.addUsage(enclosedFunctionFqn, calledFunctionFqn)
53+
)
54+
);
55+
}
56+
57+
private static Optional<String> getCalledFunctionFqn(CallExpression callExpr) {
58+
var calleeType = callExpr.callee().typeV2();
59+
return CallGraphCollector.getFqn(calleeType);
60+
}
61+
62+
private static Optional<String> getEnclosedFunctionFqn(CallExpression callExpr) {
63+
Tree enclosingFuncDefTree = TreeUtils.firstAncestorOfKind(callExpr, Tree.Kind.FUNCDEF, Tree.Kind.LAMBDA);
64+
if(enclosingFuncDefTree instanceof FunctionDef enclosingFunctionDef) {
65+
return CallGraphCollector.getFqn(enclosingFunctionDef.name().typeV2());
66+
}
67+
// lambdas are not supported; thus returning empty
68+
return Optional.empty();
69+
}
70+
71+
public CallGraph build() {
72+
return callGraphBuilder.build();
73+
}
74+
}
75+
76+
private static Optional<String> getFqn(PythonType type) {
77+
if(type instanceof FunctionType functionType) {
78+
return Optional.of(functionType.fullyQualifiedName());
79+
} else if (type instanceof ModuleType moduleType) {
80+
return Optional.of(moduleType.fullyQualifiedName());
81+
} else if(type instanceof UnresolvedImportType unresolvedImportType) {
82+
return Optional.of(unresolvedImportType.importPath());
83+
} else if(type instanceof UnionType unionType) {
84+
Set<String> unionFqnSet = unionType.candidates().stream()
85+
.flatMap(candidate -> getFqn(candidate).stream())
86+
.collect(Collectors.toSet());
87+
88+
if (unionFqnSet.size() == 1) {
89+
return unionFqnSet.stream().findFirst();
90+
} else {
91+
// Multiple candidates, cannot determine a single FQN
92+
return Optional.empty();
93+
}
94+
} else {
95+
return Optional.empty();
96+
}
97+
}
98+
}

python-frontend/src/test/java/org/sonar/plugins/python/api/PythonVisitorContextTest.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
import org.sonar.python.parser.PythonParser;
3535
import org.sonar.python.semantic.ProjectLevelSymbolTable;
3636
import org.sonar.python.semantic.v2.ProjectLevelTypeTable;
37+
import org.sonar.python.semantic.v2.callgraph.CallGraph;
38+
import org.sonar.python.semantic.v2.callgraph.CallGraphNode;
3739
import org.sonar.python.tree.FileInputImpl;
3840
import org.sonar.python.tree.PythonTreeMaker;
3941

@@ -98,6 +100,31 @@ void globalSymbols() {
98100
assertThat(fileInput.globalVariables()).extracting(Symbol::name).containsExactlyInAnyOrder("a", "b");
99101
}
100102

103+
@Test
104+
void callGraph() {
105+
String code = """
106+
def bar():
107+
pass
108+
109+
def foo():
110+
bar()
111+
""";
112+
FileInput fileInput = new PythonTreeMaker().fileInput(PythonParser.create().parse(code));
113+
PythonFile pythonFile = pythonFile("my_module.py");
114+
115+
var ctx = new PythonVisitorContext.Builder(fileInput, pythonFile)
116+
.packageName("my_package")
117+
.cacheContext(CacheContextImpl.dummyCache())
118+
.build();
119+
120+
CallGraph callGraph = ctx.callGraph();
121+
122+
assertThat(callGraph.getUsages("my_package.my_module.foo")).isEmpty();
123+
assertThat(callGraph.getUsages("my_package.my_module.bar")).extracting(CallGraphNode::fqn)
124+
.containsExactly("my_package.my_module.foo");
125+
126+
}
127+
101128
@Test
102129
void sonar_product() {
103130
CacheContextImpl cacheContext = CacheContextImpl.dummyCache();

0 commit comments

Comments
 (0)