Skip to content

Commit c50ae79

Browse files
maksim-grebeniuk-sonarsourcesonartech
authored andcommitted
SONARPY-3188 Implement a way to provide information about project aws lambda handlers to a rule (#398)
GitOrigin-RevId: 1bd7c5c7ff85ad4e8409e340363ea06511380455
1 parent 45f61b7 commit c50ae79

File tree

13 files changed

+277
-13
lines changed

13 files changed

+277
-13
lines changed

python-checks-testkit/src/main/java/org/sonar/python/checks/utils/PythonCheckVerifier.java

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.sonar.plugins.python.api.PythonCheck.PreciseIssue;
2828
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
2929
import org.sonar.plugins.python.api.PythonVisitorContext;
30+
import org.sonar.plugins.python.api.project.configuration.ProjectConfiguration;
3031
import org.sonar.plugins.python.api.tree.Token;
3132
import org.sonar.plugins.python.api.tree.Trivia;
3233
import org.sonar.python.SubscriptionVisitor;
@@ -57,42 +58,59 @@ public static void verify(String path, PythonCheck check) {
5758

5859
public static void verifyNoIssue(String path, PythonCheck check) {
5960
File file = new File(path);
60-
createVerifier(Collections.singletonList(file), check, ProjectLevelSymbolTable.empty(), null).assertNoIssues();
61+
createVerifier(Collections.singletonList(file), check, ProjectLevelSymbolTable.empty(), null, new ProjectConfiguration())
62+
.assertNoIssues();
6163
}
6264

6365
public static void verify(List<String> paths, PythonCheck check) {
66+
verify(paths, check, new ProjectConfiguration());
67+
}
68+
69+
public static void verify(List<String> paths, PythonCheck check, ProjectConfiguration projectConfiguration) {
6470
List<File> files = paths.stream().map(File::new).toList();
6571
File baseDirFile = new File(files.get(0).getParent());
6672
ProjectLevelSymbolTable projectLevelSymbolTable = TestPythonVisitorRunner.globalSymbols(files, baseDirFile);
67-
createVerifier(files, check, projectLevelSymbolTable, baseDirFile).assertOneOrMoreIssues();
73+
createVerifier(files, check, projectLevelSymbolTable, baseDirFile, projectConfiguration).assertOneOrMoreIssues();
6874
}
6975

7076
public static void verifyNoIssue(List<String> paths, PythonCheck check) {
7177
List<File> files = paths.stream().map(File::new).toList();
7278
File baseDirFile = new File(files.get(0).getParent());
7379
ProjectLevelSymbolTable projectLevelSymbolTable = TestPythonVisitorRunner.globalSymbols(files, baseDirFile);
74-
createVerifier(files, check, projectLevelSymbolTable, baseDirFile).assertNoIssues();
80+
createVerifier(files, check, projectLevelSymbolTable, baseDirFile, new ProjectConfiguration()).assertNoIssues();
7581
}
7682

7783
public static List<PreciseIssue> issues(String path, PythonCheck check) {
7884
File file = new File(path);
79-
PythonVisitorContext context = createContext(file, ProjectLevelSymbolTable.empty(), null);
85+
PythonVisitorContext context = createContext(file, ProjectLevelSymbolTable.empty(), new ProjectConfiguration(), null);
8086
return scanFileForIssues(check, context);
8187
}
8288

83-
private static MultiFileVerifier createVerifier(List<File> files, PythonCheck check, ProjectLevelSymbolTable projectLevelSymbolTable, @Nullable File baseDir) {
89+
private static MultiFileVerifier createVerifier(List<File> files,
90+
PythonCheck check,
91+
ProjectLevelSymbolTable projectLevelSymbolTable,
92+
@Nullable File baseDir,
93+
ProjectConfiguration projectConfiguration) {
8494
MultiFileVerifier multiFileVerifier = MultiFileVerifier.create(files.get(0).toPath(), UTF_8);
8595
for (File file : files) {
86-
PythonVisitorContext context = createContext(file, projectLevelSymbolTable, baseDir);
96+
PythonVisitorContext context = createContext(file, projectLevelSymbolTable, projectConfiguration, baseDir);
8797
addFileIssues(check, multiFileVerifier, file, context);
8898
}
8999
return multiFileVerifier;
90100
}
91101

92-
private static PythonVisitorContext createContext(File file, ProjectLevelSymbolTable projectLevelSymbolTable, @Nullable File baseDir) {
102+
private static PythonVisitorContext createContext(File file,
103+
ProjectLevelSymbolTable projectLevelSymbolTable,
104+
ProjectConfiguration projectConfiguration,
105+
@Nullable File baseDir) {
93106
return baseDir != null
94-
? TestPythonVisitorRunner.createContext(file, null, pythonPackageName(file, baseDir.getAbsolutePath()), projectLevelSymbolTable, CacheContextImpl.dummyCache())
95-
: TestPythonVisitorRunner.createContext(file);
107+
? TestPythonVisitorRunner.createContext(file,
108+
null,
109+
pythonPackageName(file, baseDir.getAbsolutePath()),
110+
projectLevelSymbolTable,
111+
CacheContextImpl.dummyCache(),
112+
projectConfiguration)
113+
: TestPythonVisitorRunner.createContext(file, null, projectConfiguration);
96114
}
97115

98116
private static void addFileIssues(PythonCheck check, MultiFileVerifier multiFileVerifier, File file, PythonVisitorContext context) {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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 org.sonar.plugins.python.api.project.configuration.ProjectConfiguration;
20+
import org.sonar.plugins.python.api.tree.FunctionDef;
21+
import org.sonar.plugins.python.api.types.v2.FunctionType;
22+
23+
public class AwsLambdaChecksUtils {
24+
25+
private AwsLambdaChecksUtils() {
26+
}
27+
28+
public static boolean isLambdaHandler(ProjectConfiguration projectConfiguration, FunctionDef functionDef) {
29+
return functionDef.name().typeV2() instanceof FunctionType functionType
30+
&& projectConfiguration.awsProjectConfiguration()
31+
.awsLambdaHandlers()
32+
.stream()
33+
.anyMatch(handler -> handler.fullyQualifiedName().equals(functionType.fullyQualifiedName()));
34+
}
35+
36+
37+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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 org.junit.jupiter.api.Test;
20+
import org.mockito.Mockito;
21+
import org.sonar.plugins.python.api.project.configuration.AwsLambdaHandlerInfo;
22+
import org.sonar.plugins.python.api.project.configuration.ProjectConfiguration;
23+
import org.sonar.plugins.python.api.tree.FunctionDef;
24+
import org.sonar.plugins.python.api.tree.Name;
25+
import org.sonar.plugins.python.api.types.v2.FunctionType;
26+
import org.sonar.plugins.python.api.types.v2.PythonType;
27+
28+
import static org.assertj.core.api.Assertions.assertThat;
29+
30+
class AwsLambdaCheckUtilsTest {
31+
32+
33+
@Test
34+
void isLambdaHandlerTest() {
35+
var projectConfiguration = new ProjectConfiguration();
36+
37+
var functionNameType = Mockito.mock(FunctionType.class);
38+
var functionName = Mockito.mock(Name.class);
39+
var functionDef = Mockito.mock(FunctionDef.class);
40+
41+
Mockito.when(functionNameType.fullyQualifiedName()).thenReturn("a.b.c");
42+
Mockito.when(functionName.typeV2()).thenReturn(functionNameType);
43+
Mockito.when(functionDef.name()).thenReturn(functionName);
44+
45+
assertThat(AwsLambdaChecksUtils.isLambdaHandler(projectConfiguration, functionDef)).isFalse();
46+
47+
projectConfiguration.awsProjectConfiguration().awsLambdaHandlers().add(new AwsLambdaHandlerInfo("a.b.c"));
48+
assertThat(AwsLambdaChecksUtils.isLambdaHandler(projectConfiguration, functionDef)).isTrue();
49+
50+
Mockito.when(functionName.typeV2()).thenReturn(PythonType.UNKNOWN);
51+
assertThat(AwsLambdaChecksUtils.isLambdaHandler(projectConfiguration, functionDef)).isFalse();
52+
}
53+
54+
}

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.sonar.api.SonarProduct;
2727
import org.sonar.plugins.python.api.PythonCheck.PreciseIssue;
2828
import org.sonar.plugins.python.api.caching.CacheContext;
29+
import org.sonar.plugins.python.api.project.configuration.ProjectConfiguration;
2930
import org.sonar.plugins.python.api.tree.FileInput;
3031
import org.sonar.plugins.python.api.types.v2.ModuleType;
3132
import org.sonar.python.caching.CacheContextImpl;
@@ -43,8 +44,13 @@ public class PythonVisitorContext extends PythonInputFileContext {
4344
private final TypeChecker typeChecker;
4445
private ModuleType moduleType = null;
4546
private final List<PreciseIssue> issues;
47+
private final ProjectConfiguration projectConfiguration;
4648

4749
public PythonVisitorContext(FileInput rootTree, PythonFile pythonFile, @Nullable File workingDirectory, String packageName) {
50+
this(rootTree, pythonFile, workingDirectory, packageName, new ProjectConfiguration());
51+
}
52+
53+
public PythonVisitorContext(FileInput rootTree, PythonFile pythonFile, @Nullable File workingDirectory, String packageName, ProjectConfiguration projectConfiguration) {
4854
super(pythonFile, workingDirectory, CacheContextImpl.dummyCache(), ProjectLevelSymbolTable.empty());
4955
buildSymbols(rootTree, pythonFile, packageName);
5056
var symbolTable = new SymbolTableBuilderV2(rootTree).build();
@@ -54,11 +60,17 @@ public PythonVisitorContext(FileInput rootTree, PythonFile pythonFile, @Nullable
5460
this.parsingException = null;
5561
this.moduleType = new TypeInferenceV2(projectLevelTypeTable, pythonFile, symbolTable, packageName).inferModuleType(rootTree);
5662
this.typeChecker = new TypeChecker(projectLevelTypeTable);
63+
this.projectConfiguration = projectConfiguration;
5764
this.issues = new ArrayList<>();
5865
}
5966

6067
public PythonVisitorContext(FileInput rootTree, PythonFile pythonFile, @Nullable File workingDirectory, String packageName,
6168
ProjectLevelSymbolTable projectLevelSymbolTable, CacheContext cacheContext) {
69+
this(rootTree, pythonFile, workingDirectory, packageName, projectLevelSymbolTable, cacheContext, new ProjectConfiguration());
70+
}
71+
72+
public PythonVisitorContext(FileInput rootTree, PythonFile pythonFile, @Nullable File workingDirectory, String packageName,
73+
ProjectLevelSymbolTable projectLevelSymbolTable, CacheContext cacheContext, ProjectConfiguration projectConfiguration) {
6274
super(pythonFile, workingDirectory, cacheContext, projectLevelSymbolTable);
6375

6476
buildSymbols(rootTree, pythonFile, packageName, projectLevelSymbolTable);
@@ -69,6 +81,7 @@ public PythonVisitorContext(FileInput rootTree, PythonFile pythonFile, @Nullable
6981
this.parsingException = null;
7082
this.moduleType = new TypeInferenceV2(projectLevelTypeTable, pythonFile, symbolTable, packageName).inferModuleType(rootTree);
7183
this.typeChecker = new TypeChecker(projectLevelTypeTable);
84+
this.projectConfiguration = projectConfiguration;
7285
this.issues = new ArrayList<>();
7386
}
7487

@@ -81,6 +94,7 @@ public PythonVisitorContext(FileInput rootTree, PythonFile pythonFile, @Nullable
8194
var projectLevelTypeTable = new ProjectLevelTypeTable(projectLevelSymbolTable);
8295
this.moduleType = new TypeInferenceV2(projectLevelTypeTable, pythonFile, symbolTable, packageName).inferModuleType(rootTree);
8396
this.typeChecker = new TypeChecker(projectLevelTypeTable);
97+
this.projectConfiguration = new ProjectConfiguration();
8498
this.rootTree = rootTree;
8599
this.parsingException = null;
86100
this.issues = new ArrayList<>();
@@ -100,6 +114,7 @@ public PythonVisitorContext(PythonFile pythonFile, RecognitionException parsingE
100114
this.rootTree = null;
101115
this.parsingException = parsingException;
102116
this.typeChecker = new TypeChecker(new ProjectLevelTypeTable(ProjectLevelSymbolTable.empty()));
117+
this.projectConfiguration = new ProjectConfiguration();
103118
this.issues = new ArrayList<>();
104119
}
105120

@@ -108,6 +123,7 @@ public PythonVisitorContext(PythonFile pythonFile, RecognitionException parsingE
108123
this.rootTree = null;
109124
this.parsingException = parsingException;
110125
this.typeChecker = new TypeChecker(new ProjectLevelTypeTable(ProjectLevelSymbolTable.empty()));
126+
this.projectConfiguration = new ProjectConfiguration();
111127
this.issues = new ArrayList<>();
112128
}
113129

@@ -136,4 +152,8 @@ public List<PreciseIssue> getIssues() {
136152
public ModuleType moduleType() {
137153
return moduleType;
138154
}
155+
156+
public ProjectConfiguration projectConfiguration() {
157+
return projectConfiguration;
158+
}
139159
}

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
@@ -23,6 +23,7 @@
2323
import javax.annotation.CheckForNull;
2424
import javax.annotation.Nullable;
2525
import org.sonar.plugins.python.api.caching.CacheContext;
26+
import org.sonar.plugins.python.api.project.configuration.ProjectConfiguration;
2627
import org.sonar.plugins.python.api.symbols.Symbol;
2728
import org.sonar.plugins.python.api.tree.Token;
2829
import org.sonar.plugins.python.api.tree.Tree;
@@ -67,4 +68,6 @@ public interface SubscriptionContext {
6768
CacheContext cacheContext();
6869

6970
TypeChecker typeChecker();
71+
72+
ProjectConfiguration projectConfiguration();
7073
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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.plugins.python.api.project.configuration;
18+
19+
public record AwsLambdaHandlerInfo(String fullyQualifiedName) {
20+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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.plugins.python.api.project.configuration;
18+
19+
import java.util.Set;
20+
21+
public record AwsProjectConfiguration(Set<AwsLambdaHandlerInfo> awsLambdaHandlers) {
22+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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.plugins.python.api.project.configuration;
18+
19+
import java.util.HashSet;
20+
21+
public record ProjectConfiguration(AwsProjectConfiguration awsProjectConfiguration) {
22+
23+
public ProjectConfiguration() {
24+
this(new AwsProjectConfiguration(new HashSet<>()));
25+
}
26+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
@ParametersAreNonnullByDefault
18+
package org.sonar.plugins.python.api.project.configuration;
19+
20+
import javax.annotation.ParametersAreNonnullByDefault;
21+

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import javax.annotation.Nullable;
3232
import org.sonar.plugins.python.api.IssueLocation;
3333
import org.sonar.plugins.python.api.LocationInFile;
34+
import org.sonar.plugins.python.api.project.configuration.ProjectConfiguration;
3435
import org.sonar.plugins.python.api.ProjectPythonVersion;
3536
import org.sonar.plugins.python.api.PythonCheck;
3637
import org.sonar.plugins.python.api.PythonFile;
@@ -184,6 +185,11 @@ public TypeChecker typeChecker() {
184185
return pythonVisitorContext.typeChecker();
185186
}
186187

188+
@Override
189+
public ProjectConfiguration projectConfiguration() {
190+
return pythonVisitorContext.projectConfiguration();
191+
}
192+
187193
public RegexParseResult regexForStringElement(StringElement stringElement, FlagSet flagSet) {
188194
return regexCache.computeIfAbsent(stringElement.hashCode() + "-" + flagSet.getMask(),
189195
s -> new RegexParser(new PythonAnalyzerRegexSource(stringElement), flagSet).parse());

0 commit comments

Comments
 (0)