Skip to content

Commit 976a8c4

Browse files
authored
feat: migrate to SonarQube 2026.1 LTA (#8)
* build: bump sonarlint-analysis-engine 10.24 -> 11.3 + LTA 2026.1 analyzers 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. * feat(daemon): add SimpleActiveRule adapter for engine 11.x ActiveRule SPI * 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. * feat(hostplugin): SonarPredictHostPlugin registers NoOpAnalysisWarnings * fix(daemon): adapt PluginRuntime to engine 11.x SonarLanguage API - 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) * 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. * 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. * build: emit sonar-predictor-host plugin JAR as classifier=host Second maven-jar-plugin execution scoped to the hostplugin package produces target/sonar-predictor-<v>-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. * test(hostplugin): pin host JAR manifest contract * build(dist): ship sonar-predictor-host JAR in the offline plugin bundle * docs(pom): document LTA 2026.1 pinning + upgrade procedure * test(hostplugin): verify host JAR loads and AnalysisWarnings bean wires * fix(hostplugin): correct lifespan + loader semantics so the host bean wires 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. * 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+'. * build(test): drop ch.qos.logback dep — Socket flagged it obfuscatedFile (3 HIGH) 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). * fix(daemon): EngineLog.current uses AtomicReference, not volatile field (S3077) 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<EngineLog>. 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).
1 parent 30c89ce commit 976a8c4

22 files changed

Lines changed: 556 additions & 54 deletions

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ jobs:
1616
- name: Checkout
1717
uses: actions/checkout@v4
1818

19-
- name: Set up JDK 17
19+
- name: Set up JDK 21
2020
uses: actions/setup-java@v4
2121
with:
2222
distribution: temurin
23-
java-version: '17'
23+
java-version: '21'
2424
cache: maven
2525

2626
- name: Set up Node.js (JS/TS analyzer tests need a Node runtime)

.github/workflows/parity.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,11 @@ jobs:
4949
with:
5050
fetch-depth: 0
5151

52-
- name: Set up JDK 17
52+
- name: Set up JDK 21
5353
uses: actions/setup-java@v4
5454
with:
5555
distribution: temurin
56-
java-version: '17'
56+
java-version: '21'
5757

5858
- name: Set up Node.js 20
5959
uses: actions/setup-node@v4

.github/workflows/publish.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,11 @@ jobs:
5050
with:
5151
fetch-depth: 0
5252

53-
- name: Set up JDK 17 + GPG
53+
- name: Set up JDK 21 + GPG
5454
uses: actions/setup-java@v4
5555
with:
5656
distribution: temurin
57-
java-version: '17'
57+
java-version: '21'
5858
server-id: central
5959
server-username: OSS_NEXUS_USER
6060
server-password: OSS_NEXUS_PASS

.github/workflows/sonar.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,13 @@ jobs:
3939
with:
4040
fetch-depth: 0
4141

42-
# JDK 17 is the project's build/runtime target. Temurin is the safe default.
43-
- name: Set up JDK 17
42+
# JDK 21 is the project's build/runtime target (required by
43+
# sonarlint-analysis-engine 11.x / LTA 2026.1). Temurin is the safe default.
44+
- name: Set up JDK 21
4445
uses: actions/setup-java@v4
4546
with:
4647
distribution: temurin
47-
java-version: '17'
48+
java-version: '21'
4849

4950
# The JS/TS analyzer plugin spawns Node at runtime to lint JS/TS sources,
5051
# so Node must be on PATH when the scan runs (not just at build time).

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ The JSON output carries both fields on every issue.
9898

9999
| Requirement | For |
100100
|---|---|
101-
| **Java 17+** (JDK or JRE) | running the CLI and daemon — auto-discovered (`JAVA_HOME``PATH` → common install locations) |
101+
| **Java 21+** (JDK or JRE) | running the CLI and daemon — auto-discovered (`JAVA_HOME``PATH` → common install locations) |
102102
| **Linux or macOS** | the daemon uses Unix domain sockets (Windows support is on the roadmap) |
103103
| **`git`** | the `check --diff` workflow |
104104
| **Node.js 18.17+** | JavaScript / TypeScript analysis |

pom.xml

Lines changed: 102 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -54,32 +54,51 @@
5454
</issueManagement>
5555

5656
<properties>
57-
<maven.compiler.release>17</maven.compiler.release>
58-
<maven.compiler.source>17</maven.compiler.source>
59-
<maven.compiler.target>17</maven.compiler.target>
57+
<!--
58+
Java 21 minimum: sonarlint-analysis-engine 11.3.0.85510 (LTA 2026.1)
59+
ships class files at major version 65 (Java 21). Compiling against
60+
it requires a JDK 21+ toolchain. CI's setup-java step is pinned to
61+
21 in .github/workflows/*.yml to match.
62+
-->
63+
<maven.compiler.release>21</maven.compiler.release>
64+
<maven.compiler.source>21</maven.compiler.source>
65+
<maven.compiler.target>21</maven.compiler.target>
6066
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
6167

6268
<jackson.version>2.17.2</jackson.version>
6369
<junit.version>5.10.2</junit.version>
64-
<sonarlint.engine.version>10.24.0.81415</sonarlint.engine.version>
70+
<!--
71+
These versions track SonarQube Server 2026.1 LTA (released 2025-12-11).
72+
73+
To upgrade to a future LTA:
74+
1. Look up the new LTA's bundled analyzer versions in the release
75+
notes at https://docs.sonarsource.com/sonarqube-server/&lt;v&gt;/setup-and-upgrade/release-upgrade-notes/
76+
2. Bump all 11 version properties below.
77+
3. Run `mvn clean test` — the daemon test suite is the canary.
78+
4. If a NEW required host bean appears (NoSuchBeanDefinitionException
79+
on a type other than AnalysisWarnings), add an @SonarLintSide no-op
80+
class in src/main/java/.../hostplugin/ and one context.addExtension(...)
81+
line in SonarPredictHostPlugin.define(). See spec §7.2 for details.
82+
-->
83+
<sonarlint.engine.version>11.3.0.85510</sonarlint.engine.version>
6584
<picocli.version>4.7.7</picocli.version>
6685

6786
<!--
6887
The 10 SonarSource analyzer plugins — the runtime assets the daemon
6988
loads. Single source of truth for the maven-dependency-plugin copy
7089
executions below, which fetch them through Maven (so settings.xml
71-
mirrors / Nexus / proxy apply). Versions match engine 10.24.0.81415.
90+
mirrors / Nexus / proxy apply). Versions match engine 11.3.0.85510 (LTA 2026.1).
7291
-->
73-
<sonar.plugin.java.version>8.15.0.39343</sonar.plugin.java.version>
74-
<sonar.plugin.python.version>5.5.0.23291</sonar.plugin.python.version>
75-
<sonar.plugin.javascript.version>10.24.0.33043</sonar.plugin.javascript.version>
76-
<sonar.plugin.php.version>3.46.0.13151</sonar.plugin.php.version>
77-
<sonar.plugin.kotlin.version>3.2.0.7239</sonar.plugin.kotlin.version>
92+
<sonar.plugin.java.version>8.29.0.43460</sonar.plugin.java.version>
93+
<sonar.plugin.python.version>5.22.0.33216</sonar.plugin.python.version>
94+
<sonar.plugin.javascript.version>12.5.0.41048</sonar.plugin.javascript.version>
95+
<sonar.plugin.php.version>3.57.0.15976</sonar.plugin.php.version>
96+
<sonar.plugin.kotlin.version>3.6.0.9326</sonar.plugin.kotlin.version>
7897
<sonar.plugin.go.version>1.18.1.827</sonar.plugin.go.version>
79-
<sonar.plugin.ruby.version>1.19.0.471</sonar.plugin.ruby.version>
80-
<sonar.plugin.scala.version>1.19.0.484</sonar.plugin.scala.version>
81-
<sonar.plugin.html.version>3.19.0.5695</sonar.plugin.html.version>
82-
<sonar.plugin.xml.version>2.13.0.5938</sonar.plugin.xml.version>
98+
<sonar.plugin.ruby.version>1.22.0.1992</sonar.plugin.ruby.version>
99+
<sonar.plugin.scala.version>1.23.0.2394</sonar.plugin.scala.version>
100+
<sonar.plugin.html.version>3.27.0.7699</sonar.plugin.html.version>
101+
<sonar.plugin.xml.version>2.17.0.7895</sonar.plugin.xml.version>
83102
</properties>
84103

85104
<dependencyManagement>
@@ -129,6 +148,18 @@
129148
<artifactId>junit-jupiter</artifactId>
130149
<scope>test</scope>
131150
</dependency>
151+
<dependency>
152+
<groupId>org.assertj</groupId>
153+
<artifactId>assertj-core</artifactId>
154+
<version>3.27.7</version>
155+
<scope>test</scope>
156+
</dependency>
157+
<dependency>
158+
<groupId>org.mockito</groupId>
159+
<artifactId>mockito-core</artifactId>
160+
<version>5.15.2</version>
161+
<scope>test</scope>
162+
</dependency>
132163
</dependencies>
133164

134165
<build>
@@ -178,6 +209,27 @@
178209
<phase>process-classes</phase>
179210
<goals><goal>jar</goal></goals>
180211
</execution>
212+
<execution>
213+
<id>host-plugin-jar</id>
214+
<phase>process-test-classes</phase>
215+
<goals><goal>jar</goal></goals>
216+
<configuration>
217+
<classifier>host</classifier>
218+
<includes>
219+
<include>io/github/randomcodespace/sonarpredict/hostplugin/**</include>
220+
</includes>
221+
<archive>
222+
<manifestEntries>
223+
<Plugin-Class>io.github.randomcodespace.sonarpredict.hostplugin.SonarPredictHostPlugin</Plugin-Class>
224+
<Plugin-Key>sonarpredict-host</Plugin-Key>
225+
<Plugin-Name>Sonar Predictor Host</Plugin-Name>
226+
<Plugin-Version>${project.version}</Plugin-Version>
227+
<Sonar-Version>9.9</Sonar-Version>
228+
<SonarLint-Supported>true</SonarLint-Supported>
229+
</manifestEntries>
230+
</archive>
231+
</configuration>
232+
</execution>
181233
</executions>
182234
</plugin>
183235

@@ -348,6 +400,42 @@
348400
</executions>
349401
</plugin>
350402

403+
<!--
404+
Copy the host plugin jar into the dev plugins/ directory so
405+
that AnalysisService (no-arg constructor → plugins/) and
406+
PluginRuntime.loadAll pick it up during mvn test.
407+
408+
The host-plugin-jar jar-plugin execution (process-test-classes)
409+
produces target/sonar-predictor-${project.version}-host.jar.
410+
This antrun execution binds to the SAME phase so it runs
411+
immediately after the jar is written.
412+
413+
The file is gitignored (plugins/*.jar). In production the host
414+
jar ships inside the dist zip's plugins/ directory via the
415+
assembly descriptor, so no separate production step is needed.
416+
-->
417+
<plugin>
418+
<groupId>org.apache.maven.plugins</groupId>
419+
<artifactId>maven-antrun-plugin</artifactId>
420+
<version>3.1.0</version>
421+
<executions>
422+
<execution>
423+
<id>copy-host-plugin-for-tests</id>
424+
<phase>process-test-classes</phase>
425+
<goals><goal>run</goal></goals>
426+
<configuration>
427+
<target>
428+
<copy todir="${project.basedir}/plugins" overwrite="true">
429+
<fileset dir="${project.build.directory}">
430+
<include name="${project.artifactId}-${project.version}-host.jar"/>
431+
</fileset>
432+
</copy>
433+
</target>
434+
</configuration>
435+
</execution>
436+
</executions>
437+
</plugin>
438+
351439
<!-- Assemble the dist bundle: bin/ launchers + lib/ fat jars +
352440
plugins/ analyzer jars, zipped. Output:
353441
target/sonar-predictor-dist-${project.version}.zip. -->

src/main/assembly/dist.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,17 @@
5050
</includes>
5151
</fileSet>
5252

53+
<!-- The host plugin JAR produced by the host-plugin-jar maven-jar-plugin
54+
execution. Ships alongside the 10 analyzer JARs in plugins/ so the
55+
daemon's PluginRuntime.loadAll picks it up via the *.jar glob. -->
56+
<fileSet>
57+
<directory>${project.build.directory}</directory>
58+
<outputDirectory>plugins</outputDirectory>
59+
<includes>
60+
<include>sonar-predictor-*-host.jar</include>
61+
</includes>
62+
</fileSet>
63+
5364
<!-- The two shaded fat jars produced by maven-shade-plugin's
5465
shade-cli + shade-daemon executions. Their on-disk names are
5566
sonar-predictor-{cli,daemon}-<version>.jar; the launcher

src/main/java/io/github/randomcodespace/sonarpredict/daemon/AnalysisService.java

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import java.util.stream.Stream;
2121

2222
import org.sonarsource.sonarlint.core.analysis.AnalysisScheduler;
23-
import org.sonarsource.sonarlint.core.analysis.api.ActiveRule;
23+
import org.sonar.api.batch.rule.ActiveRule;
2424
import org.sonarsource.sonarlint.core.analysis.api.AnalysisConfiguration;
2525
import org.sonarsource.sonarlint.core.analysis.api.AnalysisResults;
2626
import org.sonarsource.sonarlint.core.analysis.api.AnalysisSchedulerConfiguration;
@@ -510,12 +510,8 @@ static List<ActiveRule> resolveActiveRules(
510510
}
511511
String languageKey = language.getSonarLanguageKey();
512512
for (String ruleKey : ruleKeys) {
513-
ActiveRule activeRule = new ActiveRule(ruleKey, languageKey);
514513
Map<String, String> params = paramDefaults.paramsFor(ruleKey);
515-
if (!params.isEmpty()) {
516-
activeRule.setParams(params);
517-
}
518-
rules.add(activeRule);
514+
rules.add(SimpleActiveRule.of(ruleKey, languageKey, params));
519515
}
520516
}
521517
return rules;
@@ -622,14 +618,10 @@ private List<ActiveRule> profileRulesFrom(String profileRef) {
622618
}
623619
// The analyzer-registered parameter defaults are the baseline; the
624620
// profile's own <parameters> override them where it sets a value.
625-
ActiveRule active = new ActiveRule(rule.ruleKey(), languageKey);
626621
Map<String, String> params =
627622
new java.util.HashMap<>(ruleParameterDefaults.paramsFor(rule.ruleKey()));
628623
params.putAll(rule.parameters());
629-
if (!params.isEmpty()) {
630-
active.setParams(Map.copyOf(params));
631-
}
632-
rules.add(active);
624+
rules.add(SimpleActiveRule.of(rule.ruleKey(), languageKey, params));
633625
}
634626
return rules;
635627
}

src/main/java/io/github/randomcodespace/sonarpredict/daemon/EngineLog.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.util.List;
44
import java.util.concurrent.CopyOnWriteArrayList;
5+
import java.util.concurrent.atomic.AtomicReference;
56

67
import org.sonarsource.sonarlint.core.commons.log.LogOutput;
78
import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger;
@@ -21,6 +22,8 @@
2122
*/
2223
public final class EngineLog implements LogOutput {
2324

25+
private static final AtomicReference<EngineLog> CURRENT = new AtomicReference<>();
26+
2427
private final List<String> messages = new CopyOnWriteArrayList<>();
2528

2629
/**
@@ -47,9 +50,21 @@ public static void install() {
4750
public static EngineLog installAndCapture() {
4851
EngineLog target = new EngineLog();
4952
SonarLintLogger.get().setTarget(target);
53+
CURRENT.set(target);
5054
return target;
5155
}
5256

57+
/**
58+
* Returns the most recently {@link #install installed} {@code EngineLog},
59+
* or {@code null} if none has been installed yet. Tests use this to assert
60+
* on engine messages emitted by code paths (such as
61+
* {@code PluginRuntime.loadAll}) that install their own {@code EngineLog}
62+
* internally without exposing the reference.
63+
*/
64+
public static EngineLog current() {
65+
return CURRENT.get();
66+
}
67+
5368
@Override
5469
public void log(String formattedMessage, Level level) {
5570
if (formattedMessage != null) {

src/main/java/io/github/randomcodespace/sonarpredict/daemon/IssueMapper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ private IssueMapper() {
4141
public static io.github.randomcodespace.sonarpredict.protocol.dto.Issue toDto(
4242
Issue engineIssue, Path baseDir, RuleCatalog catalog) {
4343
return map(
44-
engineIssue.getRuleKey(),
44+
engineIssue.getRuleKey().toString(),
4545
resolveFilePath(engineIssue.getInputFile(), baseDir),
4646
engineIssue.getTextRange(),
4747
engineIssue.getMessage(),

0 commit comments

Comments
 (0)