Skip to content

Commit c61d672

Browse files
SONARPY-1232 Load all Typeshed symbols used by the project in PR analysis context (#1323)
1 parent 16bfd5c commit c61d672

File tree

8 files changed

+103
-3
lines changed

8 files changed

+103
-3
lines changed

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@
2020
package org.sonar.plugins.python.api;
2121

2222
import java.io.File;
23+
import java.util.Collection;
2324
import javax.annotation.CheckForNull;
2425
import javax.annotation.Nullable;
2526
import org.sonar.api.Beta;
2627
import org.sonar.plugins.python.api.caching.CacheContext;
28+
import org.sonar.plugins.python.api.symbols.Symbol;
29+
import org.sonar.python.types.TypeShed;
2730

2831
public class PythonInputFileContext {
2932

@@ -46,6 +49,11 @@ public CacheContext cacheContext() {
4649
return cacheContext;
4750
}
4851

52+
@Beta
53+
public Collection<Symbol> stubFilesSymbols() {
54+
return TypeShed.stubFilesSymbols();
55+
}
56+
4957
@CheckForNull
5058
public File workingDirectory() {
5159
return workingDirectory;

python-frontend/src/main/java/org/sonar/python/types/TypeShed.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,16 @@ public static Collection<Symbol> stubFilesSymbols() {
182182
return symbols;
183183
}
184184

185+
public static Set<String> stubModules() {
186+
Set<String> modules = new HashSet<>();
187+
for (Map.Entry<String, Map<String, Symbol>> entry : typeShedSymbols.entrySet()) {
188+
if (!entry.getValue().isEmpty()) {
189+
modules.add(entry.getKey());
190+
}
191+
}
192+
return modules;
193+
}
194+
185195
public static String normalizedFqn(String fqn) {
186196
if (fqn.startsWith(BUILTINS_PREFIX)) {
187197
return fqn.substring(BUILTINS_PREFIX.length());
@@ -259,7 +269,7 @@ public static SymbolsProtos.ClassSymbol classDescriptorWithFQN(String fullyQuali
259269
//================================================================================
260270

261271
// used by tests whenever 'sonar.python.version' changes
262-
static void resetBuiltinSymbols() {
272+
public static void resetBuiltinSymbols() {
263273
builtins = null;
264274
typeShedSymbols.clear();
265275
builtinSymbols();

python-frontend/src/test/java/org/sonar/python/PythonVisitorCheckTest.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import org.sonar.plugins.python.api.IssueLocation;
3232
import org.sonar.plugins.python.api.PythonCheck;
3333
import org.sonar.plugins.python.api.PythonCheck.PreciseIssue;
34+
import org.sonar.plugins.python.api.PythonFile;
35+
import org.sonar.plugins.python.api.PythonInputFileContext;
3436
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
3537
import org.sonar.plugins.python.api.PythonVisitorCheck;
3638
import org.sonar.plugins.python.api.PythonVisitorContext;
@@ -40,10 +42,12 @@
4042
import org.sonar.plugins.python.api.tree.FunctionDef;
4143
import org.sonar.plugins.python.api.tree.Name;
4244
import org.sonar.plugins.python.api.tree.Tree;
45+
import org.sonar.python.caching.CacheContextImpl;
4346
import org.sonar.python.semantic.ProjectLevelSymbolTable;
4447
import org.sonar.python.types.TypeShed;
4548

4649
import static org.assertj.core.api.Assertions.assertThat;
50+
import static org.mockito.Mockito.mock;
4751

4852
public class PythonVisitorCheckTest {
4953

@@ -143,7 +147,7 @@ public void initialize(SubscriptionCheck.Context context) {
143147
};
144148
File tmpFile = Files.createTempFile("foo", "py").toFile();
145149

146-
var cache = Mockito.mock(CacheContext.class);
150+
var cache = mock(CacheContext.class);
147151

148152
PythonVisitorContext context = TestPythonVisitorRunner.createContext(tmpFile, null, "", ProjectLevelSymbolTable.empty(), cache);
149153
assertThat(context.workingDirectory()).isNull();
@@ -164,6 +168,9 @@ public void stubFilesSymbols() {
164168
SubscriptionVisitor.analyze(Collections.singletonList(check), context);
165169

166170
assertThat(check.symbols).isEqualTo(TypeShed.stubFilesSymbols());
171+
172+
PythonInputFileContext inputFileContext = new PythonInputFileContext(mock(PythonFile.class), null, CacheContextImpl.dummyCache());
173+
assertThat(inputFileContext.stubFilesSymbols()).isEqualTo(TypeShed.stubFilesSymbols());
167174
}
168175

169176
private static class TestPythonCheck extends PythonVisitorCheck {

python-frontend/src/test/java/org/sonar/python/types/TypeShedTest.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,13 @@ public void symbolWithFQN_should_be_consistent() {
474474
assertThat(sequence).isSameAs(typing.get("Sequence"));
475475
}
476476

477+
@Test
478+
public void stubModules() {
479+
TypeShed.symbolsForModule("doesnotexist");
480+
TypeShed.symbolsForModule("math");
481+
assertThat(TypeShed.stubModules()).containsExactly("math");
482+
}
483+
477484
private static SymbolsProtos.ModuleSymbol moduleSymbol(String protobuf) throws TextFormat.ParseException {
478485
SymbolsProtos.ModuleSymbol.Builder builder = SymbolsProtos.ModuleSymbol.newBuilder();
479486
TextFormat.merge(protobuf, builder);

sonar-python-plugin/src/main/java/org/sonar/plugins/python/caching/Caching.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public class Caching {
4343
public static final String IMPORTS_MAP_CACHE_KEY_PREFIX = "python:imports:";
4444
public static final String PROJECT_SYMBOL_TABLE_CACHE_KEY_PREFIX = "python:descriptors:";
4545
public static final String PROJECT_FILES_KEY = "python:files";
46+
public static final String TYPESHED_MODULES_KEY = "python:typeshed_modules";
4647
public static final String CACHE_VERSION_KEY = "python:cache_version";
4748

4849
private static final Logger LOG = Loggers.get(Caching.class);
@@ -65,6 +66,11 @@ public void writeFilesList(List<String> mainFiles) {
6566
cacheContext.getWriteCache().write(PROJECT_FILES_KEY, projectFiles);
6667
}
6768

69+
public void writeTypeshedModules(Set<String> stubModules) {
70+
byte[] stubModulesBytes = String.join(";", stubModules).getBytes(StandardCharsets.UTF_8);
71+
cacheContext.getWriteCache().write(TYPESHED_MODULES_KEY, stubModulesBytes);
72+
}
73+
6874
public void writeCacheVersion() {
6975
cacheContext.getWriteCache().write(CACHE_VERSION_KEY, cacheVersion.getBytes(StandardCharsets.UTF_8));
7076
}
@@ -106,7 +112,15 @@ public Set<String> readImportMapEntry(String moduleFqn) {
106112
}
107113

108114
public Set<String> readFilesList() {
109-
byte[] bytes = cacheContext.getReadCache().readBytes(PROJECT_FILES_KEY);
115+
return readSet(PROJECT_FILES_KEY);
116+
}
117+
118+
public Set<String> readTypeshedModules() {
119+
return readSet(TYPESHED_MODULES_KEY);
120+
}
121+
122+
private Set<String> readSet(String cacheKey) {
123+
byte[] bytes = cacheContext.getReadCache().readBytes(cacheKey);
110124
if (bytes != null) {
111125
return new HashSet<>(Arrays.asList(new String(bytes, StandardCharsets.UTF_8).split(";")));
112126
}

sonar-python-plugin/src/main/java/org/sonar/plugins/python/indexer/SonarQubePythonIndexer.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.sonar.python.index.Descriptor;
3737
import org.sonar.python.semantic.DependencyGraph;
3838
import org.sonar.python.semantic.SymbolUtils;
39+
import org.sonar.python.types.TypeShed;
3940
import org.sonarsource.performance.measure.PerformanceMeasure;
4041

4142
import static org.sonar.plugins.python.api.PythonVersionUtils.PYTHON_VERSION_KEY;
@@ -88,6 +89,7 @@ private boolean shouldOptimizeAnalysis(SensorContext context) {
8889
}
8990

9091
private void computeGlobalSymbolsUsingCache(SensorContext context) {
92+
loadTypeshedSymbols();
9193
LOG.info("Using cached data to retrieve global symbols.");
9294
Set<String> currentProjectModulesFQNs = new HashSet<>(inputFileToFQN.values());
9395
Set<String> deletedModulesFQNs = deletedModulesFQNs(currentProjectModulesFQNs);
@@ -121,6 +123,17 @@ private void computeGlobalSymbolsUsingCache(SensorContext context) {
121123
computeGlobalSymbols(impactfulFiles, context);
122124
}
123125

126+
/*
127+
In a full analysis, Typeshed symbols are loaded lazily depending on which module is encountered during parsing.
128+
SonarSecurity needs all Typeshed symbols used in the project to be properly loaded.
129+
For that reason, we load all symbols that were used in the previous analysis upfront, even if the file using them will not be parsed.
130+
*/
131+
private void loadTypeshedSymbols() {
132+
TypeShed.builtinSymbols();
133+
Set<String> typeShedModules = caching.readTypeshedModules();
134+
typeShedModules.forEach(TypeShed::symbolsForModule);
135+
}
136+
124137
private boolean tryToUseCache(Map<String, Set<String>> importsByModule, InputFile inputFile, String currFQN) {
125138
if (!inputFile.status().equals(InputFile.Status.SAME)) {
126139
return false;
@@ -150,6 +163,10 @@ public void computeGlobalSymbols(List<InputFile> files, SensorContext context) {
150163
if (caching.isCacheEnabled()) {
151164
saveGlobalSymbolsInCache(files);
152165
saveMainFilesListInCache(new HashSet<>(inputFileToFQN.values()));
166+
Set<String> stubModules = TypeShed.stubModules();
167+
if (!stubModules.isEmpty()) {
168+
caching.writeTypeshedModules(stubModules);
169+
}
153170
caching.writeCacheVersion();
154171
}
155172
}

sonar-python-plugin/src/test/java/org/sonar/plugins/python/indexer/SonarQubePythonIndexerTest.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,16 @@
4545
import org.sonar.python.caching.PythonReadCacheImpl;
4646
import org.sonar.python.caching.PythonWriteCacheImpl;
4747
import org.sonar.python.index.VariableDescriptor;
48+
import org.sonar.python.types.TypeShed;
4849

50+
import static org.assertj.core.api.Assertions.as;
4951
import static org.mockito.ArgumentMatchers.any;
5052
import static org.mockito.Mockito.spy;
5153
import static org.mockito.Mockito.when;
5254
import static org.sonar.plugins.python.caching.Caching.CACHE_VERSION_KEY;
5355
import static org.sonar.plugins.python.caching.Caching.PROJECT_FILES_KEY;
5456
import static org.sonar.plugins.python.caching.Caching.PROJECT_SYMBOL_TABLE_CACHE_KEY_PREFIX;
57+
import static org.sonar.plugins.python.caching.Caching.TYPESHED_MODULES_KEY;
5558
import static org.sonar.python.index.DescriptorsToProtobuf.toProtobufModuleDescriptor;
5659
import static org.assertj.core.api.Assertions.assertThat;
5760
import static org.sonar.plugins.python.TestUtils.createInputFile;
@@ -75,6 +78,7 @@ public class SonarQubePythonIndexerTest {
7578

7679
@Before
7780
public void init() throws IOException {
81+
TypeShed.resetBuiltinSymbols();
7882
context = SensorContextTester.create(baseDir);
7983
Path workDir = Files.createTempDirectory("workDir");
8084
context.fileSystem().setWorkDir(workDir);
@@ -375,6 +379,35 @@ public void test_disabled_cache() {
375379
assertThat(logTester.logs(LoggerLevel.INFO)).doesNotContain("Using cached data to retrieve global symbols.");
376380
}
377381

382+
@Test
383+
public void test_typeshed_modules_cached() {
384+
file1 = createInputFile(baseDir, "uses_typeshed.py", InputFile.Status.CHANGED, InputFile.Type.MAIN);
385+
386+
List<InputFile> inputFiles = new ArrayList<>(List.of(file1));
387+
388+
pythonIndexer = new SonarQubePythonIndexer(inputFiles, cacheContext, context);
389+
pythonIndexer.buildOnce(context);
390+
391+
assertThat(pythonIndexer.canBeScannedWithoutParsing(file1)).isFalse();
392+
393+
byte[] bytes = writeCache.getData().get(TYPESHED_MODULES_KEY);
394+
Set<String> resolvedTypeshedModules = new HashSet<>(Arrays.asList(new String(bytes, StandardCharsets.UTF_8).split(";")));
395+
assertThat(resolvedTypeshedModules).containsExactlyInAnyOrder("math");
396+
}
397+
398+
@Test
399+
public void test_typeshed_modules_not_cached_if_empty() {
400+
file1 = createInputFile(baseDir, "main.py", InputFile.Status.CHANGED, InputFile.Type.MAIN);
401+
402+
List<InputFile> inputFiles = new ArrayList<>(List.of(file1));
403+
404+
pythonIndexer = new SonarQubePythonIndexer(inputFiles, cacheContext, context);
405+
pythonIndexer.buildOnce(context);
406+
407+
assertThat(pythonIndexer.canBeScannedWithoutParsing(file1)).isFalse();
408+
assertThat(writeCache.getData()).doesNotContainKey(TYPESHED_MODULES_KEY);
409+
}
410+
378411
@Test
379412
public void test_regular_scan_when_scan_without_parsing_fails() {
380413
List<InputFile> files = List.of(createInputFile(baseDir, "main.py", InputFile.Status.SAME, InputFile.Type.MAIN));
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import math
2+
3+
def func(a):
4+
...

0 commit comments

Comments
 (0)