Skip to content

Commit 7ed07f2

Browse files
committed
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.
1 parent c0ff8fd commit 7ed07f2

4 files changed

Lines changed: 96 additions & 14 deletions

File tree

pom.xml

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@
211211
</execution>
212212
<execution>
213213
<id>host-plugin-jar</id>
214-
<phase>prepare-package</phase>
214+
<phase>process-test-classes</phase>
215215
<goals><goal>jar</goal></goals>
216216
<configuration>
217217
<classifier>host</classifier>
@@ -400,6 +400,42 @@
400400
</executions>
401401
</plugin>
402402

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+
403439
<!-- Assemble the dist bundle: bin/ launchers + lib/ fat jars +
404440
plugins/ analyzer jars, zipped. Output:
405441
target/sonar-predictor-dist-${project.version}.zip. -->

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,13 @@ public static LoadedPlugins loadFrom(Set<Path> jars) {
105105
V1_LANGUAGES,
106106
false,
107107
detectNodeVersion());
108-
PluginsLoadResult result = new PluginsLoader().load(config, Set.of("sonarpredict-host"));
108+
// Second argument is disabledPluginsForAnalysis — keys in this set are excluded
109+
// from getAnalysisPluginInstancesByKeys() and therefore never installed into any
110+
// Spring analysis container. Pass an empty set so the host plugin's extensions
111+
// (NoOpAnalysisWarnings) are visible to sensors such as HtmlSensor that autowire
112+
// AnalysisWarnings. The engine's own "additionalAllowedPlugins" list (textdeveloper,
113+
// textenterprise, etc.) is built internally by PluginsLoader and is separate.
114+
PluginsLoadResult result = new PluginsLoader().load(config, Set.of());
109115
return result.getLoadedPlugins();
110116
}
111117

src/main/java/io/github/randomcodespace/sonarpredict/hostplugin/NoOpAnalysisWarnings.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@
1212
* Spring-autowire. Engine 11.3 ships zero references to {@code AnalysisWarnings}, so the
1313
* host plugin provides this bean.
1414
*
15-
* <p>Lifespan is {@code INSTANCE} (per-analysis) — the engine instantiates a fresh
16-
* instance per analysis, which resets the dedupe set automatically.
15+
* <p>Lifespan is {@code SINGLE_ANALYSIS} — maps to {@code ContainerLifespan.ANALYSIS}
16+
* in the engine. The per-analysis {@code AnalysisContainer} calls
17+
* {@code install(ContainerLifespan.ANALYSIS)}, so this bean is registered into the
18+
* same Spring container that wires sensors such as {@code HtmlSensor} which declare
19+
* {@code AnalysisWarnings} as a constructor parameter.
1720
*/
18-
@SonarLintSide(lifespan = SonarLintSide.INSTANCE)
21+
@SonarLintSide(lifespan = SonarLintSide.SINGLE_ANALYSIS)
1922
public final class NoOpAnalysisWarnings implements AnalysisWarnings {
2023

2124
private static final Logger LOG = LoggerFactory.getLogger(NoOpAnalysisWarnings.class);

src/test/java/io/github/randomcodespace/sonarpredict/hostplugin/HostPluginIntegrationTest.java

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,15 @@
1212
import java.util.stream.Stream;
1313
import org.junit.jupiter.api.BeforeEach;
1414
import org.junit.jupiter.api.Test;
15+
import org.junit.jupiter.api.io.TempDir;
1516
import org.slf4j.LoggerFactory;
1617
import org.sonarsource.sonarlint.core.plugin.commons.LoadedPlugins;
1718

1819
class HostPluginIntegrationTest {
1920

21+
@TempDir
22+
Path tempPluginsDir;
23+
2024
private ListAppender<ILoggingEvent> appender;
2125

2226
@BeforeEach
@@ -28,29 +32,62 @@ void setUp() {
2832

2933
@Test
3034
void loadAll_includesHostPluginAndNoMissingBeanErrors() throws IOException {
31-
Path pluginsDir = findDistPluginsDir();
35+
populateTempPluginsDir(tempPluginsDir);
3236

33-
LoadedPlugins loaded = PluginRuntime.loadAll(pluginsDir);
37+
LoadedPlugins loaded = PluginRuntime.loadAll(tempPluginsDir);
3438

3539
assertThat(loaded.getAllPluginInstancesByKeys()).containsKey("sonarpredict-host");
3640
assertThat(appender.list).noneMatch(e ->
3741
e.getFormattedMessage() != null
3842
&& e.getFormattedMessage().contains("NoSuchBeanDefinitionException"));
3943
}
4044

41-
private static Path findDistPluginsDir() throws IOException {
45+
/**
46+
* Assembles a temporary plugins directory by hard-linking all JARs from the
47+
* vendored {@code plugins/} directory (populated at process-test-resources)
48+
* and the host plugin JAR produced at process-test-classes.
49+
*
50+
* <p>Hard-links are used where supported (same filesystem); Files.copy falls
51+
* back transparently when the link target is on a different device.
52+
*/
53+
private static void populateTempPluginsDir(Path tempDir) throws IOException {
54+
// Vendor analyzer JARs — present at process-test-resources phase.
55+
// Exclude the host JAR (sonar-predictor-*-host.jar) from this glob:
56+
// antrun copies it into plugins/ at process-test-classes, but findHostJar()
57+
// below adds it explicitly to avoid a duplicate / FileAlreadyExistsException.
58+
Path vendorPluginsDir = Path.of("plugins");
59+
try (Stream<Path> entries = Files.list(vendorPluginsDir)) {
60+
for (Path jar : (Iterable<Path>) entries
61+
.filter(p -> p.getFileName().toString().endsWith(".jar"))
62+
.filter(p -> !p.getFileName().toString().contains("-host.jar"))::iterator) {
63+
linkOrCopy(jar, tempDir.resolve(jar.getFileName()));
64+
}
65+
}
66+
67+
// Host plugin JAR — produced at process-test-classes phase
68+
Path hostJar = findHostJar();
69+
linkOrCopy(hostJar, tempDir.resolve(hostJar.getFileName()));
70+
}
71+
72+
private static void linkOrCopy(Path source, Path target) throws IOException {
73+
try {
74+
Files.createLink(target, source.toAbsolutePath());
75+
} catch (UnsupportedOperationException | IOException e) {
76+
Files.copy(source.toAbsolutePath(), target);
77+
}
78+
}
79+
80+
private static Path findHostJar() throws IOException {
4281
Path targetDir = Path.of("target");
43-
try (Stream<Path> dirs = Files.list(targetDir)) {
44-
Path distRoot = dirs
45-
.filter(Files::isDirectory)
82+
try (Stream<Path> entries = Files.list(targetDir)) {
83+
return entries
4684
.filter(p -> {
4785
String n = p.getFileName().toString();
48-
return n.startsWith("sonar-predictor-dist-");
86+
return n.startsWith("sonar-predictor-") && n.endsWith("-host.jar");
4987
})
5088
.findFirst()
5189
.orElseThrow(() -> new IllegalStateException(
52-
"dist exploded dir not found in target/; run `mvn package -DskipTests` first"));
53-
return distRoot.resolve("sonar-predictor").resolve("plugins");
90+
"host jar not found in target/; run `mvn package -DskipTests` first"));
5491
}
5592
}
5693
}

0 commit comments

Comments
 (0)