Skip to content

Commit 0a5e7a4

Browse files
SONARJAVA-4988: Use SonarLintCache component and make it accessible to custom rules via the caching APIs (#4792)
To enable DBD support in SonarLint for Java in VSCode, DBD needs to be able to access the intermediate representation (IR) files it generates for the Java code under analysis. This IR is generated by custom rules for sonar-java which are provided by DBD, and usually it is stored in the file system. However, no file system is available in a SonarLint context. Hence, the IR needs to be transferred in memory. For DBD Python analysis, this has been achieved by utilizing a cache context. I.e. a component SonarLintCache is injected into the Python analyzer frontend, a CacheContext is constructed from it, and DBD’s custom rules store the IR in this cache. Then, when the DBD plugin is executed, it can retrieve the IR from the cache. This PR applies the same change to sonar-java. --- * SONARJAVA-4988: Expose SonarProduct on ModuleScannerContext DBD custom rules need this information to turn off saving IR to the filesystem in a SonarLint context * SONARJAVA-4988: Always provide CacheContext if SonarLintCache is available * SONARJAVA-4988: CacheContexts based on SonarLintCache should not report as a proper cache * SONARJAVA-4988: Permit sensor execution ordering using @DependedUpon annotations
1 parent aad7d6f commit 0a5e7a4

18 files changed

Lines changed: 707 additions & 141 deletions

File tree

java-frontend/src/main/java/org/sonar/java/JavaFrontend.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ long getBatchModeSizeInKB() {
392392
}
393393

394394
private boolean isCacheEnabled() {
395-
return sonarComponents != null && CacheContextImpl.of(sonarComponents.context()).isCacheEnabled();
395+
return sonarComponents != null && CacheContextImpl.of(sonarComponents).isCacheEnabled();
396396
}
397397

398398
private boolean canOptimizeScanning() {

java-frontend/src/main/java/org/sonar/java/SonarComponents.java

Lines changed: 72 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
import org.sonar.plugins.java.api.CheckRegistrar;
7474
import org.sonar.plugins.java.api.JavaCheck;
7575
import org.sonar.plugins.java.api.JspCodeVisitor;
76+
import org.sonar.plugins.java.api.caching.SonarLintCache;
7677
import org.sonarsource.api.sonarlint.SonarLintSide;
7778
import org.sonarsource.sonarlint.plugin.api.SonarLintRuntime;
7879

@@ -117,6 +118,8 @@ public class SonarComponents extends CheckRegistrar.RegistrarContext {
117118
private final ActiveRules activeRules;
118119
@Nullable
119120
private final ProjectDefinition projectDefinition;
121+
@Nullable
122+
private final SonarLintCache sonarLintCache;
120123
private final FileSystem fs;
121124
private final List<JavaCheck> mainChecks;
122125
private final List<JavaCheck> testChecks;
@@ -129,43 +132,85 @@ public class SonarComponents extends CheckRegistrar.RegistrarContext {
129132
private boolean alreadyLoggedSkipStatus = false;
130133

131134
public SonarComponents(FileLinesContextFactory fileLinesContextFactory, FileSystem fs,
132-
ClasspathForMain javaClasspath, ClasspathForTest javaTestClasspath,
133-
CheckFactory checkFactory, ActiveRules activeRules) {
134-
this(fileLinesContextFactory, fs, javaClasspath, javaTestClasspath, checkFactory, activeRules, null, null);
135+
ClasspathForMain javaClasspath, ClasspathForTest javaTestClasspath,
136+
CheckFactory checkFactory, ActiveRules activeRules) {
137+
this(fileLinesContextFactory, fs, javaClasspath, javaTestClasspath, checkFactory, activeRules, null, null, null);
138+
}
139+
140+
/**
141+
* Can be called in SonarLint context when custom rules are present.
142+
*/
143+
public SonarComponents(FileLinesContextFactory fileLinesContextFactory, FileSystem fs,
144+
ClasspathForMain javaClasspath, ClasspathForTest javaTestClasspath, CheckFactory checkFactory,
145+
ActiveRules activeRules, @Nullable CheckRegistrar[] checkRegistrars) {
146+
this(fileLinesContextFactory, fs, javaClasspath, javaTestClasspath, checkFactory, activeRules, checkRegistrars, null, null);
147+
}
148+
149+
/**
150+
* Will *only* be called in SonarLint context and when custom rules are present.
151+
* <p>
152+
* This is because {@link SonarLintCache} is only added as an extension in a SonarLint context.
153+
* See also {@code JavaPlugin#define} in the {@code sonar-java-plugin} module.
154+
* <p>
155+
* {@code SonarLintCache} is used only by newer custom rules, e.g. DBD.
156+
* Thus, for this constructor, we can also assume the presence of {@code CheckRegistrar} instances.
157+
*/
158+
public SonarComponents(FileLinesContextFactory fileLinesContextFactory, FileSystem fs,
159+
ClasspathForMain javaClasspath, ClasspathForTest javaTestClasspath, CheckFactory checkFactory,
160+
ActiveRules activeRules, @Nullable CheckRegistrar[] checkRegistrars, SonarLintCache sonarLintCache) {
161+
this(fileLinesContextFactory, fs, javaClasspath, javaTestClasspath, checkFactory, activeRules, checkRegistrars, null, sonarLintCache);
135162
}
136163

137164
/**
138-
* Will be called in SonarLint context when custom rules are present
165+
* Will be called in SonarScanner context when no custom rules are present.
166+
* May be called in some SonarLint contexts, but not others, since ProjectDefinition might not be available.
139167
*/
140168
public SonarComponents(FileLinesContextFactory fileLinesContextFactory, FileSystem fs,
141-
ClasspathForMain javaClasspath, ClasspathForTest javaTestClasspath, CheckFactory checkFactory,
142-
ActiveRules activeRules, @Nullable CheckRegistrar[] checkRegistrars) {
143-
this(fileLinesContextFactory, fs, javaClasspath, javaTestClasspath, checkFactory, activeRules, checkRegistrars, null);
169+
ClasspathForMain javaClasspath, ClasspathForTest javaTestClasspath, CheckFactory checkFactory,
170+
ActiveRules activeRules, @Nullable ProjectDefinition projectDefinition) {
171+
this(fileLinesContextFactory, fs, javaClasspath, javaTestClasspath, checkFactory, activeRules, null, projectDefinition, null);
144172
}
145173

146174
/**
147-
* Will be called in SonarScanner context when no custom rules is present
175+
* May be called in some SonarLint contexts, but not others, since ProjectDefinition might not be available.
148176
*/
149177
public SonarComponents(FileLinesContextFactory fileLinesContextFactory, FileSystem fs,
150-
ClasspathForMain javaClasspath, ClasspathForTest javaTestClasspath, CheckFactory checkFactory,
151-
ActiveRules activeRules, @Nullable ProjectDefinition projectDefinition) {
152-
this(fileLinesContextFactory, fs, javaClasspath, javaTestClasspath, checkFactory, activeRules,null, projectDefinition);
178+
ClasspathForMain javaClasspath, ClasspathForTest javaTestClasspath, CheckFactory checkFactory,
179+
ActiveRules activeRules, @Nullable CheckRegistrar[] checkRegistrars,
180+
@Nullable ProjectDefinition projectDefinition) {
181+
this(
182+
fileLinesContextFactory,
183+
fs,
184+
javaClasspath,
185+
javaTestClasspath,
186+
checkFactory,
187+
activeRules,
188+
checkRegistrars,
189+
projectDefinition,
190+
null
191+
);
153192
}
154193

194+
155195
/**
156-
* ProjectDefinition class is not available in SonarLint context, so this constructor will never be called when using SonarLint
196+
* All other constructors delegate to this one.
197+
* <p>
198+
* It will also be called directly when constructing a SonarComponents instance for injection if all parameters are available.
199+
* This is for example the case for SonarLint in IntelliJ when DBD is present
200+
* (because ProjectDefinition can be available in recent SonarLint versions, and DBD provides a CheckRegistrar.)
157201
*/
158202
public SonarComponents(FileLinesContextFactory fileLinesContextFactory, FileSystem fs,
159-
ClasspathForMain javaClasspath, ClasspathForTest javaTestClasspath, CheckFactory checkFactory,
160-
ActiveRules activeRules, @Nullable CheckRegistrar[] checkRegistrars,
161-
@Nullable ProjectDefinition projectDefinition) {
203+
ClasspathForMain javaClasspath, ClasspathForTest javaTestClasspath, CheckFactory checkFactory,
204+
ActiveRules activeRules, @Nullable CheckRegistrar[] checkRegistrars,
205+
@Nullable ProjectDefinition projectDefinition, @Nullable SonarLintCache sonarLintCache) {
162206
this.fileLinesContextFactory = fileLinesContextFactory;
163207
this.fs = fs;
164208
this.javaClasspath = javaClasspath;
165209
this.javaTestClasspath = javaTestClasspath;
166210
this.checkFactory = checkFactory;
167211
this.activeRules = activeRules;
168212
this.projectDefinition = projectDefinition;
213+
this.sonarLintCache = sonarLintCache;
169214
this.mainChecks = new ArrayList<>();
170215
this.testChecks = new ArrayList<>();
171216
this.jspChecks = new ArrayList<>();
@@ -341,7 +386,8 @@ void reportIssue(AnalyzerMessage analyzerMessage, RuleKey key, InputComponent fi
341386
if (!textSpan.onLine()) {
342387
Preconditions.checkState(!textSpan.isEmpty(), "Issue location should not be empty");
343388
}
344-
issue.setPrimaryLocation((InputFile) fileOrProject, analyzerMessage.getMessage(), textSpan.startLine, textSpan.startCharacter, textSpan.endLine, textSpan.endCharacter);
389+
issue.setPrimaryLocation((InputFile) fileOrProject, analyzerMessage.getMessage(), textSpan.startLine, textSpan.startCharacter,
390+
textSpan.endLine, textSpan.endCharacter);
345391
}
346392
if (!analyzerMessage.flows.isEmpty()) {
347393
issue.addFlow((InputFile) analyzerMessage.getInputComponent(), analyzerMessage.flows);
@@ -493,7 +539,7 @@ public boolean canSkipUnchangedFiles() throws ApiMismatchException {
493539

494540

495541
public boolean fileCanBeSkipped(InputFile inputFile) {
496-
var contentHashCache = new ContentHashCache(context);
542+
var contentHashCache = new ContentHashCache(this);
497543
if (inputFile instanceof GeneratedFile) {
498544
// Generated files should not be skipped as we cannot assess the change status of the source file
499545
return false;
@@ -513,7 +559,8 @@ public boolean fileCanBeSkipped(InputFile inputFile) {
513559
} catch (ApiMismatchException e) {
514560
if (!alreadyLoggedSkipStatus) {
515561
LOG.info(
516-
"Cannot determine whether the context allows skipping unchanged files: canSkipUnchangedFiles not part of sonar-plugin-api. Not skipping. {}",
562+
"Cannot determine whether the context allows skipping unchanged files: canSkipUnchangedFiles not part of sonar-plugin-api. " +
563+
"Not skipping. {}",
517564
e.getCause().getMessage()
518565
);
519566
alreadyLoggedSkipStatus = true;
@@ -572,7 +619,8 @@ private void logUndefinedTypes(int maxLines) {
572619
);
573620
}
574621

575-
private static void logParserMessages(Stream<Map.Entry<JProblem, List<String>>> messages, int maxProblems, String warningMessage, String debugMessage) {
622+
private static void logParserMessages(Stream<Map.Entry<JProblem, List<String>>> messages, int maxProblems, String warningMessage,
623+
String debugMessage) {
576624
String problemDelimiter = System.lineSeparator() + "- ";
577625
List<List<String>> messagesList = messages
578626
.sorted(Comparator.comparing(entry -> entry.getKey().toString()))
@@ -608,4 +656,9 @@ private static void logParserMessages(Stream<Map.Entry<JProblem, List<String>>>
608656
public SensorContext context() {
609657
return context;
610658
}
659+
660+
@CheckForNull
661+
public SonarLintCache sonarLintCache() {
662+
return sonarLintCache;
663+
}
611664
}

java-frontend/src/main/java/org/sonar/java/caching/CacheContextImpl.java

Lines changed: 65 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@
2424
import org.slf4j.Logger;
2525
import org.slf4j.LoggerFactory;
2626
import org.sonar.api.batch.sensor.SensorContext;
27+
import org.sonar.java.SonarComponents;
2728
import org.sonar.plugins.java.api.caching.CacheContext;
2829
import org.sonar.plugins.java.api.caching.JavaReadCache;
2930
import org.sonar.plugins.java.api.caching.JavaWriteCache;
31+
import org.sonar.plugins.java.api.caching.SonarLintCache;
3032

3133
public class CacheContextImpl implements CacheContext {
3234
/**
@@ -47,36 +49,75 @@ private CacheContextImpl(boolean isCacheEnabled, JavaReadCache readCache, JavaWr
4749
this.writeCache = writeCache;
4850
}
4951

50-
public static CacheContextImpl of(@Nullable SensorContext context) {
51-
52-
if (context != null) {
53-
try {
54-
boolean cacheEnabled =
55-
(context.config() == null ? Optional.<Boolean>empty() : context.config().getBoolean(SONAR_CACHING_ENABLED_KEY))
56-
.map(flag -> {
57-
LOGGER.debug("Forcing caching behavior. Caching will be enabled: {}", flag);
58-
return flag;
59-
})
60-
.orElse(context.isCacheEnabled());
61-
62-
LOGGER.trace("Caching is enabled: {}", cacheEnabled);
63-
64-
if (cacheEnabled) {
65-
return new CacheContextImpl(
66-
true,
67-
new JavaReadCacheImpl(context.previousCache()),
68-
new JavaWriteCacheImpl(context.nextCache())
69-
);
70-
}
71-
} catch (NoSuchMethodError error) {
72-
LOGGER.debug("Missing cache related method from sonar-plugin-api: {}.", error.getMessage());
52+
public static CacheContextImpl of(@Nullable SonarComponents sonarComponents) {
53+
if (sonarComponents == null) {
54+
return dummyCache();
55+
}
56+
57+
// If a SonarLintCache is available, it means we must be running in a SonarLint context, and we should use it,
58+
// regardless of whether settings for caching are enabled or not.
59+
// This is because custom rules (i.e. DBD rules) are depending on SonarLintCache in a SonarLint context.
60+
var sonarLintCache = sonarComponents.sonarLintCache();
61+
if (sonarLintCache != null) {
62+
return fromSonarLintCache(sonarLintCache);
63+
}
64+
65+
var sensorContext = sonarComponents.context();
66+
if (sensorContext == null) {
67+
return dummyCache();
68+
}
69+
70+
try {
71+
var isCachingEnabled = isCachingEnabled(sensorContext);
72+
LOGGER.trace("Caching is enabled: {}", isCachingEnabled);
73+
if (!isCachingEnabled) {
74+
return dummyCache();
7375
}
76+
77+
return fromSensorContext(sensorContext);
78+
} catch (NoSuchMethodError error) {
79+
LOGGER.debug("Missing cache related method from sonar-plugin-api: {}.", error.getMessage());
80+
return dummyCache();
7481
}
82+
}
7583

76-
DummyCache dummyCache = new DummyCache();
84+
private static CacheContextImpl dummyCache() {
85+
var dummyCache = new DummyCache();
7786
return new CacheContextImpl(false, dummyCache, dummyCache);
7887
}
7988

89+
private static CacheContextImpl fromSensorContext(SensorContext context) {
90+
return new CacheContextImpl(
91+
true,
92+
new JavaReadCacheImpl(context.previousCache()),
93+
new JavaWriteCacheImpl(context.nextCache())
94+
);
95+
}
96+
97+
private static CacheContextImpl fromSonarLintCache(SonarLintCache sonarLintCache) {
98+
return new CacheContextImpl(
99+
// SonarLintCache is not an actual cache, but a temporary solution to transferring data between plugins in SonarLint.
100+
// Hence, it should not report that caching is enabled so that no logic which is not aware of SonarLintCache tries to use it like
101+
// a regular cache.
102+
// (However, this means code which is aware of SonarLintCache needs to consciously ignore the `isCacheEnabled` setting where
103+
// appropriate.)
104+
false,
105+
new JavaReadCacheImpl(sonarLintCache),
106+
new JavaWriteCacheImpl(sonarLintCache)
107+
);
108+
}
109+
110+
private static boolean isCachingEnabled(SensorContext context) {
111+
return
112+
Optional.ofNullable(context.config())
113+
.flatMap(config -> config.getBoolean(SONAR_CACHING_ENABLED_KEY))
114+
.map(flag -> {
115+
LOGGER.debug("Forcing caching behavior. Caching will be enabled: {}", flag);
116+
return flag;
117+
})
118+
.orElse(context.isCacheEnabled());
119+
}
120+
80121
@Override
81122
public boolean isCacheEnabled() {
82123
return isCacheEnabled;

java-frontend/src/main/java/org/sonar/java/caching/ContentHashCache.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525
import org.slf4j.Logger;
2626
import org.slf4j.LoggerFactory;
2727
import org.sonar.api.batch.fs.InputFile;
28-
import org.sonar.api.batch.sensor.SensorContext;
2928
import org.sonar.api.batch.sensor.cache.ReadCache;
3029
import org.sonar.api.batch.sensor.cache.WriteCache;
30+
import org.sonar.java.SonarComponents;
3131

3232
public class ContentHashCache {
3333

@@ -39,13 +39,14 @@ public class ContentHashCache {
3939
private WriteCache writeCache;
4040
private final boolean enabled;
4141

42-
public ContentHashCache(SensorContext context) {
43-
CacheContextImpl cacheContext = CacheContextImpl.of(context);
42+
public ContentHashCache(SonarComponents sonarComponents) {
43+
CacheContextImpl cacheContext = CacheContextImpl.of(sonarComponents);
4444
enabled = cacheContext.isCacheEnabled();
4545

46+
var sensorContext = sonarComponents.context();
4647
if (enabled) {
47-
readCache = context.previousCache();
48-
writeCache = context.nextCache();
48+
readCache = sensorContext.previousCache();
49+
writeCache = sensorContext.nextCache();
4950
}
5051
}
5152

java-frontend/src/main/java/org/sonar/java/model/DefaultModuleScannerContext.java

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
package org.sonar.java.model;
2121

2222
import java.io.File;
23+
import javax.annotation.CheckForNull;
2324
import javax.annotation.Nullable;
25+
import org.sonar.api.SonarProduct;
2426
import org.sonar.api.batch.fs.InputComponent;
2527
import org.sonar.java.SonarComponents;
2628
import org.sonar.java.caching.CacheContextImpl;
@@ -36,14 +38,15 @@ public class DefaultModuleScannerContext implements ModuleScannerContext {
3638
protected final boolean inAndroidContext;
3739
protected final CacheContext cacheContext;
3840

39-
public DefaultModuleScannerContext(@Nullable SonarComponents sonarComponents, JavaVersion javaVersion, boolean inAndroidContext, @Nullable CacheContext cacheContext) {
41+
public DefaultModuleScannerContext(@Nullable SonarComponents sonarComponents, JavaVersion javaVersion, boolean inAndroidContext,
42+
@Nullable CacheContext cacheContext) {
4043
this.sonarComponents = sonarComponents;
4144
this.javaVersion = javaVersion;
4245
this.inAndroidContext = inAndroidContext;
4346
if (cacheContext != null) {
4447
this.cacheContext = cacheContext;
4548
} else {
46-
this.cacheContext = CacheContextImpl.of(sonarComponents != null ? sonarComponents.context() : null);
49+
this.cacheContext = CacheContextImpl.of(sonarComponents);
4750
}
4851
}
4952

@@ -85,4 +88,21 @@ public File getRootProjectWorkingDirectory() {
8588
public String getModuleKey() {
8689
return sonarComponents.getModuleKey();
8790
}
91+
92+
@CheckForNull
93+
@Override
94+
public SonarProduct sonarProduct() {
95+
// In production, sonarComponents and sonarComponents.context() should never be null.
96+
// However, in testing contexts, this can happen and calling this method should not cause tests to fail.
97+
if (sonarComponents == null) {
98+
return null;
99+
}
100+
101+
var context = sonarComponents.context();
102+
if (context == null) {
103+
return null;
104+
}
105+
106+
return context.runtime().getProduct();
107+
}
88108
}

java-frontend/src/main/java/org/sonar/java/model/VisitorsBridge.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@
4141
import org.sonar.java.CheckFailureException;
4242
import org.sonar.java.ExceptionHandler;
4343
import org.sonar.java.IllegalRuleParameterException;
44-
import org.sonar.plugins.java.api.JavaVersionAwareVisitor;
4544
import org.sonar.java.SonarComponents;
4645
import org.sonar.java.annotations.VisibleForTesting;
4746
import org.sonar.java.ast.visitors.SonarSymbolTableVisitor;
@@ -55,6 +54,7 @@
5554
import org.sonar.plugins.java.api.JavaFileScanner;
5655
import org.sonar.plugins.java.api.JavaFileScannerContext;
5756
import org.sonar.plugins.java.api.JavaVersion;
57+
import org.sonar.plugins.java.api.JavaVersionAwareVisitor;
5858
import org.sonar.plugins.java.api.ModuleScannerContext;
5959
import org.sonar.plugins.java.api.caching.CacheContext;
6060
import org.sonar.plugins.java.api.internal.EndOfAnalysis;
@@ -97,7 +97,8 @@ public VisitorsBridge(Iterable<? extends JavaCheck> visitors, List<File> project
9797
this.scannersThatCannotBeSkipped = new ArrayList<>();
9898
this.classpath = projectClasspath;
9999
this.sonarComponents = sonarComponents;
100-
this.cacheContext = CacheContextImpl.of(sonarComponents != null ? sonarComponents.context() : null);
100+
this.cacheContext = CacheContextImpl.of(sonarComponents);
101+
101102
this.javaVersion = javaVersion;
102103
updateScanners();
103104
}

0 commit comments

Comments
 (0)