Skip to content

Commit 10fa50f

Browse files
Seppli11sonartech
authored andcommitted
SONARPY-3193 implement CallGraph data structure
pass call graph to checks use call graph in AwsLambdaChecksUtils fixes after PR add isTrue and isFalse to TriBool GitOrigin-RevId: 23a990b2adb91191a658a5448ab1c70dac6c5b01
1 parent 5f00640 commit 10fa50f

File tree

15 files changed

+556
-62
lines changed

15 files changed

+556
-62
lines changed

python-checks/src/main/java/org/sonar/python/checks/utils/AwsLambdaChecksUtils.java

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,38 @@
1616
*/
1717
package org.sonar.python.checks.utils;
1818

19+
import org.sonar.plugins.python.api.PythonVisitorContext;
1920
import org.sonar.plugins.python.api.project.configuration.ProjectConfiguration;
2021
import org.sonar.plugins.python.api.tree.FunctionDef;
2122
import org.sonar.plugins.python.api.types.v2.FunctionType;
23+
import org.sonar.plugins.python.api.types.v2.TriBool;
24+
import org.sonar.python.semantic.v2.callgraph.CallGraph;
25+
import org.sonar.python.semantic.v2.callgraph.CallGraphWalker;
2226

2327
public class AwsLambdaChecksUtils {
2428

2529
private AwsLambdaChecksUtils() {
2630
}
2731

28-
public static boolean isLambdaHandler(ProjectConfiguration projectConfiguration, FunctionDef functionDef) {
29-
return functionDef.name().typeV2() instanceof FunctionType functionType
30-
&& projectConfiguration.awsProjectConfiguration()
32+
public static boolean isLambdaHandler(PythonVisitorContext ctx, FunctionDef functionDef) {
33+
if (functionDef.name().typeV2() instanceof FunctionType functionType) {
34+
String fqn = functionType.fullyQualifiedName();
35+
return isLambdaHandlerFqn(ctx.projectConfiguration(), fqn)
36+
|| isFqnCalledFromLambdaHandler(ctx.callGraph(), ctx.projectConfiguration(), fqn);
37+
}
38+
return false;
39+
}
40+
41+
private static boolean isLambdaHandlerFqn(ProjectConfiguration projectConfiguration, String fqn) {
42+
return projectConfiguration.awsProjectConfiguration()
3143
.awsLambdaHandlers()
3244
.stream()
33-
.anyMatch(handler -> handler.fullyQualifiedName().equals(functionType.fullyQualifiedName()));
45+
.anyMatch(handler -> handler.fullyQualifiedName().equals(fqn));
3446
}
3547

36-
48+
private static boolean isFqnCalledFromLambdaHandler(CallGraph callGraph, ProjectConfiguration projectConfiguration, String fqn) {
49+
return new CallGraphWalker(callGraph)
50+
.isUsedFrom(fqn, node -> isLambdaHandlerFqn(projectConfiguration, node.fqn()))
51+
.isTrue();
52+
}
3753
}

python-checks/src/test/java/org/sonar/python/checks/utils/AwsLambdaCheckUtilsTest.java

Lines changed: 0 additions & 54 deletions
This file was deleted.
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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.checks.utils;
18+
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
import static org.mockito.Mockito.mock;
21+
22+
import java.io.IOException;
23+
import java.nio.file.Files;
24+
import org.junit.jupiter.api.Test;
25+
import org.mockito.Mockito;
26+
import org.sonar.plugins.python.api.PythonFile;
27+
import org.sonar.plugins.python.api.PythonVisitorContext;
28+
import org.sonar.plugins.python.api.project.configuration.AwsLambdaHandlerInfo;
29+
import org.sonar.plugins.python.api.project.configuration.ProjectConfiguration;
30+
import org.sonar.plugins.python.api.tree.FileInput;
31+
import org.sonar.plugins.python.api.tree.FunctionDef;
32+
import org.sonar.plugins.python.api.tree.Name;
33+
import org.sonar.plugins.python.api.types.v2.FunctionType;
34+
import org.sonar.plugins.python.api.types.v2.PythonType;
35+
import org.sonar.python.semantic.v2.callgraph.CallGraph;
36+
import org.sonar.python.tree.FileInputImpl;
37+
38+
class AwsLambdaChecksUtilsTest {
39+
@Test
40+
void isLambdaHandlerTest_direct() {
41+
var pythonVisitorContext = pythonVisitorContext(CallGraph.EMPTY);
42+
43+
var functionDef = functionDef("a.b.c");
44+
assertThat(AwsLambdaChecksUtils.isLambdaHandler(pythonVisitorContext, functionDef)).isFalse();
45+
46+
pythonVisitorContext.projectConfiguration().awsProjectConfiguration().awsLambdaHandlers().add(new AwsLambdaHandlerInfo("a.b.c"));
47+
assertThat(AwsLambdaChecksUtils.isLambdaHandler(pythonVisitorContext, functionDef)).isTrue();
48+
49+
var functionDefWithUnknownType = functionDef(PythonType.UNKNOWN);
50+
assertThat(AwsLambdaChecksUtils.isLambdaHandler(pythonVisitorContext, functionDefWithUnknownType)).isFalse();
51+
}
52+
53+
@Test
54+
void isLambdaHandlerTest_callGraph() {
55+
var callGraph = new CallGraph.Builder()
56+
.addUsage("lambda.handler", "a.b.c")
57+
.addUsage("a.b.c", "e.f.g")
58+
.build();
59+
60+
var pythonVisitorContext = pythonVisitorContext(callGraph);
61+
62+
var functionDef = functionDef("e.f.g");
63+
64+
assertThat(AwsLambdaChecksUtils.isLambdaHandler(pythonVisitorContext, functionDef)).isFalse();
65+
66+
pythonVisitorContext.projectConfiguration().awsProjectConfiguration().awsLambdaHandlers().add(new AwsLambdaHandlerInfo("lambda.handler"));
67+
assertThat(AwsLambdaChecksUtils.isLambdaHandler(pythonVisitorContext, functionDef)).isTrue();
68+
}
69+
70+
private static PythonVisitorContext pythonVisitorContext(CallGraph callGraph) {
71+
PythonFile pythonFile = pythonFile("test.py");
72+
FileInput fileInput = mock(FileInputImpl.class);
73+
74+
return new PythonVisitorContext.Builder(fileInput, pythonFile)
75+
.projectConfiguration(new ProjectConfiguration())
76+
.callGraph(callGraph)
77+
.build();
78+
}
79+
80+
81+
private static FunctionDef functionDef(String name) {
82+
FunctionType functionNameType = Mockito.mock(FunctionType.class);
83+
Mockito.when(functionNameType.fullyQualifiedName()).thenReturn(name);
84+
return functionDef(functionNameType);
85+
}
86+
87+
private static FunctionDef functionDef(PythonType type) {
88+
Name functionName = Mockito.mock(Name.class);
89+
FunctionDef functionDef = Mockito.mock(FunctionDef.class);
90+
91+
Mockito.when(functionName.typeV2()).thenReturn(type);
92+
Mockito.when(functionDef.name()).thenReturn(functionName);
93+
return functionDef;
94+
}
95+
96+
private static PythonFile pythonFile(String fileName) {
97+
PythonFile pythonFile = Mockito.mock(PythonFile.class);
98+
Mockito.when(pythonFile.fileName()).thenReturn(fileName);
99+
try {
100+
Mockito.when(pythonFile.uri()).thenReturn(Files.createTempFile(fileName, "py").toUri());
101+
} catch (IOException e) {
102+
throw new IllegalStateException("Cannot create temporary file");
103+
}
104+
return pythonFile;
105+
}
106+
107+
}

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.sonar.python.semantic.v2.ProjectLevelTypeTable;
3737
import org.sonar.python.semantic.v2.SymbolTableBuilderV2;
3838
import org.sonar.python.semantic.v2.TypeInferenceV2;
39+
import org.sonar.python.semantic.v2.callgraph.CallGraph;
3940
import org.sonar.python.types.v2.TypeChecker;
4041

4142
public class PythonVisitorContext extends PythonInputFileContext {
@@ -46,6 +47,7 @@ public class PythonVisitorContext extends PythonInputFileContext {
4647
private ModuleType moduleType = null;
4748
private final List<PreciseIssue> issues;
4849
private final ProjectConfiguration projectConfiguration;
50+
private final CallGraph callGraph;
4951

5052
private PythonVisitorContext(FileInput rootTree,
5153
PythonFile pythonFile,
@@ -54,7 +56,8 @@ private PythonVisitorContext(FileInput rootTree,
5456
ProjectLevelSymbolTable projectLevelSymbolTable,
5557
CacheContext cacheContext,
5658
SonarProduct sonarProduct,
57-
ProjectConfiguration projectConfiguration) {
59+
ProjectConfiguration projectConfiguration,
60+
CallGraph callGraph) {
5861

5962
super(pythonFile, workingDirectory, cacheContext, sonarProduct, projectLevelSymbolTable);
6063
var symbolTableBuilderV2 = new SymbolTableBuilderV2(rootTree);
@@ -64,6 +67,7 @@ private PythonVisitorContext(FileInput rootTree,
6467
this.moduleType = new TypeInferenceV2(projectLevelTypeTable, pythonFile, symbolTable, packageName).inferModuleType(rootTree);
6568
this.typeChecker = new TypeChecker(projectLevelTypeTable);
6669
this.projectConfiguration = projectConfiguration;
70+
this.callGraph = callGraph;
6771
this.rootTree = rootTree;
6872
this.parsingException = null;
6973
this.issues = new ArrayList<>();
@@ -79,6 +83,7 @@ public PythonVisitorContext(PythonFile pythonFile, RecognitionException parsingE
7983
this.parsingException = parsingException;
8084
this.typeChecker = new TypeChecker(new ProjectLevelTypeTable(ProjectLevelSymbolTable.empty()));
8185
this.projectConfiguration = new ProjectConfiguration();
86+
this.callGraph = CallGraph.EMPTY;
8287
this.issues = new ArrayList<>();
8388
}
8489

@@ -112,6 +117,10 @@ public ProjectConfiguration projectConfiguration() {
112117
return projectConfiguration;
113118
}
114119

120+
public CallGraph callGraph() {
121+
return callGraph;
122+
}
123+
115124
public static class Builder {
116125
private final PythonFile pythonFile;
117126
private final FileInput rootTree;
@@ -121,6 +130,7 @@ public static class Builder {
121130
private Optional<SonarProduct> sonarProduct = Optional.empty();
122131
private Optional<File> workingDirectory = Optional.empty();
123132
private Optional<ProjectConfiguration> projectConfiguration = Optional.empty();
133+
private Optional<CallGraph> callGraph = Optional.empty();
124134
private Optional<String> packageName = Optional.empty();
125135

126136
public Builder(FileInput rootTree, PythonFile pythonFile) {
@@ -158,6 +168,11 @@ public Builder projectConfiguration(ProjectConfiguration projectConfiguration) {
158168
return this;
159169
}
160170

171+
public Builder callGraph(CallGraph callGraph) {
172+
this.callGraph = Optional.ofNullable(callGraph);
173+
return this;
174+
}
175+
161176
public PythonVisitorContext build() {
162177
return new PythonVisitorContext(
163178
rootTree,
@@ -167,7 +182,8 @@ public PythonVisitorContext build() {
167182
projectLevelSymbolTable.orElseGet(ProjectLevelSymbolTable::empty),
168183
cacheContext.orElseGet(CacheContextImpl::dummyCache),
169184
sonarProduct.orElse(SonarProduct.SONARQUBE),
170-
projectConfiguration.orElse(new ProjectConfiguration())
185+
projectConfiguration.orElse(new ProjectConfiguration()),
186+
callGraph.orElse(CallGraph.EMPTY)
171187
);
172188
}
173189
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.sonar.plugins.python.api.symbols.Symbol;
2828
import org.sonar.plugins.python.api.tree.Token;
2929
import org.sonar.plugins.python.api.tree.Tree;
30+
import org.sonar.python.semantic.v2.callgraph.CallGraph;
3031
import org.sonar.python.types.v2.TypeChecker;
3132

3233
public interface SubscriptionContext {
@@ -70,4 +71,6 @@ public interface SubscriptionContext {
7071
TypeChecker typeChecker();
7172

7273
ProjectConfiguration projectConfiguration();
74+
75+
CallGraph callGraph();
7376
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,12 @@ public TriBool and(TriBool triBool) {
3333
}
3434
return FALSE;
3535
}
36+
37+
public boolean isTrue() {
38+
return this.equals(TRUE);
39+
}
40+
41+
public boolean isFalse() {
42+
return this.equals(FALSE);
43+
}
3644
}

python-frontend/src/main/java/org/sonar/python/SubscriptionVisitor.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import org.sonar.python.regex.PythonAnalyzerRegexSource;
5050
import org.sonar.python.regex.PythonRegexIssueLocation;
5151
import org.sonar.python.regex.RegexContext;
52+
import org.sonar.python.semantic.v2.callgraph.CallGraph;
5253
import org.sonar.python.types.v2.TypeChecker;
5354
import org.sonarsource.analyzer.commons.regex.RegexParseResult;
5455
import org.sonarsource.analyzer.commons.regex.RegexParser;
@@ -190,6 +191,12 @@ public ProjectConfiguration projectConfiguration() {
190191
return pythonVisitorContext.projectConfiguration();
191192
}
192193

194+
@Override
195+
public CallGraph callGraph() {
196+
return pythonVisitorContext.callGraph();
197+
}
198+
199+
@Override
193200
public RegexParseResult regexForStringElement(StringElement stringElement, FlagSet flagSet) {
194201
return regexCache.computeIfAbsent(stringElement.hashCode() + "-" + flagSet.getMask(),
195202
s -> new RegexParser(new PythonAnalyzerRegexSource(stringElement), flagSet).parse());
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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.ArrayList;
20+
import java.util.Collections;
21+
import java.util.HashMap;
22+
import java.util.List;
23+
import java.util.Map;
24+
25+
public class CallGraph {
26+
public static final CallGraph EMPTY = new CallGraph(Map.of());
27+
28+
private final Map<String, List<CallGraphNode>> usageEdges;
29+
30+
private CallGraph(Map<String, List<CallGraphNode>> usageEdges) {
31+
this.usageEdges = usageEdges;
32+
}
33+
34+
public List<CallGraphNode> getUsages(String fqn) {
35+
return Collections.unmodifiableList(usageEdges.getOrDefault(fqn, List.of()));
36+
}
37+
38+
public static class Builder {
39+
private Map<String, List<CallGraphNode>> builderUsageEdges = new HashMap<>();
40+
41+
/**
42+
* Adds a usage edge from one function to another in the call graph.
43+
* @param from The calling function's fully qualified name (FQN).
44+
* @param to The functions that is being called
45+
* @return this builder
46+
*/
47+
public Builder addUsage(String from, String to) {
48+
var node = new CallGraphNode(from);
49+
builderUsageEdges.computeIfAbsent(to, k -> new ArrayList<>()).add(node);
50+
return this;
51+
}
52+
53+
public CallGraph build() {
54+
var graph = new CallGraph(builderUsageEdges);
55+
builderUsageEdges = new HashMap<>();
56+
return graph;
57+
}
58+
}
59+
60+
}

0 commit comments

Comments
 (0)