From 0a5e7a4066d1887073ee2faec152a7fc1badecc8 Mon Sep 17 00:00:00 2001 From: Anton Haubner Date: Tue, 4 Jun 2024 09:38:35 +0200 Subject: [PATCH] SONARJAVA-4988: Use SonarLintCache component and make it accessible to custom rules via the caching APIs (#4792) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../java/org/sonar/java/JavaFrontend.java | 2 +- .../java/org/sonar/java/SonarComponents.java | 91 +++++++-- .../sonar/java/caching/CacheContextImpl.java | 89 ++++++--- .../sonar/java/caching/ContentHashCache.java | 11 +- .../model/DefaultModuleScannerContext.java | 24 ++- .../org/sonar/java/model/VisitorsBridge.java | 5 +- .../java/api/ModuleScannerContext.java | 13 +- .../java/api/caching/SonarLintCache.java | 81 ++++++++ .../org/sonar/java/SonarComponentsTest.java | 138 ++++++++++---- .../java/caching/CacheContextImplTest.java | 178 ++++++++++++++---- .../java/caching/ContentHashCacheTest.java | 56 ++++-- .../DefaultModuleScannerContextTest.java | 56 ++++++ .../testing/VisitorsBridgeForTestsTest.java | 2 +- .../java/api/caching/SonarLintCacheTest.java | 89 +++++++++ .../org/sonar/plugins/java/JavaPlugin.java | 4 + .../org/sonar/plugins/java/JavaSensor.java | 2 + .../sonar/plugins/java/JavaPluginTest.java | 5 +- .../sonar/plugins/java/JavaSensorTest.java | 2 +- 18 files changed, 707 insertions(+), 141 deletions(-) create mode 100644 java-frontend/src/main/java/org/sonar/plugins/java/api/caching/SonarLintCache.java create mode 100644 java-frontend/src/test/java/org/sonar/plugins/java/api/caching/SonarLintCacheTest.java diff --git a/java-frontend/src/main/java/org/sonar/java/JavaFrontend.java b/java-frontend/src/main/java/org/sonar/java/JavaFrontend.java index 5c3d49f6dbc..e2f50181957 100644 --- a/java-frontend/src/main/java/org/sonar/java/JavaFrontend.java +++ b/java-frontend/src/main/java/org/sonar/java/JavaFrontend.java @@ -392,7 +392,7 @@ long getBatchModeSizeInKB() { } private boolean isCacheEnabled() { - return sonarComponents != null && CacheContextImpl.of(sonarComponents.context()).isCacheEnabled(); + return sonarComponents != null && CacheContextImpl.of(sonarComponents).isCacheEnabled(); } private boolean canOptimizeScanning() { diff --git a/java-frontend/src/main/java/org/sonar/java/SonarComponents.java b/java-frontend/src/main/java/org/sonar/java/SonarComponents.java index a567a81f665..b77476beb87 100644 --- a/java-frontend/src/main/java/org/sonar/java/SonarComponents.java +++ b/java-frontend/src/main/java/org/sonar/java/SonarComponents.java @@ -73,6 +73,7 @@ import org.sonar.plugins.java.api.CheckRegistrar; import org.sonar.plugins.java.api.JavaCheck; import org.sonar.plugins.java.api.JspCodeVisitor; +import org.sonar.plugins.java.api.caching.SonarLintCache; import org.sonarsource.api.sonarlint.SonarLintSide; import org.sonarsource.sonarlint.plugin.api.SonarLintRuntime; @@ -117,6 +118,8 @@ public class SonarComponents extends CheckRegistrar.RegistrarContext { private final ActiveRules activeRules; @Nullable private final ProjectDefinition projectDefinition; + @Nullable + private final SonarLintCache sonarLintCache; private final FileSystem fs; private final List mainChecks; private final List testChecks; @@ -129,36 +132,77 @@ public class SonarComponents extends CheckRegistrar.RegistrarContext { private boolean alreadyLoggedSkipStatus = false; public SonarComponents(FileLinesContextFactory fileLinesContextFactory, FileSystem fs, - ClasspathForMain javaClasspath, ClasspathForTest javaTestClasspath, - CheckFactory checkFactory, ActiveRules activeRules) { - this(fileLinesContextFactory, fs, javaClasspath, javaTestClasspath, checkFactory, activeRules, null, null); + ClasspathForMain javaClasspath, ClasspathForTest javaTestClasspath, + CheckFactory checkFactory, ActiveRules activeRules) { + this(fileLinesContextFactory, fs, javaClasspath, javaTestClasspath, checkFactory, activeRules, null, null, null); + } + + /** + * Can be called in SonarLint context when custom rules are present. + */ + public SonarComponents(FileLinesContextFactory fileLinesContextFactory, FileSystem fs, + ClasspathForMain javaClasspath, ClasspathForTest javaTestClasspath, CheckFactory checkFactory, + ActiveRules activeRules, @Nullable CheckRegistrar[] checkRegistrars) { + this(fileLinesContextFactory, fs, javaClasspath, javaTestClasspath, checkFactory, activeRules, checkRegistrars, null, null); + } + + /** + * Will *only* be called in SonarLint context and when custom rules are present. + *

+ * This is because {@link SonarLintCache} is only added as an extension in a SonarLint context. + * See also {@code JavaPlugin#define} in the {@code sonar-java-plugin} module. + *

+ * {@code SonarLintCache} is used only by newer custom rules, e.g. DBD. + * Thus, for this constructor, we can also assume the presence of {@code CheckRegistrar} instances. + */ + public SonarComponents(FileLinesContextFactory fileLinesContextFactory, FileSystem fs, + ClasspathForMain javaClasspath, ClasspathForTest javaTestClasspath, CheckFactory checkFactory, + ActiveRules activeRules, @Nullable CheckRegistrar[] checkRegistrars, SonarLintCache sonarLintCache) { + this(fileLinesContextFactory, fs, javaClasspath, javaTestClasspath, checkFactory, activeRules, checkRegistrars, null, sonarLintCache); } /** - * Will be called in SonarLint context when custom rules are present + * Will be called in SonarScanner context when no custom rules are present. + * May be called in some SonarLint contexts, but not others, since ProjectDefinition might not be available. */ public SonarComponents(FileLinesContextFactory fileLinesContextFactory, FileSystem fs, - ClasspathForMain javaClasspath, ClasspathForTest javaTestClasspath, CheckFactory checkFactory, - ActiveRules activeRules, @Nullable CheckRegistrar[] checkRegistrars) { - this(fileLinesContextFactory, fs, javaClasspath, javaTestClasspath, checkFactory, activeRules, checkRegistrars, null); + ClasspathForMain javaClasspath, ClasspathForTest javaTestClasspath, CheckFactory checkFactory, + ActiveRules activeRules, @Nullable ProjectDefinition projectDefinition) { + this(fileLinesContextFactory, fs, javaClasspath, javaTestClasspath, checkFactory, activeRules, null, projectDefinition, null); } /** - * Will be called in SonarScanner context when no custom rules is present + * May be called in some SonarLint contexts, but not others, since ProjectDefinition might not be available. */ public SonarComponents(FileLinesContextFactory fileLinesContextFactory, FileSystem fs, - ClasspathForMain javaClasspath, ClasspathForTest javaTestClasspath, CheckFactory checkFactory, - ActiveRules activeRules, @Nullable ProjectDefinition projectDefinition) { - this(fileLinesContextFactory, fs, javaClasspath, javaTestClasspath, checkFactory, activeRules,null, projectDefinition); + ClasspathForMain javaClasspath, ClasspathForTest javaTestClasspath, CheckFactory checkFactory, + ActiveRules activeRules, @Nullable CheckRegistrar[] checkRegistrars, + @Nullable ProjectDefinition projectDefinition) { + this( + fileLinesContextFactory, + fs, + javaClasspath, + javaTestClasspath, + checkFactory, + activeRules, + checkRegistrars, + projectDefinition, + null + ); } + /** - * ProjectDefinition class is not available in SonarLint context, so this constructor will never be called when using SonarLint + * All other constructors delegate to this one. + *

+ * It will also be called directly when constructing a SonarComponents instance for injection if all parameters are available. + * This is for example the case for SonarLint in IntelliJ when DBD is present + * (because ProjectDefinition can be available in recent SonarLint versions, and DBD provides a CheckRegistrar.) */ public SonarComponents(FileLinesContextFactory fileLinesContextFactory, FileSystem fs, - ClasspathForMain javaClasspath, ClasspathForTest javaTestClasspath, CheckFactory checkFactory, - ActiveRules activeRules, @Nullable CheckRegistrar[] checkRegistrars, - @Nullable ProjectDefinition projectDefinition) { + ClasspathForMain javaClasspath, ClasspathForTest javaTestClasspath, CheckFactory checkFactory, + ActiveRules activeRules, @Nullable CheckRegistrar[] checkRegistrars, + @Nullable ProjectDefinition projectDefinition, @Nullable SonarLintCache sonarLintCache) { this.fileLinesContextFactory = fileLinesContextFactory; this.fs = fs; this.javaClasspath = javaClasspath; @@ -166,6 +210,7 @@ public SonarComponents(FileLinesContextFactory fileLinesContextFactory, FileSyst this.checkFactory = checkFactory; this.activeRules = activeRules; this.projectDefinition = projectDefinition; + this.sonarLintCache = sonarLintCache; this.mainChecks = new ArrayList<>(); this.testChecks = new ArrayList<>(); this.jspChecks = new ArrayList<>(); @@ -341,7 +386,8 @@ void reportIssue(AnalyzerMessage analyzerMessage, RuleKey key, InputComponent fi if (!textSpan.onLine()) { Preconditions.checkState(!textSpan.isEmpty(), "Issue location should not be empty"); } - issue.setPrimaryLocation((InputFile) fileOrProject, analyzerMessage.getMessage(), textSpan.startLine, textSpan.startCharacter, textSpan.endLine, textSpan.endCharacter); + issue.setPrimaryLocation((InputFile) fileOrProject, analyzerMessage.getMessage(), textSpan.startLine, textSpan.startCharacter, + textSpan.endLine, textSpan.endCharacter); } if (!analyzerMessage.flows.isEmpty()) { issue.addFlow((InputFile) analyzerMessage.getInputComponent(), analyzerMessage.flows); @@ -493,7 +539,7 @@ public boolean canSkipUnchangedFiles() throws ApiMismatchException { public boolean fileCanBeSkipped(InputFile inputFile) { - var contentHashCache = new ContentHashCache(context); + var contentHashCache = new ContentHashCache(this); if (inputFile instanceof GeneratedFile) { // Generated files should not be skipped as we cannot assess the change status of the source file return false; @@ -513,7 +559,8 @@ public boolean fileCanBeSkipped(InputFile inputFile) { } catch (ApiMismatchException e) { if (!alreadyLoggedSkipStatus) { LOG.info( - "Cannot determine whether the context allows skipping unchanged files: canSkipUnchangedFiles not part of sonar-plugin-api. Not skipping. {}", + "Cannot determine whether the context allows skipping unchanged files: canSkipUnchangedFiles not part of sonar-plugin-api. " + + "Not skipping. {}", e.getCause().getMessage() ); alreadyLoggedSkipStatus = true; @@ -572,7 +619,8 @@ private void logUndefinedTypes(int maxLines) { ); } - private static void logParserMessages(Stream>> messages, int maxProblems, String warningMessage, String debugMessage) { + private static void logParserMessages(Stream>> messages, int maxProblems, String warningMessage, + String debugMessage) { String problemDelimiter = System.lineSeparator() + "- "; List> messagesList = messages .sorted(Comparator.comparing(entry -> entry.getKey().toString())) @@ -608,4 +656,9 @@ private static void logParserMessages(Stream>> public SensorContext context() { return context; } + + @CheckForNull + public SonarLintCache sonarLintCache() { + return sonarLintCache; + } } diff --git a/java-frontend/src/main/java/org/sonar/java/caching/CacheContextImpl.java b/java-frontend/src/main/java/org/sonar/java/caching/CacheContextImpl.java index 98109d8a13b..8685c43927f 100644 --- a/java-frontend/src/main/java/org/sonar/java/caching/CacheContextImpl.java +++ b/java-frontend/src/main/java/org/sonar/java/caching/CacheContextImpl.java @@ -24,9 +24,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.java.SonarComponents; import org.sonar.plugins.java.api.caching.CacheContext; import org.sonar.plugins.java.api.caching.JavaReadCache; import org.sonar.plugins.java.api.caching.JavaWriteCache; +import org.sonar.plugins.java.api.caching.SonarLintCache; public class CacheContextImpl implements CacheContext { /** @@ -47,36 +49,75 @@ private CacheContextImpl(boolean isCacheEnabled, JavaReadCache readCache, JavaWr this.writeCache = writeCache; } - public static CacheContextImpl of(@Nullable SensorContext context) { - - if (context != null) { - try { - boolean cacheEnabled = - (context.config() == null ? Optional.empty() : context.config().getBoolean(SONAR_CACHING_ENABLED_KEY)) - .map(flag -> { - LOGGER.debug("Forcing caching behavior. Caching will be enabled: {}", flag); - return flag; - }) - .orElse(context.isCacheEnabled()); - - LOGGER.trace("Caching is enabled: {}", cacheEnabled); - - if (cacheEnabled) { - return new CacheContextImpl( - true, - new JavaReadCacheImpl(context.previousCache()), - new JavaWriteCacheImpl(context.nextCache()) - ); - } - } catch (NoSuchMethodError error) { - LOGGER.debug("Missing cache related method from sonar-plugin-api: {}.", error.getMessage()); + public static CacheContextImpl of(@Nullable SonarComponents sonarComponents) { + if (sonarComponents == null) { + return dummyCache(); + } + + // If a SonarLintCache is available, it means we must be running in a SonarLint context, and we should use it, + // regardless of whether settings for caching are enabled or not. + // This is because custom rules (i.e. DBD rules) are depending on SonarLintCache in a SonarLint context. + var sonarLintCache = sonarComponents.sonarLintCache(); + if (sonarLintCache != null) { + return fromSonarLintCache(sonarLintCache); + } + + var sensorContext = sonarComponents.context(); + if (sensorContext == null) { + return dummyCache(); + } + + try { + var isCachingEnabled = isCachingEnabled(sensorContext); + LOGGER.trace("Caching is enabled: {}", isCachingEnabled); + if (!isCachingEnabled) { + return dummyCache(); } + + return fromSensorContext(sensorContext); + } catch (NoSuchMethodError error) { + LOGGER.debug("Missing cache related method from sonar-plugin-api: {}.", error.getMessage()); + return dummyCache(); } + } - DummyCache dummyCache = new DummyCache(); + private static CacheContextImpl dummyCache() { + var dummyCache = new DummyCache(); return new CacheContextImpl(false, dummyCache, dummyCache); } + private static CacheContextImpl fromSensorContext(SensorContext context) { + return new CacheContextImpl( + true, + new JavaReadCacheImpl(context.previousCache()), + new JavaWriteCacheImpl(context.nextCache()) + ); + } + + private static CacheContextImpl fromSonarLintCache(SonarLintCache sonarLintCache) { + return new CacheContextImpl( + // SonarLintCache is not an actual cache, but a temporary solution to transferring data between plugins in SonarLint. + // Hence, it should not report that caching is enabled so that no logic which is not aware of SonarLintCache tries to use it like + // a regular cache. + // (However, this means code which is aware of SonarLintCache needs to consciously ignore the `isCacheEnabled` setting where + // appropriate.) + false, + new JavaReadCacheImpl(sonarLintCache), + new JavaWriteCacheImpl(sonarLintCache) + ); + } + + private static boolean isCachingEnabled(SensorContext context) { + return + Optional.ofNullable(context.config()) + .flatMap(config -> config.getBoolean(SONAR_CACHING_ENABLED_KEY)) + .map(flag -> { + LOGGER.debug("Forcing caching behavior. Caching will be enabled: {}", flag); + return flag; + }) + .orElse(context.isCacheEnabled()); + } + @Override public boolean isCacheEnabled() { return isCacheEnabled; diff --git a/java-frontend/src/main/java/org/sonar/java/caching/ContentHashCache.java b/java-frontend/src/main/java/org/sonar/java/caching/ContentHashCache.java index f75beffb607..8c13796f8d3 100644 --- a/java-frontend/src/main/java/org/sonar/java/caching/ContentHashCache.java +++ b/java-frontend/src/main/java/org/sonar/java/caching/ContentHashCache.java @@ -25,9 +25,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.api.batch.fs.InputFile; -import org.sonar.api.batch.sensor.SensorContext; import org.sonar.api.batch.sensor.cache.ReadCache; import org.sonar.api.batch.sensor.cache.WriteCache; +import org.sonar.java.SonarComponents; public class ContentHashCache { @@ -39,13 +39,14 @@ public class ContentHashCache { private WriteCache writeCache; private final boolean enabled; - public ContentHashCache(SensorContext context) { - CacheContextImpl cacheContext = CacheContextImpl.of(context); + public ContentHashCache(SonarComponents sonarComponents) { + CacheContextImpl cacheContext = CacheContextImpl.of(sonarComponents); enabled = cacheContext.isCacheEnabled(); + var sensorContext = sonarComponents.context(); if (enabled) { - readCache = context.previousCache(); - writeCache = context.nextCache(); + readCache = sensorContext.previousCache(); + writeCache = sensorContext.nextCache(); } } diff --git a/java-frontend/src/main/java/org/sonar/java/model/DefaultModuleScannerContext.java b/java-frontend/src/main/java/org/sonar/java/model/DefaultModuleScannerContext.java index 9e8d8f15557..25c08985df7 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/DefaultModuleScannerContext.java +++ b/java-frontend/src/main/java/org/sonar/java/model/DefaultModuleScannerContext.java @@ -20,7 +20,9 @@ package org.sonar.java.model; import java.io.File; +import javax.annotation.CheckForNull; import javax.annotation.Nullable; +import org.sonar.api.SonarProduct; import org.sonar.api.batch.fs.InputComponent; import org.sonar.java.SonarComponents; import org.sonar.java.caching.CacheContextImpl; @@ -36,14 +38,15 @@ public class DefaultModuleScannerContext implements ModuleScannerContext { protected final boolean inAndroidContext; protected final CacheContext cacheContext; - public DefaultModuleScannerContext(@Nullable SonarComponents sonarComponents, JavaVersion javaVersion, boolean inAndroidContext, @Nullable CacheContext cacheContext) { + public DefaultModuleScannerContext(@Nullable SonarComponents sonarComponents, JavaVersion javaVersion, boolean inAndroidContext, + @Nullable CacheContext cacheContext) { this.sonarComponents = sonarComponents; this.javaVersion = javaVersion; this.inAndroidContext = inAndroidContext; if (cacheContext != null) { this.cacheContext = cacheContext; } else { - this.cacheContext = CacheContextImpl.of(sonarComponents != null ? sonarComponents.context() : null); + this.cacheContext = CacheContextImpl.of(sonarComponents); } } @@ -85,4 +88,21 @@ public File getRootProjectWorkingDirectory() { public String getModuleKey() { return sonarComponents.getModuleKey(); } + + @CheckForNull + @Override + public SonarProduct sonarProduct() { + // In production, sonarComponents and sonarComponents.context() should never be null. + // However, in testing contexts, this can happen and calling this method should not cause tests to fail. + if (sonarComponents == null) { + return null; + } + + var context = sonarComponents.context(); + if (context == null) { + return null; + } + + return context.runtime().getProduct(); + } } diff --git a/java-frontend/src/main/java/org/sonar/java/model/VisitorsBridge.java b/java-frontend/src/main/java/org/sonar/java/model/VisitorsBridge.java index 430471f9821..264cb4ee314 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/VisitorsBridge.java +++ b/java-frontend/src/main/java/org/sonar/java/model/VisitorsBridge.java @@ -41,7 +41,6 @@ import org.sonar.java.CheckFailureException; import org.sonar.java.ExceptionHandler; import org.sonar.java.IllegalRuleParameterException; -import org.sonar.plugins.java.api.JavaVersionAwareVisitor; import org.sonar.java.SonarComponents; import org.sonar.java.annotations.VisibleForTesting; import org.sonar.java.ast.visitors.SonarSymbolTableVisitor; @@ -55,6 +54,7 @@ import org.sonar.plugins.java.api.JavaFileScanner; import org.sonar.plugins.java.api.JavaFileScannerContext; import org.sonar.plugins.java.api.JavaVersion; +import org.sonar.plugins.java.api.JavaVersionAwareVisitor; import org.sonar.plugins.java.api.ModuleScannerContext; import org.sonar.plugins.java.api.caching.CacheContext; import org.sonar.plugins.java.api.internal.EndOfAnalysis; @@ -97,7 +97,8 @@ public VisitorsBridge(Iterable visitors, List project this.scannersThatCannotBeSkipped = new ArrayList<>(); this.classpath = projectClasspath; this.sonarComponents = sonarComponents; - this.cacheContext = CacheContextImpl.of(sonarComponents != null ? sonarComponents.context() : null); + this.cacheContext = CacheContextImpl.of(sonarComponents); + this.javaVersion = javaVersion; updateScanners(); } diff --git a/java-frontend/src/main/java/org/sonar/plugins/java/api/ModuleScannerContext.java b/java-frontend/src/main/java/org/sonar/plugins/java/api/ModuleScannerContext.java index 5872138b72b..baa65831d2d 100644 --- a/java-frontend/src/main/java/org/sonar/plugins/java/api/ModuleScannerContext.java +++ b/java-frontend/src/main/java/org/sonar/plugins/java/api/ModuleScannerContext.java @@ -20,6 +20,9 @@ package org.sonar.plugins.java.api; import java.io.File; +import javax.annotation.CheckForNull; +import org.sonar.api.SonarProduct; +import org.sonar.api.SonarRuntime; import org.sonar.api.batch.fs.InputComponent; import org.sonar.plugins.java.api.caching.CacheContext; @@ -43,8 +46,8 @@ public interface ModuleScannerContext { /** * The working directory used by the analysis. - * @return the current working directory. * + * @return the current working directory. * @deprecated use {@link #getRootProjectWorkingDirectory()} instead */ @Deprecated(since = "7.12") @@ -80,4 +83,12 @@ public interface ModuleScannerContext { * @return A key that uniquely identifies the current module, provided that this project consists of multiple modules. */ String getModuleKey(); + + /** + * @return The Sonar product (SONARQUBE/SONARLINT) which forms the current execution context of the scan. + * See also {@link SonarRuntime#getProduct()}. + * In a production environment, this method never returns null but in testing contexts, it may happen. + */ + @CheckForNull + SonarProduct sonarProduct(); } diff --git a/java-frontend/src/main/java/org/sonar/plugins/java/api/caching/SonarLintCache.java b/java-frontend/src/main/java/org/sonar/plugins/java/api/caching/SonarLintCache.java new file mode 100644 index 00000000000..fa5d4d91621 --- /dev/null +++ b/java-frontend/src/main/java/org/sonar/plugins/java/api/caching/SonarLintCache.java @@ -0,0 +1,81 @@ +/* + * SonarQube Java + * Copyright (C) 2012-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.plugins.java.api.caching; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import org.sonar.api.Beta; +import org.sonar.api.batch.sensor.cache.ReadCache; +import org.sonar.api.batch.sensor.cache.WriteCache; +import org.sonarsource.api.sonarlint.SonarLintSide; + +/** + * Component used in SonarLint to transfer data in memory between plugins. + * At the time of writing, this is used only by the DBD plugin, to consume IRs produced by DBD custom rules in SonarLint context. + * This component is just an intermediate solution until a dedicated mechanism to communicate between plugins with sufficient capabilities + * is available. + *

+ * By default, this component has {@code SINGLE_ANALYSIS} lifetime, meaning that it does not need to be manually cleared after analysis. + */ +@SonarLintSide() +@Beta +public class SonarLintCache implements ReadCache, WriteCache { + + private final Map cache = new HashMap<>(); + + + @Override + public InputStream read(String s) { + if (!contains(s)) { + throw new IllegalArgumentException(String.format("SonarLintCache does not contain key \"%s\"", s)); + } + return new ByteArrayInputStream(cache.get(s)); + } + + @Override + public boolean contains(String s) { + return cache.containsKey(s); + } + + @Override + public void write(String s, InputStream inputStream) { + try { + write(s, inputStream.readAllBytes()); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + @Override + public void write(String s, byte[] bytes) { + if (contains(s)) { + throw new IllegalArgumentException(String.format("Same key cannot be written to multiple times (%s)", s)); + } + cache.put(s, bytes); + } + + @Override + public void copyFromPrevious(String s) { + throw new UnsupportedOperationException("SonarLintCache does not allow to copy from previous."); + } +} diff --git a/java-frontend/src/test/java/org/sonar/java/SonarComponentsTest.java b/java-frontend/src/test/java/org/sonar/java/SonarComponentsTest.java index 33da233c3dc..672013eba1d 100644 --- a/java-frontend/src/test/java/org/sonar/java/SonarComponentsTest.java +++ b/java-frontend/src/test/java/org/sonar/java/SonarComponentsTest.java @@ -85,6 +85,7 @@ import org.sonar.plugins.java.api.CheckRegistrar; import org.sonar.plugins.java.api.JavaCheck; import org.sonar.plugins.java.api.JspCodeVisitor; +import org.sonar.plugins.java.api.caching.SonarLintCache; import org.sonarsource.sonarlint.core.plugin.commons.sonarapi.SonarLintRuntimeImpl; import static org.assertj.core.api.Assertions.assertThat; @@ -115,10 +116,14 @@ class SonarComponentsTest { private static final String REPOSITORY_NAME = "custom"; - private static final String LOG_MESSAGE_FILES_CAN_BE_SKIPPED = "The Java analyzer is running in a context where unchanged files can be skipped. Full analysis is performed " + - "for changed files, optimized analysis for unchanged files."; - private static final String LOG_MESSAGE_FILES_CANNOT_BE_SKIPPED = "The Java analyzer cannot skip unchanged files in this context. A full analysis is performed for all files."; - private static final String LOG_MESSAGE_CANNOT_DETERMINE_IF_FILES_CAN_BE_SKIPPED = "Cannot determine whether the context allows skipping unchanged files: canSkipUnchangedFiles not part of sonar-plugin-api. Not skipping. {}"; + private static final String LOG_MESSAGE_FILES_CAN_BE_SKIPPED = + "The Java analyzer is running in a context where unchanged files can be skipped. " + + "Full analysis is performed for changed files, optimized analysis for unchanged files."; + private static final String LOG_MESSAGE_FILES_CANNOT_BE_SKIPPED = + "The Java analyzer cannot skip unchanged files in this context. A full analysis is performed for all files."; + private static final String LOG_MESSAGE_CANNOT_DETERMINE_IF_FILES_CAN_BE_SKIPPED = + "Cannot determine whether the context allows skipping unchanged files: " + + "canSkipUnchangedFiles not part of sonar-plugin-api. Not skipping. {}"; private static final String DEFAULT_PATH = Path.of("src", "main", "java", "com", "acme", "Source.java").toString(); @@ -160,7 +165,8 @@ void base_and_work_directories() { DefaultFileSystem fs = context.fileSystem(); fs.setWorkDir(workDir.toPath()); - SonarComponents sonarComponents = new SonarComponents(fileLinesContextFactory, fs, null, mock(ClasspathForTest.class), checkFactory, context.activeRules()); + SonarComponents sonarComponents = new SonarComponents( + fileLinesContextFactory, fs, null, mock(ClasspathForTest.class), checkFactory, context.activeRules()); assertThat(sonarComponents.projectLevelWorkDir()).isEqualTo(workDir); } @@ -358,14 +364,14 @@ class RuleF implements JavaCheck { } SonarComponents sonarComponents = new SonarComponents(fileLinesContextFactory, null, null, - null, checkFactory, activeRules, new CheckRegistrar[] { - ctx -> ctx.registerMainSharedCheck(new RuleA(), ruleKeys("java:S404", "java:S102")), - ctx -> ctx.registerMainSharedCheck(new RuleB(), ruleKeys("java:S404", "java:S500")), - ctx -> ctx.registerMainSharedCheck(new RuleC(), ruleKeys("java:S101", "java:S102")), - ctx -> ctx.registerTestSharedCheck(new RuleD(), ruleKeys("java:S404", "java:S405", "java:S406")), - ctx -> ctx.registerTestSharedCheck(new RuleE(), ruleKeys("java:S102")), - ctx -> ctx.registerTestSharedCheck(new RuleF(), List.of()) - }); + null, checkFactory, activeRules, new CheckRegistrar[]{ + ctx -> ctx.registerMainSharedCheck(new RuleA(), ruleKeys("java:S404", "java:S102")), + ctx -> ctx.registerMainSharedCheck(new RuleB(), ruleKeys("java:S404", "java:S500")), + ctx -> ctx.registerMainSharedCheck(new RuleC(), ruleKeys("java:S101", "java:S102")), + ctx -> ctx.registerTestSharedCheck(new RuleD(), ruleKeys("java:S404", "java:S405", "java:S406")), + ctx -> ctx.registerTestSharedCheck(new RuleE(), ruleKeys("java:S102")), + ctx -> ctx.registerTestSharedCheck(new RuleF(), List.of()) + }); sonarComponents.setSensorContext(context); assertThat(sonarComponents.mainChecks()) @@ -391,13 +397,13 @@ class RuleB implements JavaCheck { class RuleC implements JavaCheck { } SonarComponents sonarComponents = new SonarComponents(fileLinesContextFactory, null, null, - null, checkFactory, activeRules, new CheckRegistrar[] { - ctx -> ctx.registerMainChecks("java", List.of( - new RuleA(), - new RuleC())), - ctx -> ctx.registerTestChecks("java", List.of( - new RuleB())) - }); + null, checkFactory, activeRules, new CheckRegistrar[]{ + ctx -> ctx.registerMainChecks("java", List.of( + new RuleA(), + new RuleC())), + ctx -> ctx.registerTestChecks("java", List.of( + new RuleB())) + }); sonarComponents.setSensorContext(context); assertThat(sonarComponents.mainChecks()) @@ -415,7 +421,7 @@ void auto_scan_compatible_rules() { SensorContextTester context = SensorContextTester.create(new File(".")).setActiveRules(activeRules); SonarComponents sonarComponents = new SonarComponents(fileLinesContextFactory, null, null, - null, checkFactory, activeRules, new CheckRegistrar[] { + null, checkFactory, activeRules, new CheckRegistrar[]{ ctx -> ctx.registerAutoScanCompatibleRules(ruleKeys("java:S101", "java:S102")), ctx -> ctx.registerAutoScanCompatibleRules(ruleKeys("javabugs:S200")) }); @@ -705,7 +711,8 @@ void batch_getters() { assertThat(sonarComponents.getBatchModeSizeInKB()).isEqualTo(1000L); // Deprecated autoscan key returns default value - // Note: it means that if someone used this key outside an autoscan context, the project will be analyzed in a single batch (unless batch size is specified) + // Note: it means that if someone used this key outside an autoscan context, the project will be analyzed in a single batch + // (unless batch size is specified) settings.clear(); settings.setProperty("sonar.java.internal.batchMode", "true"); assertThat(sonarComponents.getBatchModeSizeInKB()).isEqualTo(-1L); @@ -921,7 +928,8 @@ private static Stream provideInputsFor_canSkipUnchangedFiles() { @ParameterizedTest @MethodSource("provideInputsFor_canSkipUnchangedFiles") - void canSkipUnchangedFiles(@CheckForNull Boolean overrideFlagVal, @CheckForNull Boolean apiResponseVal, @CheckForNull Boolean expectedResult) throws ApiMismatchException { + void canSkipUnchangedFiles(@CheckForNull Boolean overrideFlagVal, @CheckForNull Boolean apiResponseVal, + @CheckForNull Boolean expectedResult) throws ApiMismatchException { SensorContextTester sensorContextTester = SensorContextTester.create(new File("")); SonarComponents sonarComponents = new SonarComponents( fileLinesContextFactory, @@ -999,12 +1007,14 @@ void log_only_50_undefined_types() { String source = generateSource(26); // artificially populated the semantic errors with 26 unknown types and 52 errors - sonarComponents.collectUndefinedTypes(DEFAULT_PATH, ((JavaTree.CompilationUnitTreeImpl) JParserTestUtils.parse(source)).sema.undefinedTypes()); + sonarComponents.collectUndefinedTypes(DEFAULT_PATH, + ((JavaTree.CompilationUnitTreeImpl) JParserTestUtils.parse(source)).sema.undefinedTypes()); // triggers log sonarComponents.logUndefinedTypes(); - assertThat(logTester.logs(Level.WARN)).containsExactly("Unresolved imports/types have been detected during analysis. Enable DEBUG mode to see them."); + assertThat(logTester.logs(Level.WARN)).containsExactly( + "Unresolved imports/types have been detected during analysis. Enable DEBUG mode to see them."); List debugLogs = logTester.logs(Level.DEBUG); assertThat(debugLogs).hasSize(1); @@ -1028,7 +1038,8 @@ void log_only_50_undefined_types() { void remove_info_and_warning_from_log_related_to_undefined_types() { logTester.setLevel(Level.ERROR); String source = generateSource(26); - sonarComponents.collectUndefinedTypes(DEFAULT_PATH, ((JavaTree.CompilationUnitTreeImpl) JParserTestUtils.parse(source)).sema.undefinedTypes()); + sonarComponents.collectUndefinedTypes(DEFAULT_PATH, + ((JavaTree.CompilationUnitTreeImpl) JParserTestUtils.parse(source)).sema.undefinedTypes()); sonarComponents.logUndefinedTypes(); assertThat(logTester.logs(Level.WARN)).isEmpty(); @@ -1040,12 +1051,14 @@ void log_all_undefined_types_if_less_than_threshold() { String source = generateSource(1); // artificially populated the semantic errors with 1 unknown types and 2 errors - sonarComponents.collectUndefinedTypes(DEFAULT_PATH, ((JavaTree.CompilationUnitTreeImpl) JParserTestUtils.parse(source)).sema.undefinedTypes()); + sonarComponents.collectUndefinedTypes(DEFAULT_PATH, + ((JavaTree.CompilationUnitTreeImpl) JParserTestUtils.parse(source)).sema.undefinedTypes()); // triggers log sonarComponents.logUndefinedTypes(); - assertThat(logTester.logs(Level.WARN)).containsExactly("Unresolved imports/types have been detected during analysis. Enable DEBUG mode to see them."); + assertThat(logTester.logs(Level.WARN)).containsExactly( + "Unresolved imports/types have been detected during analysis. Enable DEBUG mode to see them."); List debugLogs = logTester.logs(Level.DEBUG); assertThat(debugLogs).hasSize(1); @@ -1062,8 +1075,10 @@ void suspicious_empty_libraries_should_be_logged() { logUndefinedTypesWithOneMainAndOneTest(); assertThat(logTester.logs(Level.WARN)) - .contains("Dependencies/libraries were not provided for analysis of SOURCE files. The 'sonar.java.libraries' property is empty. Verify your configuration, as you might end up with less precise results.") - .contains("Dependencies/libraries were not provided for analysis of TEST files. The 'sonar.java.test.libraries' property is empty. Verify your configuration, as you might end up with less precise results."); + .contains("Dependencies/libraries were not provided for analysis of SOURCE files. The 'sonar.java.libraries' property is empty. " + + "Verify your configuration, as you might end up with less precise results.") + .contains("Dependencies/libraries were not provided for analysis of TEST files. " + + "The 'sonar.java.test.libraries' property is empty. Verify your configuration, as you might end up with less precise results."); } @Test @@ -1074,8 +1089,10 @@ void suspicious_empty_libraries_should_not_be_logged_in_autoscan() { logUndefinedTypesWithOneMainAndOneTest(); assertThat(logTester.logs(Level.WARN)) - .contains("Dependencies/libraries were not provided for analysis of SOURCE files. The 'sonar.java.libraries' property is empty. Verify your configuration, as you might end up with less precise results.") - .doesNotContain("Dependencies/libraries were not provided for analysis of TEST files. The 'sonar.java.test.libraries' property is empty. Verify your configuration, as you might end up with less precise results."); + .contains("Dependencies/libraries were not provided for analysis of SOURCE files. The 'sonar.java.libraries' property is empty. " + + "Verify your configuration, as you might end up with less precise results.") + .doesNotContain("Dependencies/libraries were not provided for analysis of TEST files. " + + "The 'sonar.java.test.libraries' property is empty. Verify your configuration, as you might end up with less precise results."); } @Test @@ -1089,8 +1106,10 @@ void log_problems_with_list_of_paths_of_files_affected() { fs.add(testFile); // artificially populated the semantic errors with 1 unknown types and 2 errors - sonarComponents.collectUndefinedTypes(mainFile.toString(), ((JavaTree.CompilationUnitTreeImpl) JParserTestUtils.parse(source)).sema.undefinedTypes()); - sonarComponents.collectUndefinedTypes(testFile.toString(), ((JavaTree.CompilationUnitTreeImpl) JParserTestUtils.parse(source)).sema.undefinedTypes()); + sonarComponents.collectUndefinedTypes(mainFile.toString(), + ((JavaTree.CompilationUnitTreeImpl) JParserTestUtils.parse(source)).sema.undefinedTypes()); + sonarComponents.collectUndefinedTypes(testFile.toString(), + ((JavaTree.CompilationUnitTreeImpl) JParserTestUtils.parse(source)).sema.undefinedTypes()); sonarComponents.logUndefinedTypes(); List debugMessage = logTester.logs(Level.DEBUG); @@ -1122,7 +1141,8 @@ private void logUndefinedTypesWithOneMainAndOneTest() { fs.add(TestUtils.emptyInputFile("fooTest.java", InputFile.Type.TEST)); // artificially populated the semantic errors with 1 unknown types and 2 errors - sonarComponents.collectUndefinedTypes(DEFAULT_PATH, ((JavaTree.CompilationUnitTreeImpl) JParserTestUtils.parse(source)).sema.undefinedTypes()); + sonarComponents.collectUndefinedTypes(DEFAULT_PATH, + ((JavaTree.CompilationUnitTreeImpl) JParserTestUtils.parse(source)).sema.undefinedTypes()); // Call these methods to initiate Main and Test ClassPath sonarComponents.getJavaClasspath(); @@ -1167,7 +1187,7 @@ void should_return_generated_code_visitors() throws Exception { JspCodeCheck check = new JspCodeCheck(); SonarComponents sonarComponents = new SonarComponents(null, null, null, null, - checkFactory, context.activeRules(), new CheckRegistrar[] {getRegistrar(check)}); + checkFactory, context.activeRules(), new CheckRegistrar[]{getRegistrar(check)}); List checks = sonarComponents.jspChecks(); assertThat(checks) .isNotEmpty() @@ -1223,4 +1243,50 @@ private static ActiveRules activeRules(String... repositoryAndKeys) { } return activeRules.build(); } + + @Test + void should_return_sonarlint_cache_if_initialized_with_a_sonarlint_cache() { + var sonarLintCache = mock(SonarLintCache.class); + + var sonarComponentsWithoutSonarLintCache = new SonarComponents( + null, + null, + null, + null, + null, + null, + null, + null, + null + ); + + assertThat(sonarComponentsWithoutSonarLintCache.sonarLintCache()).isNull(); + + var sonarComponentsWithSonarLintCache01 = new SonarComponents( + null, + null, + null, + null, + null, + null, + null, + sonarLintCache + ); + + assertThat(sonarComponentsWithSonarLintCache01.sonarLintCache()).isSameAs(sonarLintCache); + + var sonarComponentsWithSonarLintCache02 = new SonarComponents( + null, + null, + null, + null, + null, + null, + null, + null, + sonarLintCache + ); + + assertThat(sonarComponentsWithSonarLintCache02.sonarLintCache()).isSameAs(sonarLintCache); + } } diff --git a/java-frontend/src/test/java/org/sonar/java/caching/CacheContextImplTest.java b/java-frontend/src/test/java/org/sonar/java/caching/CacheContextImplTest.java index bd3011beecd..cc43d869075 100644 --- a/java-frontend/src/test/java/org/sonar/java/caching/CacheContextImplTest.java +++ b/java-frontend/src/test/java/org/sonar/java/caching/CacheContextImplTest.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.Optional; +import javax.annotation.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.slf4j.event.Level; @@ -30,68 +31,86 @@ import org.sonar.api.config.Configuration; import org.sonar.api.testfixtures.log.LogAndArguments; import org.sonar.api.testfixtures.log.LogTesterJUnit5; +import org.sonar.java.SonarComponents; +import org.sonar.plugins.java.api.caching.CacheContext; +import org.sonar.plugins.java.api.caching.SonarLintCache; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; class CacheContextImplTest { @RegisterExtension LogTesterJUnit5 logTester = new LogTesterJUnit5().setLevel(Level.DEBUG); + @Test + void should_use_dummy_cache_if_sonarcomponents_is_unavailable() { + var cci = CacheContextImpl.of(null); + + verifyCacheContextUsesDummyCache(cci); + } + + @Test + void should_use_dummy_cache_if_neither_sonarlint_cache_nor_sensor_context_cache_are_available() { + var sonarComponents = mockSonarComponents(null, null); + var cci = CacheContextImpl.of(sonarComponents); + + verifyCacheContextUsesDummyCache(cci); + } + @Test void isCacheEnabled_returns_true_when_context_implements_isCacheEnabled_and_is_true() { - SensorContext sensorContext = mock(SensorContext.class); + var sensorContext = mockSensorContext(); doReturn(true).when(sensorContext).isCacheEnabled(); - var readCache = mock(ReadCache.class); - doReturn(readCache).when(sensorContext).previousCache(); - var writeCache = mock(WriteCache.class); - doReturn(writeCache).when(sensorContext).nextCache(); - CacheContextImpl cci = CacheContextImpl.of(sensorContext); + var sonarComponents = mockSonarComponents(sensorContext, null); + + CacheContextImpl cci = CacheContextImpl.of(sonarComponents); assertThat(cci.isCacheEnabled()).isTrue(); - assertThat(cci.getReadCache()).isEqualTo(new JavaReadCacheImpl(readCache)); - assertThat(cci.getWriteCache()).isEqualTo(new JavaWriteCacheImpl(writeCache)); + assertThat(cci.getReadCache()).isEqualTo(new JavaReadCacheImpl(sensorContext.previousCache())); + assertThat(cci.getWriteCache()).isEqualTo(new JavaWriteCacheImpl(sensorContext.nextCache())); } @Test void isCacheEnabled_returns_false_when_appropriate() { - SensorContext sensorContext = mock(SensorContext.class); + var sensorContext = mockSensorContext(); doReturn(false).when(sensorContext).isCacheEnabled(); - var readCache = mock(ReadCache.class); - doReturn(readCache).when(sensorContext).previousCache(); - var writeCache = mock(WriteCache.class); - doReturn(writeCache).when(sensorContext).nextCache(); + var sonarComponents = mockSonarComponents(sensorContext, null); - CacheContextImpl cci = CacheContextImpl.of(sensorContext); + CacheContextImpl cci = CacheContextImpl.of(sonarComponents); assertThat(cci.isCacheEnabled()).isFalse(); - assertThat(cci.getReadCache()).isInstanceOf(DummyCache.class); - assertThat(cci.getWriteCache()).isInstanceOf(DummyCache.class); + verifyCacheContextUsesDummyCache(cci); } @Test void isCacheEnabled_returns_false_in_case_of_api_mismatch() { - SensorContext sensorContext = mock(SensorContext.class); + var sensorContext = mockSensorContext(); doThrow(new NoSuchMethodError("boom")).when(sensorContext).isCacheEnabled(); - CacheContextImpl cci = CacheContextImpl.of(sensorContext); + var sonarComponents = mockSonarComponents(sensorContext, null); + + CacheContextImpl cci = CacheContextImpl.of(sonarComponents); assertThat(cci.isCacheEnabled()).isFalse(); - assertThat(cci.getReadCache()) - .isInstanceOf(DummyCache.class) - .isSameAs(cci.getWriteCache()); + verifyCacheContextUsesDummyCache(cci); } @Test void of_logs_at_debug_level_when_the_api_is_not_supported() { logTester.setLevel(Level.DEBUG); - SensorContext sensorContext = mock(SensorContext.class); + + var sensorContext = mockSensorContext(); doThrow(new NoSuchMethodError("bim")).when(sensorContext).isCacheEnabled(); - CacheContextImpl.of(sensorContext); + + var sonarComponents = mockSonarComponents(sensorContext, null); + + CacheContextImpl.of(sonarComponents); List logs = logTester.getLogs(Level.DEBUG).stream() .map(LogAndArguments::getFormattedMsg) .toList(); @@ -102,31 +121,120 @@ void of_logs_at_debug_level_when_the_api_is_not_supported() { @Test void override_flag_caching_enabled_true() { - SensorContext context = mock(SensorContext.class); + var context = mockSensorContext(); doReturn(false).when(context).isCacheEnabled(); - doReturn(mock(ReadCache.class)).when(context).previousCache(); - doReturn(mock(WriteCache.class)).when(context).nextCache(); - Configuration config = mock(Configuration.class); doReturn(config).when(context).config(); - assertThat(CacheContextImpl.of(context).isCacheEnabled()).isFalse(); + var sonarComponents = mockSonarComponents(context, null); + + assertThat(CacheContextImpl.of(sonarComponents).isCacheEnabled()).isFalse(); doReturn(Optional.of(true)).when(config).getBoolean(CacheContextImpl.SONAR_CACHING_ENABLED_KEY); - assertThat(CacheContextImpl.of(context).isCacheEnabled()).isTrue(); + assertThat(CacheContextImpl.of(sonarComponents).isCacheEnabled()).isTrue(); } @Test void override_flag_caching_enabled_false() { - SensorContext context = mock(SensorContext.class); + var context = mockSensorContext(); doReturn(true).when(context).isCacheEnabled(); - doReturn(mock(ReadCache.class)).when(context).previousCache(); - doReturn(mock(WriteCache.class)).when(context).nextCache(); - Configuration config = mock(Configuration.class); doReturn(config).when(context).config(); - assertThat(CacheContextImpl.of(context).isCacheEnabled()).isTrue(); + var sonarComponents = mockSonarComponents(context, null); + + assertThat(CacheContextImpl.of(sonarComponents).isCacheEnabled()).isTrue(); doReturn(Optional.of(false)).when(config).getBoolean(CacheContextImpl.SONAR_CACHING_ENABLED_KEY); - assertThat(CacheContextImpl.of(context).isCacheEnabled()).isFalse(); + assertThat(CacheContextImpl.of(sonarComponents).isCacheEnabled()).isFalse(); + } + + @Test + void should_use_sonarlint_cache_when_available() { + var sonarLintCache = mock(SonarLintCache.class); + var sonarComponents = mockSonarComponents(null, sonarLintCache); + + CacheContextImpl cci = CacheContextImpl.of(sonarComponents); + verifyCacheContextUsesSonarLintCache(cci, sonarLintCache); + assertThat(cci.isCacheEnabled()).isFalse(); + } + + @Test + void should_prefer_sonarlint_cache_when_sensor_context_also_offers_caching() { + var sonarLintCache = mock(SonarLintCache.class); + + var sensorContext = mockSensorContext(); + doReturn(true).when(sensorContext).isCacheEnabled(); + + var sonarComponents = mockSonarComponents(sensorContext, sonarLintCache); + + CacheContextImpl cci = CacheContextImpl.of(sonarComponents); + verifyCacheContextUsesSonarLintCache(cci, sonarLintCache); + } + + @Test + void should_use_sonarlint_cache_even_when_caching_is_disabled_in_the_sensor_context() { + var sonarLintCache = mock(SonarLintCache.class); + + var sensorContext = mockSensorContext(); + doReturn(false).when(sensorContext).isCacheEnabled(); + + var sonarComponents = mockSonarComponents(sensorContext, sonarLintCache); + + CacheContextImpl cci = CacheContextImpl.of(sonarComponents); + verifyCacheContextUsesSonarLintCache(cci, sonarLintCache); + } + + @Test + void should_use_sonarlint_cache_even_when_caching_is_disabled_by_the_configuration() { + var sonarLintCache = mock(SonarLintCache.class); + + var config = mock(Configuration.class); + doReturn(Optional.of(false)).when(config).getBoolean(CacheContextImpl.SONAR_CACHING_ENABLED_KEY); + + var sensorContext = mockSensorContext(); + doReturn(false).when(sensorContext).isCacheEnabled(); + doReturn(config).when(sensorContext).config(); + + var sonarComponents = mockSonarComponents(sensorContext, sonarLintCache); + + CacheContextImpl cci = CacheContextImpl.of(sonarComponents); + verifyCacheContextUsesSonarLintCache(cci, sonarLintCache); + } + + private SensorContext mockSensorContext() { + SensorContext sensorContext = mock(SensorContext.class); + var readCache = mock(ReadCache.class); + doReturn(readCache).when(sensorContext).previousCache(); + var writeCache = mock(WriteCache.class); + doReturn(writeCache).when(sensorContext).nextCache(); + + return sensorContext; + } + + private SonarComponents mockSonarComponents(@Nullable SensorContext sensorContext, @Nullable SonarLintCache sonarLintCache) { + var sonarComponents = mock(SonarComponents.class); + doReturn(sensorContext).when(sonarComponents).context(); + doReturn(sonarLintCache).when(sonarComponents).sonarLintCache(); + + return sonarComponents; + } + + private static void verifyCacheContextUsesDummyCache(CacheContext cacheContext) { + assertThat(cacheContext.isCacheEnabled()).isFalse(); + + assertThat(cacheContext.getReadCache()) + .isInstanceOf(DummyCache.class) + .isSameAs(cacheContext.getWriteCache()); + } + + private static void verifyCacheContextUsesSonarLintCache(CacheContext cacheContext, SonarLintCache sonarLintCache) { + // CacheContext does not expose the underlying cache. + // Hence, we have to test whether the read/write behaviour that it is exposing will affect the sonarLintCache. + + cacheContext.getReadCache().read("key"); + verify(sonarLintCache, times(1)).read("key"); + + var bytes = new byte[0]; + cacheContext.getWriteCache().write("key", bytes); + verify(sonarLintCache, times(1)).write("key", bytes); } } diff --git a/java-frontend/src/test/java/org/sonar/java/caching/ContentHashCacheTest.java b/java-frontend/src/test/java/org/sonar/java/caching/ContentHashCacheTest.java index fac6ba039bb..9bfa03f5f7b 100644 --- a/java-frontend/src/test/java/org/sonar/java/caching/ContentHashCacheTest.java +++ b/java-frontend/src/test/java/org/sonar/java/caching/ContentHashCacheTest.java @@ -29,14 +29,18 @@ import org.junit.jupiter.api.extension.RegisterExtension; import org.slf4j.event.Level; import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.sensor.SensorContext; import org.sonar.api.batch.sensor.cache.ReadCache; import org.sonar.api.batch.sensor.cache.WriteCache; import org.sonar.api.batch.sensor.internal.SensorContextTester; import org.sonar.api.testfixtures.log.LogAndArguments; import org.sonar.api.testfixtures.log.LogTesterJUnit5; +import org.sonar.java.SonarComponents; import org.sonar.java.TestUtils; +import org.sonar.plugins.java.api.caching.SonarLintCache; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -52,7 +56,7 @@ class ContentHashCacheTest { @Test void hasSameHashCached_returns_true_when_content_hash_file_is_in_read_cache() throws IOException, NoSuchAlgorithmException { logTester.setLevel(Level.TRACE); - ContentHashCache contentHashCache = new ContentHashCache(getSensorContextTester()); + ContentHashCache contentHashCache = new ContentHashCache(mockSonarComponents(getSonarComponentsTester())); Assertions.assertTrue(contentHashCache.hasSameHashCached(inputFile)); List logs = logTester.getLogs(Level.TRACE).stream().map(LogAndArguments::getFormattedMsg).toList(); @@ -74,7 +78,7 @@ void hasSameHashCached_returns_false_when_content_hash_file_is_not_in_read_cache private List hasSameHashCached_returns_false_when_content_hash_file_is_not_in_read_cache(Level level) { logTester.setLevel(level); - ContentHashCache contentHashCache = new ContentHashCache(getSensorContextTesterWithEmptyCache(true)); + ContentHashCache contentHashCache = new ContentHashCache(mockSonarComponents(getSensorContextTesterWithEmptyCache(true))); Assertions.assertFalse(contentHashCache.hasSameHashCached(inputFile)); return logTester.getLogs(level).stream().map(LogAndArguments::getFormattedMsg).toList(); } @@ -85,7 +89,7 @@ void hasSameHashCached_returns_false_when_cache_is_disabled_and_input_file_statu InputFile inputFile1 = mock(InputFile.class); when(inputFile1.status()).thenReturn(InputFile.Status.SAME); when(inputFile1.key()).thenReturn("key"); - ContentHashCache contentHashCache = new ContentHashCache(getSensorContextTesterWithEmptyCache(false)); + ContentHashCache contentHashCache = new ContentHashCache(mockSonarComponents(getSensorContextTesterWithEmptyCache(false))); Assertions.assertTrue(contentHashCache.hasSameHashCached(inputFile1)); List logs = logTester.getLogs(Level.TRACE).stream().map(LogAndArguments::getFormattedMsg).toList(); @@ -98,7 +102,7 @@ void hasSameHashCached_returns_false_cache_is_disabled_and_input_file_status_is_ logTester.setLevel(Level.TRACE); InputFile inputFile1 = mock(InputFile.class); when(inputFile1.status()).thenReturn(InputFile.Status.CHANGED); - ContentHashCache contentHashCache = new ContentHashCache(getSensorContextTesterWithEmptyCache(false)); + ContentHashCache contentHashCache = new ContentHashCache(mockSonarComponents(getSensorContextTesterWithEmptyCache(false))); Assertions.assertFalse(contentHashCache.hasSameHashCached(inputFile1)); List logs = logTester.getLogs(Level.TRACE).stream().map(LogAndArguments::getFormattedMsg).toList(); @@ -109,7 +113,7 @@ void hasSameHashCached_returns_false_cache_is_disabled_and_input_file_status_is_ @Test void hasSameHashCached_writesToCache_when_key_is_not_present() { logTester.setLevel(Level.TRACE); - ContentHashCache contentHashCache = new ContentHashCache(getSensorContextTesterWithEmptyCache(true)); + ContentHashCache contentHashCache = new ContentHashCache(mockSonarComponents(getSensorContextTesterWithEmptyCache(true))); contentHashCache.hasSameHashCached(inputFile); Assertions.assertTrue(contentHashCache.writeToCache(inputFile)); @@ -129,7 +133,7 @@ void hasSameHashCached_returns_false_when_content_hash_file_is_not_same_as_one_i WriteCache writeCache = mock(WriteCache.class); sensorContext.setPreviousCache(readCache); sensorContext.setNextCache(writeCache); - ContentHashCache contentHashCache = new ContentHashCache(sensorContext); + ContentHashCache contentHashCache = new ContentHashCache(mockSonarComponents(sensorContext)); Assertions.assertFalse(contentHashCache.hasSameHashCached(inputFile)); List logs = logTester.getLogs(Level.TRACE).stream().map(LogAndArguments::getFormattedMsg).toList(); @@ -154,7 +158,7 @@ void hasSameHashCached_returns_false_when_FileHashingUtils_throws_exception() th sensorContext.setPreviousCache(readCache); sensorContext.setNextCache(writeCache); when(inputFile1.contents()).thenThrow(new IOException()); - ContentHashCache contentHashCache = new ContentHashCache(sensorContext); + ContentHashCache contentHashCache = new ContentHashCache(mockSonarComponents(sensorContext)); Assertions.assertFalse(contentHashCache.hasSameHashCached(inputFile1)); List logs = logTester.getLogs(Level.WARN).stream().map(LogAndArguments::getFormattedMsg).toList(); @@ -164,19 +168,19 @@ void hasSameHashCached_returns_false_when_FileHashingUtils_throws_exception() th @Test void contains_returns_true_when_file_is_in_cache() throws IOException, NoSuchAlgorithmException { - ContentHashCache contentHashCache = new ContentHashCache(getSensorContextTester()); + ContentHashCache contentHashCache = new ContentHashCache(mockSonarComponents(getSonarComponentsTester())); Assertions.assertTrue(contentHashCache.contains(inputFile)); } @Test void contains_returns_false_when_file_is_not_in_cache() { - ContentHashCache contentHashCache = new ContentHashCache(getSensorContextTesterWithEmptyCache(true)); + ContentHashCache contentHashCache = new ContentHashCache(mockSonarComponents(getSensorContextTesterWithEmptyCache(true))); Assertions.assertFalse(contentHashCache.contains(inputFile)); } @Test void contains_returns_false_when_cache_is_disabled() { - ContentHashCache contentHashCache = new ContentHashCache(getSensorContextTesterWithEmptyCache(false)); + ContentHashCache contentHashCache = new ContentHashCache(mockSonarComponents(getSensorContextTesterWithEmptyCache(false))); Assertions.assertFalse(contentHashCache.contains(inputFile)); } @@ -198,7 +202,7 @@ private List writeToCache_returns_false_when_writing_to_cache_throws_exc sensorContext.setNextCache(writeCache); doThrow(new IllegalArgumentException()).when(writeCache).write("java:contentHash:MD5:" + inputFile.key(), FileHashingUtils.inputFileContentHash(file.getPath())); - ContentHashCache contentHashCache = new ContentHashCache(sensorContext); + ContentHashCache contentHashCache = new ContentHashCache(mockSonarComponents(sensorContext)); Assertions.assertFalse(contentHashCache.writeToCache(inputFile)); return logTester.getLogs(level).stream().map(LogAndArguments::getFormattedMsg).toList(); } @@ -215,7 +219,7 @@ void writeToCache_returns_false_when_FileHashingUtils_throws_exception() throws InputFile inputFile1 = mock(InputFile.class); when(inputFile1.key()).thenReturn("key"); when(inputFile1.contents()).thenThrow(new IOException()); - ContentHashCache contentHashCache = new ContentHashCache(sensorContext); + ContentHashCache contentHashCache = new ContentHashCache(mockSonarComponents(sensorContext)); Assertions.assertFalse(contentHashCache.writeToCache(inputFile1)); List logs = logTester.getLogs(Level.WARN).stream().map(LogAndArguments::getFormattedMsg).toList(); @@ -223,6 +227,26 @@ void writeToCache_returns_false_when_FileHashingUtils_throws_exception() throws contains("Failed to compute content hash for file " + inputFile1.key()); } + @Test + void should_not_enable_content_hash_cache_when_using_sonarlint_cache() { + logTester.setLevel(Level.TRACE); + + var sensorContext = getSensorContextTesterWithEmptyCache(true); + var sonarLintCache = mock(SonarLintCache.class); + + var sonarComponents = mockSonarComponents(sensorContext); + doReturn(sonarLintCache).when(sonarComponents).sonarLintCache(); + + var contentHashCache = new ContentHashCache(sonarComponents); + + InputFile inputFile1 = mock(InputFile.class); + assertThat(contentHashCache.contains(inputFile1)).isFalse(); + + List logs = logTester.getLogs(Level.TRACE).stream().map(LogAndArguments::getFormattedMsg).toList(); + assertThat(logs). + contains("Cannot lookup cached hashes when the cache is disabled (null)."); + } + private SensorContextTester getSensorContextTesterWithEmptyCache(boolean isCacheEnabled) { SensorContextTester sensorContext = SensorContextTester.create(file.getAbsoluteFile()); sensorContext.setCacheEnabled(isCacheEnabled); @@ -235,7 +259,7 @@ private SensorContextTester getSensorContextTesterWithEmptyCache(boolean isCache return sensorContext; } - private SensorContextTester getSensorContextTester() throws IOException, NoSuchAlgorithmException { + private SensorContextTester getSonarComponentsTester() throws IOException, NoSuchAlgorithmException { SensorContextTester sensorContext = SensorContextTester.create(file.getAbsoluteFile()); sensorContext.setCacheEnabled(true); ReadCache readCache = mock(ReadCache.class); @@ -248,4 +272,10 @@ private SensorContextTester getSensorContextTester() throws IOException, NoSuchA return sensorContext; } + private static SonarComponents mockSonarComponents(SensorContext sensorContext) { + var sonarComponents = mock(SonarComponents.class); + doReturn(sensorContext).when(sonarComponents).context(); + + return sonarComponents; + } } diff --git a/java-frontend/src/test/java/org/sonar/java/model/DefaultModuleScannerContextTest.java b/java-frontend/src/test/java/org/sonar/java/model/DefaultModuleScannerContextTest.java index 91b4abe5308..1b58b7f3436 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/DefaultModuleScannerContextTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/DefaultModuleScannerContextTest.java @@ -21,6 +21,10 @@ import java.io.File; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.sonar.api.SonarProduct; +import org.sonar.api.SonarRuntime; import org.sonar.api.batch.fs.InputComponent; import org.sonar.api.batch.sensor.SensorContext; import org.sonar.java.SonarComponents; @@ -196,4 +200,56 @@ void getRootProjectWorkingDirectory_returns_the_working_dir_from_sonarComponents ); assertThat(context.getRootProjectWorkingDirectory()).isSameAs(projectLevelWorkDirFile); } + + @ParameterizedTest + @EnumSource(SonarProduct.class) + void should_properly_report_sonar_product(SonarProduct product) { + var runtime = mock(SonarRuntime.class); + doReturn(product).when(runtime).getProduct(); + + var sensorContext = mock(SensorContext.class); + doReturn(runtime).when(sensorContext).runtime(); + + var sonarComponents = mock(SonarComponents.class); + doReturn(sensorContext).when(sonarComponents).context(); + + var context = new DefaultModuleScannerContext( + sonarComponents, + JParserConfig.MAXIMUM_SUPPORTED_JAVA_VERSION, + false, + null + ); + + assertThat(context.sonarProduct()) + .isEqualTo(product); + } + + @Test + void should_not_report_product_if_sonarcomponents_is_not_available() { + var context = new DefaultModuleScannerContext( + null, + JParserConfig.MAXIMUM_SUPPORTED_JAVA_VERSION, + false, + null + ); + + assertThat(context.sonarProduct()) + .isNull(); + } + + @Test + void should_not_report_product_if_no_sensor_context_is_available() { + var sonarComponents = mock(SonarComponents.class); + doReturn(null).when(sonarComponents).context(); + + var context = new DefaultModuleScannerContext( + sonarComponents, + JParserConfig.MAXIMUM_SUPPORTED_JAVA_VERSION, + false, + null + ); + + assertThat(context.sonarProduct()) + .isNull(); + } } diff --git a/java-frontend/src/test/java/org/sonar/java/testing/VisitorsBridgeForTestsTest.java b/java-frontend/src/test/java/org/sonar/java/testing/VisitorsBridgeForTestsTest.java index dca2ded0e3a..2ecce90b9da 100644 --- a/java-frontend/src/test/java/org/sonar/java/testing/VisitorsBridgeForTestsTest.java +++ b/java-frontend/src/test/java/org/sonar/java/testing/VisitorsBridgeForTestsTest.java @@ -95,7 +95,7 @@ void create_InputFileScannerContext_also_sets_testContext_field() { var inputFile = mock(InputFile.class); var expectedTestContext = - visitorsBridgeForTests.createScannerContext(sonarComponents, inputFile, new JavaVersionImpl(), false, CacheContextImpl.of(context)); + visitorsBridgeForTests.createScannerContext(sonarComponents, inputFile, new JavaVersionImpl(), false, CacheContextImpl.of(sonarComponents)); assertThat(visitorsBridgeForTests.lastCreatedTestContext()).isSameAs(expectedTestContext); } diff --git a/java-frontend/src/test/java/org/sonar/plugins/java/api/caching/SonarLintCacheTest.java b/java-frontend/src/test/java/org/sonar/plugins/java/api/caching/SonarLintCacheTest.java new file mode 100644 index 00000000000..dbbc4343538 --- /dev/null +++ b/java-frontend/src/test/java/org/sonar/plugins/java/api/caching/SonarLintCacheTest.java @@ -0,0 +1,89 @@ +/* + * SonarQube Java + * Copyright (C) 2012-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.plugins.java.api.caching; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +class SonarLintCacheTest { + @Test + void read_non_existing_key() { + SonarLintCache sonarLintCache = new SonarLintCache(); + assertThatThrownBy(() -> sonarLintCache.read("foo")).hasMessage("SonarLintCache does not contain key \"foo\""); + } + + @Test + void write_and_read_existing_key() throws IOException { + SonarLintCache sonarLintCache = new SonarLintCache(); + byte[] bytes = {42}; + sonarLintCache.write("foo", bytes); + try (var value = sonarLintCache.read("foo")) { + assertThat(value.readAllBytes()).isEqualTo(bytes); + } + + sonarLintCache.write("bar", new ByteArrayInputStream(bytes)); + try (var value = sonarLintCache.read("bar")) { + assertThat(value.readAllBytes()).isEqualTo(bytes); + } + } + + @Test + void contains() { + SonarLintCache sonarLintCache = new SonarLintCache(); + assertThat(sonarLintCache.contains("foo")).isFalse(); + byte[] bytes = {42}; + sonarLintCache.write("foo", bytes); + assertThat(sonarLintCache.contains("foo")).isTrue(); + assertThat(sonarLintCache.contains("bar")).isFalse(); + } + + @Test + void write_non_valid_input_stream() throws IOException { + InputStream inputStream = Mockito.mock(InputStream.class); + Mockito.when(inputStream.readAllBytes()).thenThrow(IOException.class); + + SonarLintCache sonarLintCache = new SonarLintCache(); + assertThatThrownBy(() -> sonarLintCache.write("foo", inputStream)).isInstanceOf(IllegalStateException.class).hasCauseInstanceOf(IOException.class); + } + + @Test + void write_same_key() { + SonarLintCache sonarLintCache = new SonarLintCache(); + byte[] bytes1 = {42}; + byte[] bytes2 = {0, 1, 2}; + sonarLintCache.write("foo", bytes1); + assertThatThrownBy(() -> sonarLintCache.write("foo", bytes2)).hasMessage("Same key cannot be written to multiple times (foo)"); + assertThatThrownBy(() -> sonarLintCache.write("foo", new ByteArrayInputStream(bytes2))).hasMessage( + "Same key cannot be written to multiple times (foo)" + ); + } + + @Test + void copy_from_previous() { + SonarLintCache sonarLintCache = new SonarLintCache(); + assertThatThrownBy(() -> sonarLintCache.copyFromPrevious("foo")).hasMessage("SonarLintCache does not allow to copy from previous."); + } +} diff --git a/sonar-java-plugin/src/main/java/org/sonar/plugins/java/JavaPlugin.java b/sonar-java-plugin/src/main/java/org/sonar/plugins/java/JavaPlugin.java index 806907f36e5..c34db8ea311 100644 --- a/sonar-java-plugin/src/main/java/org/sonar/plugins/java/JavaPlugin.java +++ b/sonar-java-plugin/src/main/java/org/sonar/plugins/java/JavaPlugin.java @@ -33,6 +33,7 @@ import org.sonar.java.DefaultJavaResourceLocator; import org.sonar.java.JavaConstants; import org.sonar.java.SonarComponents; +import org.sonar.plugins.java.api.caching.SonarLintCache; import org.sonar.java.classpath.ClasspathForMain; import org.sonar.java.classpath.ClasspathForMainForSonarLint; import org.sonar.java.classpath.ClasspathForTest; @@ -51,6 +52,9 @@ public void define(Context context) { if (context.getRuntime().getProduct() == SonarProduct.SONARLINT) { list.add(ClasspathForMainForSonarLint.class); + // Some custom rules (i.e. DBD) depend on the presence of SonarLintCache when executing in a SonarLint context. + // Hence, we must provide it here. + list.add(SonarLintCache.class); } else { list.addAll(SurefireExtensions.getExtensions()); list.add(DroppedPropertiesSensor.class); diff --git a/sonar-java-plugin/src/main/java/org/sonar/plugins/java/JavaSensor.java b/sonar-java-plugin/src/main/java/org/sonar/plugins/java/JavaSensor.java index aae5aaafedc..e9161694ff0 100644 --- a/sonar-java-plugin/src/main/java/org/sonar/plugins/java/JavaSensor.java +++ b/sonar-java-plugin/src/main/java/org/sonar/plugins/java/JavaSensor.java @@ -31,6 +31,7 @@ import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.sonar.api.batch.DependedUpon; import org.sonar.api.batch.Phase; import org.sonar.api.batch.fs.FileSystem; import org.sonar.api.batch.fs.InputFile; @@ -58,6 +59,7 @@ import static org.sonar.api.rules.RuleAnnotationUtils.getRuleKey; @Phase(name = Phase.Name.PRE) +@DependedUpon("org.sonar.plugins.java.JavaSensor") public class JavaSensor implements Sensor { private static final Logger LOG = LoggerFactory.getLogger(JavaSensor.class); diff --git a/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaPluginTest.java b/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaPluginTest.java index 143d5d526e6..eedda4696ed 100644 --- a/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaPluginTest.java +++ b/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaPluginTest.java @@ -27,6 +27,7 @@ import org.sonar.api.internal.SonarRuntimeImpl; import org.sonar.api.utils.Version; import org.sonar.java.jsp.Jasper; +import org.sonar.plugins.java.api.caching.SonarLintCache; import static org.assertj.core.api.Assertions.assertThat; @@ -41,7 +42,9 @@ void sonarLint_9_9_extensions() { SonarRuntime runtime = SonarRuntimeImpl.forSonarLint(VERSION_9_9); Plugin.Context context = new Plugin.Context(runtime); javaPlugin.define(context); - assertThat(context.getExtensions()).hasSize(17); + assertThat(context.getExtensions()) + .hasSize(18) + .contains(SonarLintCache.class); } diff --git a/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaSensorTest.java b/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaSensorTest.java index e9a4b7bb149..aa3dfa4bc4f 100644 --- a/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaSensorTest.java +++ b/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaSensorTest.java @@ -483,7 +483,7 @@ private SensorContextTester analyzeTwoFilesWithIssues(MapSettings settings) thro CheckFactory checkFactory = new CheckFactory(activeRulesBuilder.build()); SonarComponents components = new SonarComponents(fileLinesContextFactory, fs, - javaClasspath, javaTestClasspath, checkFactory, context.activeRules(), checkRegistrars, null); + javaClasspath, javaTestClasspath, checkFactory, context.activeRules(), checkRegistrars, null, null); JavaSensor jss = new JavaSensor(components, fs, resourceLocator, context.config(), mock(NoSonarFilter.class), null); jss.execute(context);