From 009e9072fb5109008569efa4da489697bb6a4303 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sun, 24 May 2026 12:49:51 +0000 Subject: [PATCH 01/16] build: bump sonarlint-analysis-engine 10.24 -> 11.3 + LTA 2026.1 analyzers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build does NOT compile after this commit — engine 11.x removed org.sonarsource.sonarlint.core.analysis.api.ActiveRule and changed two other signatures. Phase 3 of the LTA migration plan adds the adapter classes; Phase 4 wires them in. This commit is the version checkpoint, intentionally pre-adapter. --- pom.xml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pom.xml b/pom.xml index 02d7bcc..1d49e9c 100644 --- a/pom.xml +++ b/pom.xml @@ -61,25 +61,25 @@ 2.17.2 5.10.2 - 10.24.0.81415 + 11.3.0.85510 4.7.7 - 8.15.0.39343 - 5.5.0.23291 - 10.24.0.33043 - 3.46.0.13151 - 3.2.0.7239 + 8.29.0.43460 + 5.22.0.33216 + 12.5.0.41048 + 3.57.0.15976 + 3.6.0.9326 1.18.1.827 - 1.19.0.471 - 1.19.0.484 - 3.19.0.5695 - 2.13.0.5938 + 1.22.0.1992 + 1.23.0.2394 + 3.27.0.7699 + 2.17.0.7895 From a3a42cd992c5e9403e854c485827a3d2a64bd113 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sun, 24 May 2026 12:54:44 +0000 Subject: [PATCH 02/16] feat(daemon): add SimpleActiveRule adapter for engine 11.x ActiveRule SPI --- .../sonarpredict/daemon/SimpleActiveRule.java | 37 +++++++++++++++++++ .../daemon/SimpleActiveRuleTest.java | 35 ++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 src/main/java/io/github/randomcodespace/sonarpredict/daemon/SimpleActiveRule.java create mode 100644 src/test/java/io/github/randomcodespace/sonarpredict/daemon/SimpleActiveRuleTest.java diff --git a/src/main/java/io/github/randomcodespace/sonarpredict/daemon/SimpleActiveRule.java b/src/main/java/io/github/randomcodespace/sonarpredict/daemon/SimpleActiveRule.java new file mode 100644 index 0000000..792aca0 --- /dev/null +++ b/src/main/java/io/github/randomcodespace/sonarpredict/daemon/SimpleActiveRule.java @@ -0,0 +1,37 @@ +package io.github.randomcodespace.sonarpredict.daemon; + +import java.util.Map; +import org.sonar.api.batch.rule.ActiveRule; +import org.sonar.api.rule.RuleKey; + +/** + * Adapter for engine 11.x: {@code AnalysisConfiguration.Builder.addActiveRules} now takes + * the public {@link ActiveRule} interface; 10.x's internal {@code ActiveRule} class is gone + * and no public concrete ships. This record is the minimal implementation that satisfies + * the interface for our analyses (no severity / template / qProfile metadata needed offline). + */ +public record SimpleActiveRule( + RuleKey ruleKey, + String language, + String severity, + String internalKey, + String templateRuleKey, + Map params, + String qpKey) implements ActiveRule { + + @Override + public String param(String key) { + return params.get(key); + } + + public static SimpleActiveRule of(String ruleKey, String language, Map params) { + return new SimpleActiveRule( + RuleKey.parse(ruleKey), + language, + null, + null, + null, + params == null ? Map.of() : Map.copyOf(params), + null); + } +} diff --git a/src/test/java/io/github/randomcodespace/sonarpredict/daemon/SimpleActiveRuleTest.java b/src/test/java/io/github/randomcodespace/sonarpredict/daemon/SimpleActiveRuleTest.java new file mode 100644 index 0000000..a4714b6 --- /dev/null +++ b/src/test/java/io/github/randomcodespace/sonarpredict/daemon/SimpleActiveRuleTest.java @@ -0,0 +1,35 @@ +package io.github.randomcodespace.sonarpredict.daemon; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.sonar.api.batch.rule.ActiveRule; +import org.sonar.api.rule.RuleKey; + +class SimpleActiveRuleTest { + + @Test + void of_buildsActiveRuleWithRuleKeyAndLanguage() { + SimpleActiveRule rule = SimpleActiveRule.of("java:S1234", "java", Map.of("foo", "bar")); + + assertThat(rule).isInstanceOf(ActiveRule.class); + assertThat(rule.ruleKey()).isEqualTo(RuleKey.parse("java:S1234")); + assertThat(rule.language()).isEqualTo("java"); + assertThat(rule.params()).containsExactlyEntriesOf(Map.of("foo", "bar")); + } + + @Test + void of_nullParams_givesEmptyMap() { + SimpleActiveRule rule = SimpleActiveRule.of("java:S1234", "java", null); + assertThat(rule.params()).isEmpty(); + } + + @Test + void of_paramsAreDefensivelyCopied() { + java.util.HashMap mutable = new java.util.HashMap<>(Map.of("a", "1")); + SimpleActiveRule rule = SimpleActiveRule.of("java:S1234", "java", mutable); + mutable.put("b", "2"); + assertThat(rule.params()).containsOnlyKeys("a"); + } +} From 0761645ba2a3d4a1c801f4cb7dae54b90e7389dc Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sun, 24 May 2026 12:58:50 +0000 Subject: [PATCH 03/16] feat(hostplugin): NoOpAnalysisWarnings bean for engine 11.x sensors Engine 11.3 ships zero references to org.sonar.api.notifications.AnalysisWarnings but the bundled HTML/Java/Python/PHP sensors Spring-autowire it. This is the host-side implementation that lives in the sonar-predictor-host plugin JAR (added in Task 10). Also adds logback-classic 1.5.13 and assertj-core 3.27.7 as test-scope deps required by the ListAppender-based log-capture test. --- pom.xml | 12 ++++ .../hostplugin/NoOpAnalysisWarnings.java | 31 +++++++++ .../hostplugin/NoOpAnalysisWarningsTest.java | 67 +++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 src/main/java/io/github/randomcodespace/sonarpredict/hostplugin/NoOpAnalysisWarnings.java create mode 100644 src/test/java/io/github/randomcodespace/sonarpredict/hostplugin/NoOpAnalysisWarningsTest.java diff --git a/pom.xml b/pom.xml index 1d49e9c..c2da708 100644 --- a/pom.xml +++ b/pom.xml @@ -129,6 +129,18 @@ junit-jupiter test + + ch.qos.logback + logback-classic + 1.5.13 + test + + + org.assertj + assertj-core + 3.27.7 + test + diff --git a/src/main/java/io/github/randomcodespace/sonarpredict/hostplugin/NoOpAnalysisWarnings.java b/src/main/java/io/github/randomcodespace/sonarpredict/hostplugin/NoOpAnalysisWarnings.java new file mode 100644 index 0000000..b25e8b2 --- /dev/null +++ b/src/main/java/io/github/randomcodespace/sonarpredict/hostplugin/NoOpAnalysisWarnings.java @@ -0,0 +1,31 @@ +package io.github.randomcodespace.sonarpredict.hostplugin; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.notifications.AnalysisWarnings; +import org.sonarsource.api.sonarlint.SonarLintSide; + +/** + * Host-side implementation of {@link AnalysisWarnings} that the bundled analyzer sensors + * Spring-autowire. Engine 11.3 ships zero references to {@code AnalysisWarnings}, so the + * host plugin provides this bean. + * + *

Lifespan is {@code INSTANCE} (per-analysis) — the engine instantiates a fresh + * instance per analysis, which resets the dedupe set automatically. + */ +@SonarLintSide(lifespan = SonarLintSide.INSTANCE) +public final class NoOpAnalysisWarnings implements AnalysisWarnings { + + private static final Logger LOG = LoggerFactory.getLogger(NoOpAnalysisWarnings.class); + + private final Set seen = ConcurrentHashMap.newKeySet(); + + @Override + public void addUnique(String message) { + if (seen.add(message)) { + LOG.warn("[sonar] {}", message); + } + } +} diff --git a/src/test/java/io/github/randomcodespace/sonarpredict/hostplugin/NoOpAnalysisWarningsTest.java b/src/test/java/io/github/randomcodespace/sonarpredict/hostplugin/NoOpAnalysisWarningsTest.java new file mode 100644 index 0000000..88a2ec2 --- /dev/null +++ b/src/test/java/io/github/randomcodespace/sonarpredict/hostplugin/NoOpAnalysisWarningsTest.java @@ -0,0 +1,67 @@ +package io.github.randomcodespace.sonarpredict.hostplugin; + +import static org.assertj.core.api.Assertions.assertThat; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; + +class NoOpAnalysisWarningsTest { + + private Logger logger; + private ListAppender appender; + private NoOpAnalysisWarnings warnings; + + @BeforeEach + void setUp() { + logger = (Logger) LoggerFactory.getLogger(NoOpAnalysisWarnings.class); + appender = new ListAppender<>(); + appender.start(); + logger.addAppender(appender); + warnings = new NoOpAnalysisWarnings(); + } + + @AfterEach + void tearDown() { + logger.detachAppender(appender); + } + + @Test + void addUnique_logsAtWarn() { + warnings.addUnique("memory limit reached"); + + assertThat(appender.list).hasSize(1); + assertThat(appender.list.get(0).getLevel()).isEqualTo(Level.WARN); + assertThat(appender.list.get(0).getFormattedMessage()).contains("[sonar]", "memory limit reached"); + } + + @Test + void addUnique_dedupsIdenticalMessages() { + warnings.addUnique("same"); + warnings.addUnique("same"); + warnings.addUnique("same"); + + assertThat(appender.list).hasSize(1); + } + + @Test + void addUnique_distinctMessagesAllLog() { + warnings.addUnique("first"); + warnings.addUnique("second"); + + assertThat(appender.list).hasSize(2); + } + + @Test + void freshInstance_doesNotShareDedupeSet() { + warnings.addUnique("X"); + new NoOpAnalysisWarnings().addUnique("X"); + + assertThat(appender.list).hasSize(2); + } +} From 991bc3e0b1db06c2a6ce530ac76718a0fbce76cd Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sun, 24 May 2026 13:01:40 +0000 Subject: [PATCH 04/16] feat(hostplugin): SonarPredictHostPlugin registers NoOpAnalysisWarnings --- pom.xml | 6 +++++ .../hostplugin/SonarPredictHostPlugin.java | 20 ++++++++++++++ .../SonarPredictHostPluginTest.java | 26 +++++++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 src/main/java/io/github/randomcodespace/sonarpredict/hostplugin/SonarPredictHostPlugin.java create mode 100644 src/test/java/io/github/randomcodespace/sonarpredict/hostplugin/SonarPredictHostPluginTest.java diff --git a/pom.xml b/pom.xml index c2da708..57393b7 100644 --- a/pom.xml +++ b/pom.xml @@ -141,6 +141,12 @@ 3.27.7 test + + org.mockito + mockito-core + 5.15.2 + test + diff --git a/src/main/java/io/github/randomcodespace/sonarpredict/hostplugin/SonarPredictHostPlugin.java b/src/main/java/io/github/randomcodespace/sonarpredict/hostplugin/SonarPredictHostPlugin.java new file mode 100644 index 0000000..81072dd --- /dev/null +++ b/src/main/java/io/github/randomcodespace/sonarpredict/hostplugin/SonarPredictHostPlugin.java @@ -0,0 +1,20 @@ +package io.github.randomcodespace.sonarpredict.hostplugin; + +import org.sonar.api.Plugin; + +/** + * The host plugin shipped as {@code sonar-predictor-host-${version}.jar} alongside the + * 10 SonarSource analyzer plugins in the bundle. Registers host-side beans that + * {@code sonarlint-analysis-engine} 11.x and later expect to find in the analysis + * container but no longer provide. + * + *

This class uses only the public {@code org.sonar.api.Plugin} SPI and does not + * depend on any {@code org.sonarsource.sonarlint.core.*} internal class. + */ +public final class SonarPredictHostPlugin implements Plugin { + + @Override + public void define(Context context) { + context.addExtension(NoOpAnalysisWarnings.class); + } +} diff --git a/src/test/java/io/github/randomcodespace/sonarpredict/hostplugin/SonarPredictHostPluginTest.java b/src/test/java/io/github/randomcodespace/sonarpredict/hostplugin/SonarPredictHostPluginTest.java new file mode 100644 index 0000000..9ef7711 --- /dev/null +++ b/src/test/java/io/github/randomcodespace/sonarpredict/hostplugin/SonarPredictHostPluginTest.java @@ -0,0 +1,26 @@ +package io.github.randomcodespace.sonarpredict.hostplugin; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import org.junit.jupiter.api.Test; +import org.sonar.api.Plugin; + +class SonarPredictHostPluginTest { + + @Test + void define_addsNoOpAnalysisWarningsExtension() { + SonarPredictHostPlugin plugin = new SonarPredictHostPlugin(); + Plugin.Context context = mock(Plugin.Context.class); + + plugin.define(context); + + verify(context).addExtension(NoOpAnalysisWarnings.class); + } + + @Test + void implementsPluginSpi() { + assertThat(new SonarPredictHostPlugin()).isInstanceOf(Plugin.class); + } +} From bb74c0cadb3c652faadccd9ce77000983ffdb055 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sun, 24 May 2026 13:04:08 +0000 Subject: [PATCH 05/16] fix(daemon): adapt PluginRuntime to engine 11.x SonarLanguage API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - language.getPluginKey() -> language.getPlugin().getKey() (11.x rename) - pass Set.of("sonarpredict-host") to PluginsLoader.load() as the additionalAllowedPlugins arg, so our host plugin is admitted into the analysis container's extension scan (belt-and-suspenders; the isNotSensor disjunct alone is also sufficient — see spec §4.1) --- .../randomcodespace/sonarpredict/daemon/PluginRuntime.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/github/randomcodespace/sonarpredict/daemon/PluginRuntime.java b/src/main/java/io/github/randomcodespace/sonarpredict/daemon/PluginRuntime.java index cc0885f..321d36d 100644 --- a/src/main/java/io/github/randomcodespace/sonarpredict/daemon/PluginRuntime.java +++ b/src/main/java/io/github/randomcodespace/sonarpredict/daemon/PluginRuntime.java @@ -105,7 +105,7 @@ public static LoadedPlugins loadFrom(Set jars) { V1_LANGUAGES, false, detectNodeVersion()); - PluginsLoadResult result = new PluginsLoader().load(config, Set.of()); + PluginsLoadResult result = new PluginsLoader().load(config, Set.of("sonarpredict-host")); return result.getLoadedPlugins(); } @@ -126,7 +126,7 @@ public static LoadedPlugins loadFrom(Set jars) { public static Set loadedLanguagesFor(Set loadedPluginKeys) { Set loaded = EnumSet.noneOf(SonarLanguage.class); for (SonarLanguage language : V1_LANGUAGES) { - if (loadedPluginKeys.contains(language.getPluginKey())) { + if (loadedPluginKeys.contains(language.getPlugin().getKey())) { loaded.add(language); } } From 3fc9b779acf6727a110f2b9ee02ce69971607bcd Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sun, 24 May 2026 13:06:37 +0000 Subject: [PATCH 06/16] fix(daemon): switch AnalysisService to public ActiveRule SPI Engine 11.x removed org.sonarsource.sonarlint.core.analysis.api.ActiveRule (the 10.x internal class). The public org.sonar.api.batch.rule.ActiveRule is the replacement; SimpleActiveRule.of(...) builds instances of the public interface in one call. --- .../sonarpredict/daemon/AnalysisService.java | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/main/java/io/github/randomcodespace/sonarpredict/daemon/AnalysisService.java b/src/main/java/io/github/randomcodespace/sonarpredict/daemon/AnalysisService.java index b196227..9717370 100644 --- a/src/main/java/io/github/randomcodespace/sonarpredict/daemon/AnalysisService.java +++ b/src/main/java/io/github/randomcodespace/sonarpredict/daemon/AnalysisService.java @@ -20,7 +20,7 @@ import java.util.stream.Stream; import org.sonarsource.sonarlint.core.analysis.AnalysisScheduler; -import org.sonarsource.sonarlint.core.analysis.api.ActiveRule; +import org.sonar.api.batch.rule.ActiveRule; import org.sonarsource.sonarlint.core.analysis.api.AnalysisConfiguration; import org.sonarsource.sonarlint.core.analysis.api.AnalysisResults; import org.sonarsource.sonarlint.core.analysis.api.AnalysisSchedulerConfiguration; @@ -510,12 +510,8 @@ static List resolveActiveRules( } String languageKey = language.getSonarLanguageKey(); for (String ruleKey : ruleKeys) { - ActiveRule activeRule = new ActiveRule(ruleKey, languageKey); Map params = paramDefaults.paramsFor(ruleKey); - if (!params.isEmpty()) { - activeRule.setParams(params); - } - rules.add(activeRule); + rules.add(SimpleActiveRule.of(ruleKey, languageKey, params)); } } return rules; @@ -622,14 +618,10 @@ private List profileRulesFrom(String profileRef) { } // The analyzer-registered parameter defaults are the baseline; the // profile's own override them where it sets a value. - ActiveRule active = new ActiveRule(rule.ruleKey(), languageKey); Map params = new java.util.HashMap<>(ruleParameterDefaults.paramsFor(rule.ruleKey())); params.putAll(rule.parameters()); - if (!params.isEmpty()) { - active.setParams(Map.copyOf(params)); - } - rules.add(active); + rules.add(SimpleActiveRule.of(rule.ruleKey(), languageKey, params)); } return rules; } From b9e0d4ec8dbea225fb225ddf738fa0c1023ac5ac Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sun, 24 May 2026 13:09:24 +0000 Subject: [PATCH 07/16] fix(daemon): IssueMapper handles RuleKey return type from engine 11.x engine.Issue.getRuleKey() returns org.sonar.api.rule.RuleKey in 11.x (was String in 10.x). Add .toString() at the IssueMapper call site that feeds the protocol's String ruleKey field. This is the third and final adapter required to make the project compile on the LTA 2026.1 toolchain. Also fix AnalysisServiceTest: SonarLanguage.getPluginKey() removed in 11.x; replaced with getPlugin().getKey() via the new SonarPlugin type. --- .../github/randomcodespace/sonarpredict/daemon/IssueMapper.java | 2 +- .../sonarpredict/daemon/AnalysisServiceTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/github/randomcodespace/sonarpredict/daemon/IssueMapper.java b/src/main/java/io/github/randomcodespace/sonarpredict/daemon/IssueMapper.java index 53bd4c5..5ca3042 100644 --- a/src/main/java/io/github/randomcodespace/sonarpredict/daemon/IssueMapper.java +++ b/src/main/java/io/github/randomcodespace/sonarpredict/daemon/IssueMapper.java @@ -41,7 +41,7 @@ private IssueMapper() { public static io.github.randomcodespace.sonarpredict.protocol.dto.Issue toDto( Issue engineIssue, Path baseDir, RuleCatalog catalog) { return map( - engineIssue.getRuleKey(), + engineIssue.getRuleKey().toString(), resolveFilePath(engineIssue.getInputFile(), baseDir), engineIssue.getTextRange(), engineIssue.getMessage(), diff --git a/src/test/java/io/github/randomcodespace/sonarpredict/daemon/AnalysisServiceTest.java b/src/test/java/io/github/randomcodespace/sonarpredict/daemon/AnalysisServiceTest.java index e2eb7ef..bc94f55 100644 --- a/src/test/java/io/github/randomcodespace/sonarpredict/daemon/AnalysisServiceTest.java +++ b/src/test/java/io/github/randomcodespace/sonarpredict/daemon/AnalysisServiceTest.java @@ -562,7 +562,7 @@ void loadedLanguages_reflectsActuallyLoadedPlugins() { for (String key : languages) { var lang = org.sonarsource.sonarlint.core.commons.api.SonarLanguage .forKey(key).orElseThrow(); - assertTrue(loadedKeys.contains(lang.getPluginKey()), + assertTrue(loadedKeys.contains(lang.getPlugin().getKey()), "reported language '" + key + "' must have a loaded plugin"); } } From 8a779678a4512462cf9eec99cc850a896153a9c2 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sun, 24 May 2026 13:11:11 +0000 Subject: [PATCH 08/16] build: emit sonar-predictor-host plugin JAR as classifier=host Second maven-jar-plugin execution scoped to the hostplugin package produces target/sonar-predictor--host.jar in prepare-package. Manifest declares Plugin-Class/Plugin-Key/Sonar-Version so the sonarlint plugin loader treats it identically to a SonarSource analyzer plugin. --- pom.xml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pom.xml b/pom.xml index 57393b7..d165ce6 100644 --- a/pom.xml +++ b/pom.xml @@ -196,6 +196,27 @@ process-classes jar + + host-plugin-jar + prepare-package + jar + + host + + io/github/randomcodespace/sonarpredict/hostplugin/** + + + + io.github.randomcodespace.sonarpredict.hostplugin.SonarPredictHostPlugin + sonarpredict-host + Sonar Predictor Host + ${project.version} + 9.9 + true + + + + From e87e8ed9ee52347bf834f10dcfdf148c77523e5a Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sun, 24 May 2026 13:14:49 +0000 Subject: [PATCH 09/16] test(hostplugin): pin host JAR manifest contract --- .../hostplugin/HostJarManifestTest.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/test/java/io/github/randomcodespace/sonarpredict/hostplugin/HostJarManifestTest.java diff --git a/src/test/java/io/github/randomcodespace/sonarpredict/hostplugin/HostJarManifestTest.java b/src/test/java/io/github/randomcodespace/sonarpredict/hostplugin/HostJarManifestTest.java new file mode 100644 index 0000000..c9f34f2 --- /dev/null +++ b/src/test/java/io/github/randomcodespace/sonarpredict/hostplugin/HostJarManifestTest.java @@ -0,0 +1,65 @@ +package io.github.randomcodespace.sonarpredict.hostplugin; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; + +class HostJarManifestTest { + + @Test + void hostJarHasRequiredPluginManifestEntries() throws IOException { + Path hostJar = findHostJar(); + + try (JarFile jar = new JarFile(hostJar.toFile())) { + Manifest mf = jar.getManifest(); + var attrs = mf.getMainAttributes(); + + assertThat(attrs.getValue("Plugin-Class")) + .isEqualTo("io.github.randomcodespace.sonarpredict.hostplugin.SonarPredictHostPlugin"); + assertThat(attrs.getValue("Plugin-Key")).isEqualTo("sonarpredict-host"); + assertThat(attrs.getValue("Plugin-Name")).isEqualTo("Sonar Predictor Host"); + assertThat(attrs.getValue("Plugin-Version")).isNotBlank(); + assertThat(attrs.getValue("Sonar-Version")).isEqualTo("9.9"); + assertThat(attrs.getValue("SonarLint-Supported")).isEqualTo("true"); + } + } + + @Test + void hostJarContainsOnlyHostPluginClasses() throws IOException { + Path hostJar = findHostJar(); + + try (JarFile jar = new JarFile(hostJar.toFile())) { + List classEntries = jar.stream() + .map(JarEntry::getName) + .filter(n -> n.endsWith(".class")) + .sorted() + .toList(); + + assertThat(classEntries).containsExactly( + "io/github/randomcodespace/sonarpredict/hostplugin/NoOpAnalysisWarnings.class", + "io/github/randomcodespace/sonarpredict/hostplugin/SonarPredictHostPlugin.class"); + } + } + + private static Path findHostJar() throws IOException { + Path targetDir = Path.of("target"); + try (Stream entries = Files.list(targetDir)) { + return entries + .filter(p -> { + String n = p.getFileName().toString(); + return n.startsWith("sonar-predictor-") && n.endsWith("-host.jar"); + }) + .findFirst() + .orElseThrow(() -> new IllegalStateException( + "host jar not found in target/; run `mvn package -DskipTests` first")); + } + } +} From 813f2fe155efc67835bc3bbffefc97ae6758ab18 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sun, 24 May 2026 13:15:57 +0000 Subject: [PATCH 10/16] build(dist): ship sonar-predictor-host JAR in the offline plugin bundle --- src/main/assembly/dist.xml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/assembly/dist.xml b/src/main/assembly/dist.xml index b156c27..9b1960f 100644 --- a/src/main/assembly/dist.xml +++ b/src/main/assembly/dist.xml @@ -50,6 +50,17 @@ + + + ${project.build.directory} + plugins + + sonar-predictor-*-host.jar + + + 11.3.0.85510 4.7.7 From 04a93b0d110bf00ac6288105222d1ccaccd76e13 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sun, 24 May 2026 13:17:57 +0000 Subject: [PATCH 12/16] test(hostplugin): verify host JAR loads and AnalysisWarnings bean wires --- .../hostplugin/HostPluginIntegrationTest.java | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/test/java/io/github/randomcodespace/sonarpredict/hostplugin/HostPluginIntegrationTest.java diff --git a/src/test/java/io/github/randomcodespace/sonarpredict/hostplugin/HostPluginIntegrationTest.java b/src/test/java/io/github/randomcodespace/sonarpredict/hostplugin/HostPluginIntegrationTest.java new file mode 100644 index 0000000..049df61 --- /dev/null +++ b/src/test/java/io/github/randomcodespace/sonarpredict/hostplugin/HostPluginIntegrationTest.java @@ -0,0 +1,56 @@ +package io.github.randomcodespace.sonarpredict.hostplugin; + +import static org.assertj.core.api.Assertions.assertThat; + +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; +import io.github.randomcodespace.sonarpredict.daemon.PluginRuntime; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; +import org.sonarsource.sonarlint.core.plugin.commons.LoadedPlugins; + +class HostPluginIntegrationTest { + + private ListAppender appender; + + @BeforeEach + void setUp() { + appender = new ListAppender<>(); + appender.start(); + ((Logger) LoggerFactory.getLogger("ROOT")).addAppender(appender); + } + + @Test + void loadAll_includesHostPluginAndNoMissingBeanErrors() throws IOException { + Path pluginsDir = findDistPluginsDir(); + + LoadedPlugins loaded = PluginRuntime.loadAll(pluginsDir); + + assertThat(loaded.getAllPluginInstancesByKeys()).containsKey("sonarpredict-host"); + assertThat(appender.list).noneMatch(e -> + e.getFormattedMessage() != null + && e.getFormattedMessage().contains("NoSuchBeanDefinitionException")); + } + + private static Path findDistPluginsDir() throws IOException { + Path targetDir = Path.of("target"); + try (Stream dirs = Files.list(targetDir)) { + Path distRoot = dirs + .filter(Files::isDirectory) + .filter(p -> { + String n = p.getFileName().toString(); + return n.startsWith("sonar-predictor-dist-"); + }) + .findFirst() + .orElseThrow(() -> new IllegalStateException( + "dist exploded dir not found in target/; run `mvn package -DskipTests` first")); + return distRoot.resolve("sonar-predictor").resolve("plugins"); + } + } +} From 3c1296ee43ffc2ea9535055199683fbd99c2e148 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sun, 24 May 2026 13:59:09 +0000 Subject: [PATCH 13/16] fix(hostplugin): correct lifespan + loader semantics so the host bean wires MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three corrections surfaced by the full 335-test gate (Task 14 of the LTA 2026.1 migration plan); none was caught by the spec's javap analysis: 1. PluginRuntime.loadFrom — second arg to PluginsLoader.load is disabledPluginsForAnalysis, not additionalAllowedPlugins as the spec assumed. Passing "sonarpredict-host" actively EXCLUDED our host plugin from every Spring analysis container, leaving AnalysisWarnings unwired and reproducing the original "wall". Reverted to Set.of(). 2. NoOpAnalysisWarnings @SonarLintSide(lifespan = INSTANCE -> SINGLE_ANALYSIS). The per-analysis AnalysisContainer calls install(ContainerLifespan.ANALYSIS), which matches the SINGLE_ANALYSIS string constant — not INSTANCE. HtmlSensor, the sensor that triggered the original NoSuchBeanDefinitionException, lives in this same container. 3. host-plugin-jar maven execution moved from prepare-package to process-test-classes, plus a maven-antrun copy-host-plugin-for-tests step that seeds the dev plugins/ dir with the host JAR. Required so the surefire-time daemon tests (which use AnalysisService's no-arg ctor = plugins/) can find the host plugin without a full assembly run. HostPluginIntegrationTest refactored to use @TempDir + hardlink-or-copy the vendored analyzer JARs plus the host JAR, filtering out *-host.jar from the vendored glob to avoid a duplicate FileAlreadyExistsException when antrun has already populated plugins/. After these three corrections, full mvn verify reports 335/335 tests passing, JaCoCo report intact, and the unpacked dist bundle analyzes a Java fixture cleanly with zero NoSuchBeanDefinitionException occurrences. --- pom.xml | 38 ++++++++++++- .../sonarpredict/daemon/PluginRuntime.java | 8 ++- .../hostplugin/NoOpAnalysisWarnings.java | 9 ++- .../hostplugin/HostPluginIntegrationTest.java | 55 ++++++++++++++++--- 4 files changed, 96 insertions(+), 14 deletions(-) diff --git a/pom.xml b/pom.xml index 9d7d77b..e147a9a 100644 --- a/pom.xml +++ b/pom.xml @@ -211,7 +211,7 @@ host-plugin-jar - prepare-package + process-test-classes jar host @@ -400,6 +400,42 @@ + + + org.apache.maven.plugins + maven-antrun-plugin + 3.1.0 + + + copy-host-plugin-for-tests + process-test-classes + run + + + + + + + + + + + + + diff --git a/src/main/java/io/github/randomcodespace/sonarpredict/daemon/PluginRuntime.java b/src/main/java/io/github/randomcodespace/sonarpredict/daemon/PluginRuntime.java index 321d36d..c3ba972 100644 --- a/src/main/java/io/github/randomcodespace/sonarpredict/daemon/PluginRuntime.java +++ b/src/main/java/io/github/randomcodespace/sonarpredict/daemon/PluginRuntime.java @@ -105,7 +105,13 @@ public static LoadedPlugins loadFrom(Set jars) { V1_LANGUAGES, false, detectNodeVersion()); - PluginsLoadResult result = new PluginsLoader().load(config, Set.of("sonarpredict-host")); + // Second argument is disabledPluginsForAnalysis — keys in this set are excluded + // from getAnalysisPluginInstancesByKeys() and therefore never installed into any + // Spring analysis container. Pass an empty set so the host plugin's extensions + // (NoOpAnalysisWarnings) are visible to sensors such as HtmlSensor that autowire + // AnalysisWarnings. The engine's own "additionalAllowedPlugins" list (textdeveloper, + // textenterprise, etc.) is built internally by PluginsLoader and is separate. + PluginsLoadResult result = new PluginsLoader().load(config, Set.of()); return result.getLoadedPlugins(); } diff --git a/src/main/java/io/github/randomcodespace/sonarpredict/hostplugin/NoOpAnalysisWarnings.java b/src/main/java/io/github/randomcodespace/sonarpredict/hostplugin/NoOpAnalysisWarnings.java index b25e8b2..1679727 100644 --- a/src/main/java/io/github/randomcodespace/sonarpredict/hostplugin/NoOpAnalysisWarnings.java +++ b/src/main/java/io/github/randomcodespace/sonarpredict/hostplugin/NoOpAnalysisWarnings.java @@ -12,10 +12,13 @@ * Spring-autowire. Engine 11.3 ships zero references to {@code AnalysisWarnings}, so the * host plugin provides this bean. * - *

Lifespan is {@code INSTANCE} (per-analysis) — the engine instantiates a fresh - * instance per analysis, which resets the dedupe set automatically. + *

Lifespan is {@code SINGLE_ANALYSIS} — maps to {@code ContainerLifespan.ANALYSIS} + * in the engine. The per-analysis {@code AnalysisContainer} calls + * {@code install(ContainerLifespan.ANALYSIS)}, so this bean is registered into the + * same Spring container that wires sensors such as {@code HtmlSensor} which declare + * {@code AnalysisWarnings} as a constructor parameter. */ -@SonarLintSide(lifespan = SonarLintSide.INSTANCE) +@SonarLintSide(lifespan = SonarLintSide.SINGLE_ANALYSIS) public final class NoOpAnalysisWarnings implements AnalysisWarnings { private static final Logger LOG = LoggerFactory.getLogger(NoOpAnalysisWarnings.class); diff --git a/src/test/java/io/github/randomcodespace/sonarpredict/hostplugin/HostPluginIntegrationTest.java b/src/test/java/io/github/randomcodespace/sonarpredict/hostplugin/HostPluginIntegrationTest.java index 049df61..78bf101 100644 --- a/src/test/java/io/github/randomcodespace/sonarpredict/hostplugin/HostPluginIntegrationTest.java +++ b/src/test/java/io/github/randomcodespace/sonarpredict/hostplugin/HostPluginIntegrationTest.java @@ -12,11 +12,15 @@ import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.slf4j.LoggerFactory; import org.sonarsource.sonarlint.core.plugin.commons.LoadedPlugins; class HostPluginIntegrationTest { + @TempDir + Path tempPluginsDir; + private ListAppender appender; @BeforeEach @@ -28,9 +32,9 @@ void setUp() { @Test void loadAll_includesHostPluginAndNoMissingBeanErrors() throws IOException { - Path pluginsDir = findDistPluginsDir(); + populateTempPluginsDir(tempPluginsDir); - LoadedPlugins loaded = PluginRuntime.loadAll(pluginsDir); + LoadedPlugins loaded = PluginRuntime.loadAll(tempPluginsDir); assertThat(loaded.getAllPluginInstancesByKeys()).containsKey("sonarpredict-host"); assertThat(appender.list).noneMatch(e -> @@ -38,19 +42,52 @@ void loadAll_includesHostPluginAndNoMissingBeanErrors() throws IOException { && e.getFormattedMessage().contains("NoSuchBeanDefinitionException")); } - private static Path findDistPluginsDir() throws IOException { + /** + * Assembles a temporary plugins directory by hard-linking all JARs from the + * vendored {@code plugins/} directory (populated at process-test-resources) + * and the host plugin JAR produced at process-test-classes. + * + *

Hard-links are used where supported (same filesystem); Files.copy falls + * back transparently when the link target is on a different device. + */ + private static void populateTempPluginsDir(Path tempDir) throws IOException { + // Vendor analyzer JARs — present at process-test-resources phase. + // Exclude the host JAR (sonar-predictor-*-host.jar) from this glob: + // antrun copies it into plugins/ at process-test-classes, but findHostJar() + // below adds it explicitly to avoid a duplicate / FileAlreadyExistsException. + Path vendorPluginsDir = Path.of("plugins"); + try (Stream entries = Files.list(vendorPluginsDir)) { + for (Path jar : (Iterable) entries + .filter(p -> p.getFileName().toString().endsWith(".jar")) + .filter(p -> !p.getFileName().toString().contains("-host.jar"))::iterator) { + linkOrCopy(jar, tempDir.resolve(jar.getFileName())); + } + } + + // Host plugin JAR — produced at process-test-classes phase + Path hostJar = findHostJar(); + linkOrCopy(hostJar, tempDir.resolve(hostJar.getFileName())); + } + + private static void linkOrCopy(Path source, Path target) throws IOException { + try { + Files.createLink(target, source.toAbsolutePath()); + } catch (UnsupportedOperationException | IOException e) { + Files.copy(source.toAbsolutePath(), target); + } + } + + private static Path findHostJar() throws IOException { Path targetDir = Path.of("target"); - try (Stream dirs = Files.list(targetDir)) { - Path distRoot = dirs - .filter(Files::isDirectory) + try (Stream entries = Files.list(targetDir)) { + return entries .filter(p -> { String n = p.getFileName().toString(); - return n.startsWith("sonar-predictor-dist-"); + return n.startsWith("sonar-predictor-") && n.endsWith("-host.jar"); }) .findFirst() .orElseThrow(() -> new IllegalStateException( - "dist exploded dir not found in target/; run `mvn package -DskipTests` first")); - return distRoot.resolve("sonar-predictor").resolve("plugins"); + "host jar not found in target/; run `mvn package -DskipTests` first")); } } } From 1bc8972c66e649823deaa2f435c6146d8f4864a9 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sun, 24 May 2026 15:34:07 +0000 Subject: [PATCH 14/16] build: require Java 21 (sonarlint-analysis-engine 11.x ships major-65 class files) LTA 2026.1's sonarlint-analysis-engine is compiled for Java 21. A javac on JDK 17 cannot read its class files and bails out with: cannot access org.sonarsource.sonarlint.core.analysis.AnalysisScheduler bad class file: .../sonarlint-analysis-engine-11.3.0.85510.jar(...) class file has wrong version 65.0, should be 61.0 The local 335-test gate masked this because the dev machine was on JDK 25. CI on JDK 17 surfaced it on the first push. Changes: - pom.xml: maven.compiler.release/source/target 17 -> 21, with comment documenting the LTA-imposed minimum. - .github/workflows/{ci,parity,publish,sonar}.yml: setup-java java-version '17' -> '21' (all four pipelines). - src/main/staging/bin/sonar: MIN_MAJOR 17 -> 21, renamed is_java_17_plus -> is_java_min_plus, updated user-facing messages. - src/main/staging/bin/sonar.bat: matched changes (GEQ 17 -> GEQ 21, messages). - README.md: prerequisites table 'Java 17+' -> 'Java 21+'. --- .github/workflows/ci.yml | 4 ++-- .github/workflows/parity.yml | 4 ++-- .github/workflows/publish.yml | 4 ++-- .github/workflows/sonar.yml | 7 ++++--- README.md | 2 +- pom.xml | 12 +++++++++--- src/main/staging/bin/sonar | 23 ++++++++++++----------- src/main/staging/bin/sonar.bat | 10 +++++----- 8 files changed, 37 insertions(+), 29 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f9c1f50..2c11fe9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,11 +16,11 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v4 with: distribution: temurin - java-version: '17' + java-version: '21' cache: maven - name: Set up Node.js (JS/TS analyzer tests need a Node runtime) diff --git a/.github/workflows/parity.yml b/.github/workflows/parity.yml index 94ce447..cc73b57 100644 --- a/.github/workflows/parity.yml +++ b/.github/workflows/parity.yml @@ -49,11 +49,11 @@ jobs: with: fetch-depth: 0 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v4 with: distribution: temurin - java-version: '17' + java-version: '21' - name: Set up Node.js 20 uses: actions/setup-node@v4 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index bf15ccc..2f2adbd 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -50,11 +50,11 @@ jobs: with: fetch-depth: 0 - - name: Set up JDK 17 + GPG + - name: Set up JDK 21 + GPG uses: actions/setup-java@v4 with: distribution: temurin - java-version: '17' + java-version: '21' server-id: central server-username: OSS_NEXUS_USER server-password: OSS_NEXUS_PASS diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 06e092a..e521b74 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -39,12 +39,13 @@ jobs: with: fetch-depth: 0 - # JDK 17 is the project's build/runtime target. Temurin is the safe default. - - name: Set up JDK 17 + # JDK 21 is the project's build/runtime target (required by + # sonarlint-analysis-engine 11.x / LTA 2026.1). Temurin is the safe default. + - name: Set up JDK 21 uses: actions/setup-java@v4 with: distribution: temurin - java-version: '17' + java-version: '21' # The JS/TS analyzer plugin spawns Node at runtime to lint JS/TS sources, # so Node must be on PATH when the scan runs (not just at build time). diff --git a/README.md b/README.md index d2093c6..0ee2f35 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ The JSON output carries both fields on every issue. | Requirement | For | |---|---| -| **Java 17+** (JDK or JRE) | running the CLI and daemon — auto-discovered (`JAVA_HOME` → `PATH` → common install locations) | +| **Java 21+** (JDK or JRE) | running the CLI and daemon — auto-discovered (`JAVA_HOME` → `PATH` → common install locations) | | **Linux or macOS** | the daemon uses Unix domain sockets (Windows support is on the roadmap) | | **`git`** | the `check --diff` workflow | | **Node.js 18.17+** | JavaScript / TypeScript analysis | diff --git a/pom.xml b/pom.xml index e147a9a..40d08b6 100644 --- a/pom.xml +++ b/pom.xml @@ -54,9 +54,15 @@ - 17 - 17 - 17 + + 21 + 21 + 21 UTF-8 2.17.2 diff --git a/src/main/staging/bin/sonar b/src/main/staging/bin/sonar index 63d5c7b..133df6c 100644 --- a/src/main/staging/bin/sonar +++ b/src/main/staging/bin/sonar @@ -1,11 +1,11 @@ #!/bin/sh # sonar-predictor launcher — self-contained distribution entry point. # -# Auto-discovers a Java 17+ runtime so neither the agent nor the user has to +# Auto-discovers a Java 21+ runtime so neither the agent nor the user has to # set JAVA_HOME or put a compatible java on PATH. Then runs the bundled CLI, # passing it the discovered java and the bundle's jar/plugin locations: # -# -Dsonar.java.exe the Java 17+ executable (used to spawn the daemon) +# -Dsonar.java.exe the Java 21+ executable (used to spawn the daemon) # -Dsonar.daemon.jar lib/sonar-predictor-daemon.jar # -Dsonar.plugins.dir plugins/ (the 10 SonarSource analyzer jars) # @@ -41,9 +41,10 @@ if [ -z "$CLI_JAR" ] || [ ! -f "$CLI_JAR" ]; then exit 2 fi -# --- Java 17+ discovery ----------------------------------------------------- -# Returns 0 and prints nothing if the given java is >= 17; non-zero otherwise. -MIN_MAJOR=17 +# --- Java 21+ discovery ----------------------------------------------------- +# Returns 0 and prints nothing if the given java is >= 21; non-zero otherwise. +# (sonarlint-analysis-engine 11.x / LTA 2026.1 ships Java 21 class files.) +MIN_MAJOR=21 java_major() { # Print the major version of the java executable in $1, or nothing. @@ -60,7 +61,7 @@ java_major() { esac } -is_java_17_plus() { +is_java_min_plus() { _m=$(java_major "$1" 2>/dev/null) || return 1 [ -n "$_m" ] || return 1 [ "$_m" -ge "$MIN_MAJOR" ] 2>/dev/null || return 1 @@ -70,14 +71,14 @@ is_java_17_plus() { JAVA="" # 1. $JAVA_HOME/bin/java -if [ -n "${JAVA_HOME:-}" ] && is_java_17_plus "$JAVA_HOME/bin/java"; then +if [ -n "${JAVA_HOME:-}" ] && is_java_min_plus "$JAVA_HOME/bin/java"; then JAVA="$JAVA_HOME/bin/java" fi # 2. java on PATH if [ -z "$JAVA" ]; then _path_java=$(command -v java 2>/dev/null || true) - if [ -n "$_path_java" ] && is_java_17_plus "$_path_java"; then + if [ -n "$_path_java" ] && is_java_min_plus "$_path_java"; then JAVA="$_path_java" fi fi @@ -93,7 +94,7 @@ if [ -z "$JAVA" ]; then /opt/*/bin/java do [ -x "$_cand" ] || continue - if is_java_17_plus "$_cand"; then + if is_java_min_plus "$_cand"; then JAVA="$_cand" break fi @@ -101,9 +102,9 @@ if [ -z "$JAVA" ]; then fi if [ -z "$JAVA" ]; then - echo "sonar-predictor needs Java 17+; none found." >&2 + echo "sonar-predictor needs Java 21+; none found." >&2 echo "Checked: \$JAVA_HOME, PATH, and common install locations." >&2 - echo "Install a JDK/JRE 17 or newer, or set JAVA_HOME to one." >&2 + echo "Install a JDK/JRE 21 or newer, or set JAVA_HOME to one." >&2 exit 2 fi diff --git a/src/main/staging/bin/sonar.bat b/src/main/staging/bin/sonar.bat index 7b804c4..c5ae03b 100644 --- a/src/main/staging/bin/sonar.bat +++ b/src/main/staging/bin/sonar.bat @@ -2,7 +2,7 @@ setlocal EnableDelayedExpansion rem sonar-predictor launcher (Windows) — self-contained distribution entry point. rem -rem Auto-discovers a Java 17+ runtime so no JAVA_HOME / PATH setup is required, +rem Auto-discovers a Java 21+ runtime so no JAVA_HOME / PATH setup is required, rem then runs the bundled CLI with the bundle's jar/plugin locations passed in. rem rem TODO(windows): the daemon's IPC uses a Unix domain socket path; full @@ -76,16 +76,16 @@ if not defined JAVA ( ) if not defined JAVA ( - echo sonar-predictor needs Java 17+; none found. 1>&2 + echo sonar-predictor needs Java 21+; none found. 1>&2 echo Checked: %%JAVA_HOME%%, PATH, and common install locations. 1>&2 - echo Install a JDK/JRE 17 or newer, or set JAVA_HOME to one. 1>&2 + echo Install a JDK/JRE 21 or newer, or set JAVA_HOME to one. 1>&2 exit /b 2 ) "%JAVA%" "-Dsonar.java.exe=%JAVA%" "-Dsonar.daemon.jar=%DAEMON_JAR%" "-Dsonar.plugins.dir=%PLUGINS_DIR%" -jar "%CLI_JAR%" %* exit /b %ERRORLEVEL% -rem --- :checkJava -> errorlevel 0 if major version >= 17 ---------- +rem --- :checkJava -> errorlevel 0 if major version >= 21 ---------- :checkJava set "_JV=" for /f "tokens=3" %%v in ('"%~1" -version 2^>^&1 ^| findstr /i "version"') do ( @@ -97,4 +97,4 @@ for /f "tokens=1,2 delims=." %%a in ("%_JV%") do ( if "%%a"=="1" ( set "_MAJOR=%%b" ) else ( set "_MAJOR=%%a" ) ) if not defined _MAJOR exit /b 1 -if %_MAJOR% GEQ 17 ( exit /b 0 ) else ( exit /b 1 ) +if %_MAJOR% GEQ 21 ( exit /b 0 ) else ( exit /b 1 ) From f42d84465555f5bbe11b9316f68f14433721dbfe Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sun, 24 May 2026 15:51:31 +0000 Subject: [PATCH 15/16] =?UTF-8?q?build(test):=20drop=20ch.qos.logback=20de?= =?UTF-8?q?p=20=E2=80=94=20Socket=20flagged=20it=20obfuscatedFile=20(3=20H?= =?UTF-8?q?IGH)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Socket Security raised three HIGH-severity obfuscatedFile alerts against ch.qos.logback:{logback-classic,logback-core}:1.5.13 on PR #8. Logback's optimized class files trip Socket's static-analysis pattern detector; it's almost certainly a false positive against a widely-trusted library, but the right answer is to not depend on it from a project that has no need for a concrete SLF4J binding. The dep was added by Task 5 (NoOpAnalysisWarnings TDD) solely so the unit test could capture log output via logback's ListAppender. Two test sites used logback APIs; both refactored to stay on slf4j-api only: - NoOpAnalysisWarnings gains a package-private mutable `log` field that defaults to the static slf4j Logger but can be replaced by tests with a Mockito-mocked org.slf4j.Logger. The production class still uses slf4j-api exclusively. NoOpAnalysisWarningsTest now verifies via verify(logger).warn(...) calls (5 tests, +1 for null-message behavior). - EngineLog gains a static current() accessor that returns the most recently installed instance. HostPluginIntegrationTest now reads EngineLog.current().messages() directly to assert no NoSuchBeanDefinitionException, instead of capturing via logback's ROOT logger ListAppender. pom.xml: ch.qos.logback:logback-classic:1.5.13 dep removed entirely; mvn dependency:tree confirms logback no longer reaches the test classpath via any transitive route. Verified: mvn test = 336/336 pass on the host JDK 21 (was 335; the refactored NoOpAnalysisWarningsTest gained one additional test for null-message behavior). --- pom.xml | 6 -- .../sonarpredict/daemon/EngineLog.java | 14 +++++ .../hostplugin/NoOpAnalysisWarnings.java | 13 +++- .../hostplugin/HostPluginIntegrationTest.java | 25 +++----- .../hostplugin/NoOpAnalysisWarningsTest.java | 61 +++++++++++-------- 5 files changed, 69 insertions(+), 50 deletions(-) diff --git a/pom.xml b/pom.xml index 40d08b6..135a623 100644 --- a/pom.xml +++ b/pom.xml @@ -148,12 +148,6 @@ junit-jupiter test - - ch.qos.logback - logback-classic - 1.5.13 - test - org.assertj assertj-core diff --git a/src/main/java/io/github/randomcodespace/sonarpredict/daemon/EngineLog.java b/src/main/java/io/github/randomcodespace/sonarpredict/daemon/EngineLog.java index 32cb249..d44fa44 100644 --- a/src/main/java/io/github/randomcodespace/sonarpredict/daemon/EngineLog.java +++ b/src/main/java/io/github/randomcodespace/sonarpredict/daemon/EngineLog.java @@ -21,6 +21,8 @@ */ public final class EngineLog implements LogOutput { + private static volatile EngineLog current; + private final List messages = new CopyOnWriteArrayList<>(); /** @@ -47,9 +49,21 @@ public static void install() { public static EngineLog installAndCapture() { EngineLog target = new EngineLog(); SonarLintLogger.get().setTarget(target); + current = target; return target; } + /** + * Returns the most recently {@link #install installed} {@code EngineLog}, + * or {@code null} if none has been installed yet. Tests use this to assert + * on engine messages emitted by code paths (such as + * {@code PluginRuntime.loadAll}) that install their own {@code EngineLog} + * internally without exposing the reference. + */ + public static EngineLog current() { + return current; + } + @Override public void log(String formattedMessage, Level level) { if (formattedMessage != null) { diff --git a/src/main/java/io/github/randomcodespace/sonarpredict/hostplugin/NoOpAnalysisWarnings.java b/src/main/java/io/github/randomcodespace/sonarpredict/hostplugin/NoOpAnalysisWarnings.java index 1679727..6732a9a 100644 --- a/src/main/java/io/github/randomcodespace/sonarpredict/hostplugin/NoOpAnalysisWarnings.java +++ b/src/main/java/io/github/randomcodespace/sonarpredict/hostplugin/NoOpAnalysisWarnings.java @@ -21,14 +21,23 @@ @SonarLintSide(lifespan = SonarLintSide.SINGLE_ANALYSIS) public final class NoOpAnalysisWarnings implements AnalysisWarnings { - private static final Logger LOG = LoggerFactory.getLogger(NoOpAnalysisWarnings.class); + private static final Logger DEFAULT_LOG = LoggerFactory.getLogger(NoOpAnalysisWarnings.class); + + /** + * The logger this instance writes through. Package-private and non-final + * so tests can substitute a Mockito-mocked {@link Logger} without + * pulling logback onto the test classpath (Socket Security flags + * logback's class files as {@code obfuscatedFile}; we keep our + * dependency surface clean by mocking the slf4j API directly). + */ + Logger log = DEFAULT_LOG; private final Set seen = ConcurrentHashMap.newKeySet(); @Override public void addUnique(String message) { if (seen.add(message)) { - LOG.warn("[sonar] {}", message); + log.warn("[sonar] {}", message); } } } diff --git a/src/test/java/io/github/randomcodespace/sonarpredict/hostplugin/HostPluginIntegrationTest.java b/src/test/java/io/github/randomcodespace/sonarpredict/hostplugin/HostPluginIntegrationTest.java index 78bf101..54c8588 100644 --- a/src/test/java/io/github/randomcodespace/sonarpredict/hostplugin/HostPluginIntegrationTest.java +++ b/src/test/java/io/github/randomcodespace/sonarpredict/hostplugin/HostPluginIntegrationTest.java @@ -2,18 +2,14 @@ import static org.assertj.core.api.Assertions.assertThat; -import ch.qos.logback.classic.Logger; -import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.core.read.ListAppender; +import io.github.randomcodespace.sonarpredict.daemon.EngineLog; import io.github.randomcodespace.sonarpredict.daemon.PluginRuntime; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.stream.Stream; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import org.slf4j.LoggerFactory; import org.sonarsource.sonarlint.core.plugin.commons.LoadedPlugins; class HostPluginIntegrationTest { @@ -21,15 +17,6 @@ class HostPluginIntegrationTest { @TempDir Path tempPluginsDir; - private ListAppender appender; - - @BeforeEach - void setUp() { - appender = new ListAppender<>(); - appender.start(); - ((Logger) LoggerFactory.getLogger("ROOT")).addAppender(appender); - } - @Test void loadAll_includesHostPluginAndNoMissingBeanErrors() throws IOException { populateTempPluginsDir(tempPluginsDir); @@ -37,9 +24,13 @@ void loadAll_includesHostPluginAndNoMissingBeanErrors() throws IOException { LoadedPlugins loaded = PluginRuntime.loadAll(tempPluginsDir); assertThat(loaded.getAllPluginInstancesByKeys()).containsKey("sonarpredict-host"); - assertThat(appender.list).noneMatch(e -> - e.getFormattedMessage() != null - && e.getFormattedMessage().contains("NoSuchBeanDefinitionException")); + + // PluginRuntime.loadAll installed a fresh EngineLog as the global + // SonarLint log target before loading; inspect its captured messages + // for the missing-bean error that signaled the original wall. + EngineLog engineLog = EngineLog.current(); + assertThat(engineLog).isNotNull(); + assertThat(engineLog.messages()).noneMatch(m -> m.contains("NoSuchBeanDefinitionException")); } /** diff --git a/src/test/java/io/github/randomcodespace/sonarpredict/hostplugin/NoOpAnalysisWarningsTest.java b/src/test/java/io/github/randomcodespace/sonarpredict/hostplugin/NoOpAnalysisWarningsTest.java index 88a2ec2..62cdfba 100644 --- a/src/test/java/io/github/randomcodespace/sonarpredict/hostplugin/NoOpAnalysisWarningsTest.java +++ b/src/test/java/io/github/randomcodespace/sonarpredict/hostplugin/NoOpAnalysisWarningsTest.java @@ -1,43 +1,34 @@ package io.github.randomcodespace.sonarpredict.hostplugin; -import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; -import ch.qos.logback.classic.Level; -import ch.qos.logback.classic.Logger; -import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.core.read.ListAppender; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.slf4j.LoggerFactory; +import org.slf4j.Logger; class NoOpAnalysisWarningsTest { private Logger logger; - private ListAppender appender; private NoOpAnalysisWarnings warnings; @BeforeEach void setUp() { - logger = (Logger) LoggerFactory.getLogger(NoOpAnalysisWarnings.class); - appender = new ListAppender<>(); - appender.start(); - logger.addAppender(appender); + logger = mock(Logger.class); warnings = new NoOpAnalysisWarnings(); - } - - @AfterEach - void tearDown() { - logger.detachAppender(appender); + warnings.log = logger; } @Test - void addUnique_logsAtWarn() { + void addUnique_logsAtWarnWithSonarPrefix() { warnings.addUnique("memory limit reached"); - assertThat(appender.list).hasSize(1); - assertThat(appender.list.get(0).getLevel()).isEqualTo(Level.WARN); - assertThat(appender.list.get(0).getFormattedMessage()).contains("[sonar]", "memory limit reached"); + verify(logger).warn(eq("[sonar] {}"), eq("memory limit reached")); + verifyNoMoreInteractions(logger); } @Test @@ -46,7 +37,8 @@ void addUnique_dedupsIdenticalMessages() { warnings.addUnique("same"); warnings.addUnique("same"); - assertThat(appender.list).hasSize(1); + verify(logger, times(1)).warn(eq("[sonar] {}"), eq("same")); + verifyNoMoreInteractions(logger); } @Test @@ -54,14 +46,33 @@ void addUnique_distinctMessagesAllLog() { warnings.addUnique("first"); warnings.addUnique("second"); - assertThat(appender.list).hasSize(2); + verify(logger).warn(eq("[sonar] {}"), eq("first")); + verify(logger).warn(eq("[sonar] {}"), eq("second")); + verifyNoMoreInteractions(logger); } @Test void freshInstance_doesNotShareDedupeSet() { warnings.addUnique("X"); - new NoOpAnalysisWarnings().addUnique("X"); - assertThat(appender.list).hasSize(2); + Logger second = mock(Logger.class); + NoOpAnalysisWarnings fresh = new NoOpAnalysisWarnings(); + fresh.log = second; + fresh.addUnique("X"); + + verify(logger).warn(eq("[sonar] {}"), eq("X")); + verify(second).warn(eq("[sonar] {}"), eq("X")); + } + + @Test + void addUnique_nullMessageStillRoutesThroughLogger() { + // Sensors may pass null in pathological cases; our implementation + // simply delegates to ConcurrentHashMap which rejects null with NPE. + // This test pins the current behavior so a future change is intentional. + org.assertj.core.api.Assertions + .assertThatThrownBy(() -> warnings.addUnique(null)) + .isInstanceOf(NullPointerException.class); + verify(logger, never()).warn(org.mockito.ArgumentMatchers.anyString(), + org.mockito.ArgumentMatchers.any()); } } From d600cdf5a5b936ff99e6aec8bf8d6117db34a80f Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sun, 24 May 2026 16:05:05 +0000 Subject: [PATCH 16/16] fix(daemon): EngineLog.current uses AtomicReference, not volatile field (S3077) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SonarCloud's quality gate flagged the volatile EngineLog reference added in the previous commit: java:S3077 — Use a thread-safe type; adding 'volatile' is not enough to make this field thread-safe. Switched to AtomicReference. The rule's intent is that mutable-state shared between threads should use the j.u.c.atomic types even when each individual read/write is atomic; volatile only handles visibility, not compound operations a future caller might add. Tests unchanged (336/336). --- .../randomcodespace/sonarpredict/daemon/EngineLog.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/github/randomcodespace/sonarpredict/daemon/EngineLog.java b/src/main/java/io/github/randomcodespace/sonarpredict/daemon/EngineLog.java index d44fa44..ae154eb 100644 --- a/src/main/java/io/github/randomcodespace/sonarpredict/daemon/EngineLog.java +++ b/src/main/java/io/github/randomcodespace/sonarpredict/daemon/EngineLog.java @@ -2,6 +2,7 @@ import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicReference; import org.sonarsource.sonarlint.core.commons.log.LogOutput; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; @@ -21,7 +22,7 @@ */ public final class EngineLog implements LogOutput { - private static volatile EngineLog current; + private static final AtomicReference CURRENT = new AtomicReference<>(); private final List messages = new CopyOnWriteArrayList<>(); @@ -49,7 +50,7 @@ public static void install() { public static EngineLog installAndCapture() { EngineLog target = new EngineLog(); SonarLintLogger.get().setTarget(target); - current = target; + CURRENT.set(target); return target; } @@ -61,7 +62,7 @@ public static EngineLog installAndCapture() { * internally without exposing the reference. */ public static EngineLog current() { - return current; + return CURRENT.get(); } @Override