Skip to content

Commit a6492c5

Browse files
SLCORE-2062 Introduce connected artifacts loading strategy (#1976)
* PR review * SLCORE-2062 Introduce connected artifacts loading strategy Add ServerPluginSource, ServerPluginDownloader, ServerPluginsCache and ConnectedArtifactsLoadingStrategy (with its factory) to handle plugin resolution in connected mode via the new ArtifactSource architecture. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * SLCORE-2062 Move ServerPluginDownloader and ServerPluginsCache to plugin/source/server Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * SLCORE-2062 Fix test compilation of connected branch - Add ServerPluginsCache import to ConnectedModeArtifactResolverTest (class moved from plugin to plugin.source.server) - Add ServerPluginDownloader import to ConnectedModeCompanionPluginResolverTest (class moved from plugin.resolvers to plugin.source.server) - Restore refreshAndGet method in ServerPluginsCache (was missing from the move) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * PR review * PR review * Fix logic around text enterprise plugin --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent a33627c commit a6492c5

15 files changed

Lines changed: 1016 additions & 58 deletions

File tree

backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/api/SonarLanguage.java

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,8 @@
2222
import java.util.ArrayList;
2323
import java.util.Collections;
2424
import java.util.HashMap;
25-
import java.util.LinkedHashSet;
2625
import java.util.Map;
2726
import java.util.Optional;
28-
import java.util.Set;
2927
import java.util.stream.Collectors;
3028
import java.util.stream.Stream;
3129
import org.sonarsource.sonarlint.core.commons.plugins.SonarPlugin;
@@ -125,10 +123,6 @@ public boolean shouldSyncInConnectedMode() {
125123
return !equals(SonarLanguage.IPYTHON);
126124
}
127125

128-
public static Set<SonarLanguage> getLanguagesByPluginKey(String pluginKey) {
129-
return Stream.of(values()).filter(l -> l.getPlugin().getKey().equals(pluginKey)).collect(Collectors.toCollection(LinkedHashSet::new));
130-
}
131-
132126
public static Optional<SonarLanguage> getLanguageByLanguageKey(String languageKey) {
133127
var languages = Stream.of(values()).filter(l -> l.getSonarLanguageKey().equals(languageKey)).collect(Collectors.toCollection(ArrayList::new));
134128
return languages.isEmpty() ? Optional.empty() : Optional.of(languages.get(0));

backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/plugins/SonarPlugin.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ public enum SonarPlugin {
5151
RUBY("ruby"),
5252
SCALA("sonarscala"),
5353
SWIFT("swift"),
54-
TEXT("text", new EnterpriseReplacement(true, Version.create("10.4"))),
54+
TEXT_ENTERPRISE("textenterprise"),
55+
TEXT("text", TEXT_ENTERPRISE),
5556
TSQL("tsql"),
5657
// VBNET_ENTERPRISE must be declared before VBNET_OSS
5758
VBNET_ENTERPRISE("vbnetenterprise"),
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
* SonarLint Core - Implementation
3+
* Copyright (C) SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonarsource.sonarlint.core.plugin.loading.strategy;
21+
22+
import java.util.ArrayList;
23+
import java.util.LinkedHashMap;
24+
import java.util.List;
25+
import org.sonarsource.sonarlint.core.commons.plugins.SonarPlugin;
26+
import org.sonarsource.sonarlint.core.languages.LanguageSupportRepository;
27+
import org.sonarsource.sonarlint.core.plugin.source.ArtifactSource;
28+
import org.sonarsource.sonarlint.core.plugin.source.AvailableArtifact;
29+
import org.sonarsource.sonarlint.core.plugin.source.ResolvedArtifact;
30+
import org.sonarsource.sonarlint.core.plugin.source.binaries.BinariesArtifactSource;
31+
import org.sonarsource.sonarlint.core.plugin.source.embedded.EmbeddedPluginSource;
32+
import org.sonarsource.sonarlint.core.plugin.source.server.ServerPluginSource;
33+
import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams;
34+
35+
/**
36+
* Artifacts loading strategy for connected mode (a specific connection).
37+
*
38+
* <p>One instance is created per connection and cached by
39+
* {@link ConnectedArtifactsLoadingStrategyFactory}.</p>
40+
*
41+
* <p>Sources, in ascending priority order:
42+
* <ol>
43+
* <li>{@link BinariesArtifactSource} — on-demand downloadable artifacts (fallback).</li>
44+
* <li>{@link ServerPluginSource} — artifacts synced from the server.</li>
45+
* <li>{@link EmbeddedPluginSource} (connected) — JARs embedded in the IDE client (highest
46+
* priority in normal circumstances).</li>
47+
* </ol>
48+
*
49+
* <p>{@link #resolveArtifacts()} uses a winner-map pattern: iterate sources in ascending
50+
* priority, last writer wins per key, then call {@code load()} exactly once per key from the
51+
* winning source.
52+
*
53+
* <p>Two post-processing passes correct the winner map before loading:
54+
* <ol>
55+
* <li><b>Enterprise-variant deduplication</b>: when a different-key enterprise variant
56+
* ({@code csharpenterprise}, {@code vbnetenterprise}) is present, the base key is removed
57+
* so both are not loaded simultaneously.</li>
58+
* <li><b>Enterprise priority override</b>: when the server reports a plugin as enterprise
59+
* ({@link AvailableArtifact#isEnterprise()}), that plugin is forced to use the server
60+
* source even if the embedded source would normally win. This applies to same-key
61+
* enterprise plugins (GO, IAC) whose enterprise edition is served when the
62+
* connection qualifies (SonarQube Server &ge; minimum version, or SonarQube Cloud).</li>
63+
* </ol>
64+
*/
65+
public class ConnectedArtifactsLoadingStrategy implements ArtifactsLoadingStrategy {
66+
private final ServerPluginSource serverSource;
67+
private final LanguageSupportRepository languageSupportRepository;
68+
private final List<ArtifactSource> artifactSourcesSortedByAscendingPriority;
69+
70+
ConnectedArtifactsLoadingStrategy(InitializeParams params, BinariesArtifactSource binariesSource,
71+
ServerPluginSource serverSource, LanguageSupportRepository languageSupportRepository) {
72+
this.serverSource = serverSource;
73+
this.languageSupportRepository = languageSupportRepository;
74+
// Ascending priority: binaries (fallback) → server → embedded (highest)
75+
this.artifactSourcesSortedByAscendingPriority = List.of(
76+
binariesSource,
77+
serverSource,
78+
EmbeddedPluginSource.forConnected(params));
79+
}
80+
81+
/**
82+
* Resolves all artifacts from all sources using a winner-map pattern. May schedule background
83+
* downloads.
84+
*
85+
* <p>Priority (highest wins in normal cases): embedded &gt; server &gt; binaries.
86+
* Exception: enterprise server plugins beat embedded (see class Javadoc).</p>
87+
*/
88+
@Override
89+
public ArtifactsLoadingResult resolveArtifacts() {
90+
var enabledLanguages = languageSupportRepository.getEnabledLanguagesInConnectedMode();
91+
92+
// Query server artifacts once; reused in normal pass and enterprise-override pass
93+
var serverArtifacts = serverSource.listAvailableArtifacts(enabledLanguages);
94+
95+
// Normal winner-map: ascending priority, last writer wins
96+
var bestSourceByArtifactKey = new LinkedHashMap<String, ArtifactSource>();
97+
for (var source : artifactSourcesSortedByAscendingPriority) {
98+
var artifacts = (source == serverSource) ? serverArtifacts : source.listAvailableArtifacts(enabledLanguages);
99+
for (var artifact : artifacts) {
100+
bestSourceByArtifactKey.put(artifact.key(), source);
101+
}
102+
}
103+
104+
// Pass 1: remove base keys superseded by a different-key enterprise variant
105+
new ArrayList<>(bestSourceByArtifactKey.keySet()).stream()
106+
.filter(SonarPlugin::isEnterpriseVariant)
107+
.forEach(entKey -> SonarPlugin.baseKeyFor(entKey).ifPresent(bestSourceByArtifactKey::remove));
108+
109+
// Pass 2: enterprise server plugins override even embedded
110+
serverArtifacts.stream()
111+
.filter(AvailableArtifact::isEnterprise)
112+
.forEach(a -> bestSourceByArtifactKey.put(a.key(), serverSource));
113+
114+
var result = new LinkedHashMap<String, ResolvedArtifact>();
115+
bestSourceByArtifactKey.forEach((artifactKey, source) -> source.load(artifactKey).ifPresent(artifact -> result.put(artifactKey, artifact)));
116+
return new ArtifactsLoadingResult(enabledLanguages, result);
117+
}
118+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* SonarLint Core - Implementation
3+
* Copyright (C) SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonarsource.sonarlint.core.plugin.loading.strategy;
21+
22+
import java.util.concurrent.ConcurrentHashMap;
23+
import org.sonarsource.sonarlint.core.languages.LanguageSupportRepository;
24+
import org.sonarsource.sonarlint.core.plugin.source.server.ServerPluginsCache;
25+
import org.sonarsource.sonarlint.core.plugin.source.server.ServerPluginSource;
26+
import org.sonarsource.sonarlint.core.plugin.source.server.ServerPluginDownloader;
27+
import org.sonarsource.sonarlint.core.plugin.source.binaries.BinariesArtifactSource;
28+
import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams;
29+
import org.sonarsource.sonarlint.core.storage.StorageService;
30+
31+
/**
32+
* Creates and caches {@link ConnectedArtifactsLoadingStrategy} instances, one per connection ID.
33+
*
34+
* <p>The cache ensures that the same strategy — and its underlying
35+
* {@link ServerPluginSource} — is reused across calls for the same connection, which is required
36+
* for consistent in-progress download tracking. Call {@link #evict(String)} when a connection is
37+
* removed.</p>
38+
*/
39+
public class ConnectedArtifactsLoadingStrategyFactory {
40+
41+
private final ConcurrentHashMap<String, ConnectedArtifactsLoadingStrategy> cache = new ConcurrentHashMap<>();
42+
43+
private final InitializeParams params;
44+
private final BinariesArtifactSource binariesSource;
45+
private final StorageService storageService;
46+
private final ServerPluginsCache serverPluginsCache;
47+
private final ServerPluginDownloader downloader;
48+
private final LanguageSupportRepository languageSupportRepository;
49+
50+
public ConnectedArtifactsLoadingStrategyFactory(InitializeParams params,
51+
BinariesArtifactSource binariesSource,
52+
StorageService storageService,
53+
ServerPluginsCache serverPluginsCache,
54+
ServerPluginDownloader downloader,
55+
LanguageSupportRepository languageSupportRepository) {
56+
this.params = params;
57+
this.binariesSource = binariesSource;
58+
this.storageService = storageService;
59+
this.serverPluginsCache = serverPluginsCache;
60+
this.downloader = downloader;
61+
this.languageSupportRepository = languageSupportRepository;
62+
}
63+
64+
public ConnectedArtifactsLoadingStrategy getOrCreate(String connectionId) {
65+
return cache.computeIfAbsent(connectionId, id -> {
66+
var serverSource = new ServerPluginSource(id, storageService, serverPluginsCache, downloader);
67+
return new ConnectedArtifactsLoadingStrategy(params, binariesSource, serverSource, languageSupportRepository);
68+
});
69+
}
70+
71+
public void evict(String connectionId) {
72+
cache.remove(connectionId);
73+
}
74+
}

backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/resolvers/ConnectedModeArtifactResolver.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,11 @@
2929
import javax.annotation.Nullable;
3030
import org.sonarsource.sonarlint.core.commons.api.SonarLanguage;
3131
import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger;
32-
import org.sonarsource.sonarlint.core.plugin.source.ArtifactState;
3332
import org.sonarsource.sonarlint.core.plugin.PluginJarUtils;
3433
import org.sonarsource.sonarlint.core.plugin.ResolvedArtifact;
35-
import org.sonarsource.sonarlint.core.plugin.ServerPluginsCache;
34+
import org.sonarsource.sonarlint.core.plugin.source.ArtifactState;
35+
import org.sonarsource.sonarlint.core.plugin.source.server.ServerPluginDownloader;
36+
import org.sonarsource.sonarlint.core.plugin.source.server.ServerPluginsCache;
3637
import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams;
3738
import org.sonarsource.sonarlint.core.serverapi.plugins.ServerPlugin;
3839
import org.sonarsource.sonarlint.core.serverconnection.StoredPlugin;
@@ -117,7 +118,7 @@ public Optional<ResolvedArtifact> resolve(SonarLanguage language, @Nullable Stri
117118
try {
118119
return serverPluginsCache.getPlugins(connectionId)
119120
.flatMap(plugins -> findServerPlugin(plugins, primaryKey, fallbackKey))
120-
.map(serverPlugin -> resolveFromStorageOrSchedule(connectionId, serverPlugin, storedPlugins, language))
121+
.map(serverPlugin -> resolveFromStorageOrSchedule(connectionId, serverPlugin, storedPlugins))
121122
.or(() -> resolveFromStorageWithFallback(connectionId, primaryKey, fallbackKey, storedPlugins));
122123
} catch (Exception e) {
123124
LOG.debug(PLUGIN_FETCH_ERROR, connectionId);
@@ -149,13 +150,13 @@ private static Optional<ServerPlugin> findServerPlugin(List<ServerPlugin> plugin
149150
}
150151

151152
private ResolvedArtifact resolveFromStorageOrSchedule(String connectionId, ServerPlugin serverPlugin,
152-
Map<String, StoredPlugin> storedPlugins, SonarLanguage language) {
153+
Map<String, StoredPlugin> storedPlugins) {
153154
var fromStorage = resolveFromStorage(connectionId, serverPlugin, storedPlugins);
154155
if (fromStorage.isPresent()) {
155156
LOG.debug("[SYNC] Code analyzer '{}' is up-to-date. Skip downloading it.", serverPlugin.getKey());
156157
return fromStorage.get();
157158
}
158-
downloader.scheduleLanguagePluginDownload(connectionId, serverPlugin, language);
159+
downloader.schedulePluginDownload(connectionId, serverPlugin);
159160
return new ResolvedArtifact(ArtifactState.DOWNLOADING, null, null, null);
160161
}
161162

backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/resolvers/ConnectedModeCompanionPluginResolver.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,10 @@
2828
import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger;
2929
import org.sonarsource.sonarlint.core.commons.plugins.SonarPlugin;
3030
import org.sonarsource.sonarlint.core.languages.LanguageSupportRepository;
31-
import org.sonarsource.sonarlint.core.plugin.source.ArtifactState;
3231
import org.sonarsource.sonarlint.core.plugin.PluginStatus;
33-
import org.sonarsource.sonarlint.core.plugin.ServerPluginsCache;
32+
import org.sonarsource.sonarlint.core.plugin.source.ArtifactState;
33+
import org.sonarsource.sonarlint.core.plugin.source.server.ServerPluginDownloader;
34+
import org.sonarsource.sonarlint.core.plugin.source.server.ServerPluginsCache;
3435
import org.sonarsource.sonarlint.core.serverapi.plugins.ServerPlugin;
3536
import org.sonarsource.sonarlint.core.serverconnection.StoredPlugin;
3637
import org.sonarsource.sonarlint.core.storage.StorageService;
@@ -118,7 +119,7 @@ private void processCompanionPlugin(String connectionId, ServerPlugin plugin, Co
118119
LOG.debug("[SYNC] Code analyzer '{}' does not support SonarLint. Skip downloading it.", plugin.getKey());
119120
return;
120121
}
121-
downloader.scheduleCompanionPluginDownload(connectionId, plugin);
122+
downloader.schedulePluginDownload(connectionId, plugin);
122123
result.put(plugin.getKey(), PluginStatus.forCompanion(plugin.getKey(), ArtifactState.DOWNLOADING, null, null, null));
123124
}
124125

0 commit comments

Comments
 (0)