Skip to content

Commit 14a250f

Browse files
SLCORE-1619 Trigger a new analysis when binding changes
1 parent e19d6ea commit 14a250f

File tree

5 files changed

+101
-45
lines changed

5 files changed

+101
-45
lines changed

backend/core/src/main/java/org/sonarsource/sonarlint/core/analysis/AnalysisService.java

Lines changed: 24 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -256,8 +256,7 @@ private AnalysisConfiguration getAnalysisConfigForEngine(String configScopeId, S
256256
var analysisProperties = analysisConfig.getAnalysisProperties();
257257
var inferredAnalysisProperties = startChild(trace, "getInferredAnalysisProperties", ANALYSIS_CFG_FOR_ENGINE,
258258
() -> client.getInferredAnalysisProperties(new GetInferredAnalysisPropertiesParams(
259-
configScopeId, filesToAnalyze.stream().map(ClientFile::getUri).toList()
260-
)).join().getProperties());
259+
configScopeId, filesToAnalyze.stream().map(ClientFile::getUri).toList())).join().getProperties());
261260
analysisProperties.putAll(inferredAnalysisProperties);
262261
var activeRules = analysisConfig.getActiveRules().stream().map(r -> {
263262
var ar = new ActiveRule(r.getRuleKey(), r.getLanguageKey());
@@ -266,8 +265,7 @@ private AnalysisConfiguration getAnalysisConfigForEngine(String configScopeId, S
266265
return ar;
267266
}).toList();
268267
trace.setData("activeRulesCount", activeRules.size());
269-
return startChild(trace, "buildAnalysisConfiguration", ANALYSIS_CFG_FOR_ENGINE, () ->
270-
AnalysisConfiguration.builder()
268+
return startChild(trace, "buildAnalysisConfiguration", ANALYSIS_CFG_FOR_ENGINE, () -> AnalysisConfiguration.builder()
271269
.addInputFiles(filesToAnalyze.stream().map(BackendInputFile::new).toList())
272270
.putAllExtraProperties(analysisProperties)
273271
// properties sent by client using new API were merged above
@@ -515,16 +513,13 @@ public void onPluginsSynchronized(PluginsSynchronizedEvent event) {
515513
var connectionId = event.connectionId();
516514
checkIfReadyForAnalysis(configurationRepository.getBoundScopesToConnection(connectionId)
517515
.stream().map(BoundScope::getConfigScopeId).collect(Collectors.toSet()));
518-
configurationRepository.getBoundScopesToConnection(connectionId)
519-
.stream().map(BoundScope::getConfigScopeId).forEach(this::autoAnalyzeOpenFiles);
520516
}
521517

522518
@EventListener
523519
public void onConfigurationScopeAdded(ConfigurationScopesAddedWithBindingEvent event) {
524520
var configScopeIds = event.getConfigScopeIds();
525521
configScopeIds.forEach(schedulerCache::registerModuleIfLeafConfigScope);
526522
checkIfReadyForAnalysis(configScopeIds);
527-
configScopeIds.forEach(this::autoAnalyzeOpenFiles);
528523
}
529524

530525
@EventListener
@@ -537,22 +532,17 @@ public void onConfigurationScopeRemoved(ConfigurationScopeRemovedEvent event) {
537532
@EventListener
538533
public void onBindingConfigurationChanged(BindingConfigChangedEvent event) {
539534
var configScopeId = event.configScopeId();
540-
analysisReadinessByConfigScopeId.remove(configScopeId);
541-
if (!checkIfReadyForAnalysis(configScopeId)) {
542-
client.didChangeAnalysisReadiness(new DidChangeAnalysisReadinessParams(Set.of(configScopeId), false));
543-
}
535+
checkIfReadyForAnalysis(Set.of(configScopeId));
544536
}
545537

546538
@EventListener
547539
public void onAnalyzerConfigurationSynchronized(AnalyzerConfigurationSynchronized event) {
548540
checkIfReadyForAnalysis(event.getConfigScopeIds());
549-
event.getConfigScopeIds().forEach(this::autoAnalyzeOpenFiles);
550541
}
551542

552543
@EventListener
553544
public void onConfigurationScopesSynchronized(ConfigurationScopesSynchronizedEvent event) {
554545
checkIfReadyForAnalysis(event.getConfigScopeIds());
555-
event.getConfigScopeIds().forEach(this::autoAnalyzeOpenFiles);
556546
}
557547

558548
@EventListener
@@ -651,38 +641,34 @@ private void streamIssue(String configScopeId, UUID analysisId, ConcurrentHashMa
651641
}
652642

653643
private void checkIfReadyForAnalysis(Set<String> configurationScopeIds) {
654-
var readyConfigScopeIds = configurationScopeIds.stream()
655-
.filter(this::isReadyForAnalysis)
656-
.collect(toSet());
657-
saveAndNotifyReadyForAnalysis(readyConfigScopeIds);
658-
}
659-
660-
private boolean checkIfReadyForAnalysis(String configurationScopeId) {
661-
if (isReadyForAnalysis(configurationScopeId)) {
662-
saveAndNotifyReadyForAnalysis(Set.of(configurationScopeId));
663-
return true;
664-
}
665-
return false;
666-
}
667-
668-
private void saveAndNotifyReadyForAnalysis(Set<String> configScopeIds) {
669-
var scopeIdsThatBecameReady = new HashSet<String>();
670-
configScopeIds.forEach(configScopeId -> {
671-
if (analysisReadinessByConfigScopeId.get(configScopeId) != Boolean.TRUE) {
672-
analysisReadinessByConfigScopeId.put(configScopeId, Boolean.TRUE);
673-
scopeIdsThatBecameReady.add(configScopeId);
644+
var readyConfigScopeIds = new HashSet<String>();
645+
var scopeThatBecameReady = new HashSet<String>();
646+
var scopeThatBecameNotReady = new HashSet<String>();
647+
configurationScopeIds.forEach(configScopeId -> {
648+
var readyForAnalysis = isReadyForAnalysis(configScopeId);
649+
var wasReady = analysisReadinessByConfigScopeId.put(configScopeId, readyForAnalysis);
650+
if (readyForAnalysis && !Boolean.TRUE.equals(wasReady)) {
651+
scopeThatBecameReady.add(configScopeId);
652+
} else if (!readyForAnalysis && Boolean.TRUE.equals(wasReady)) {
653+
scopeThatBecameNotReady.add(configScopeId);
654+
}
655+
if (readyForAnalysis) {
656+
readyConfigScopeIds.add(configScopeId);
674657
}
675658
});
676-
if (!scopeIdsThatBecameReady.isEmpty()) {
677-
reanalyseOpenFiles(scopeIdsThatBecameReady::contains);
678-
scopeIdsThatBecameReady.forEach(scopeId -> {
659+
if (!scopeThatBecameReady.isEmpty()) {
660+
scopeThatBecameReady.forEach(scopeId -> {
679661
var scheduler = schedulerCache.getOrCreateAnalysisScheduler(scopeId);
680662
if (scheduler != null) {
681663
scheduler.wakeUp();
682664
}
683665
});
684-
client.didChangeAnalysisReadiness(new DidChangeAnalysisReadinessParams(scopeIdsThatBecameReady, true));
666+
client.didChangeAnalysisReadiness(new DidChangeAnalysisReadinessParams(scopeThatBecameReady, true));
667+
}
668+
if (!scopeThatBecameNotReady.isEmpty()) {
669+
client.didChangeAnalysisReadiness(new DidChangeAnalysisReadinessParams(scopeThatBecameNotReady, false));
685670
}
671+
reanalyseOpenFiles(readyConfigScopeIds::contains);
686672
}
687673

688674
private boolean isReadyForAnalysis(String configScopeId) {
@@ -797,8 +783,7 @@ private void analysisStarted(String configurationScopeId, UUID analysisId, List<
797783

798784
private CompletableFuture<AnalysisResult> schedule(String configScopeId, AnalyzeCommand command, UUID analysisId, ArrayList<RawIssue> rawIssues,
799785
boolean shouldFetchServerIssues, @Nullable Trace trace) {
800-
var scheduler = startChild(trace, "getOrCreateAnalysisScheduler", "schedule", () ->
801-
schedulerCache.getOrCreateAnalysisScheduler(configScopeId, command.getTrace()));
786+
var scheduler = startChild(trace, "getOrCreateAnalysisScheduler", "schedule", () -> schedulerCache.getOrCreateAnalysisScheduler(configScopeId, command.getTrace()));
802787
startChild(trace, "post", "schedule", () -> scheduler.post(command));
803788
var result = command.getFutureResult();
804789
result.exceptionally(exception -> {

its/tests/src/test/java/its/FileExclusionTests.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,10 +213,8 @@ void should_respect_exclusion_settings_on_SQ() {
213213

214214
private static void forceBackendToPullSettings(String configScopeId, String projectKey) {
215215
// The only way to force a sync of the storage is to unbind/rebind
216-
analysisReadinessByConfigScopeId.clear();
217216
backend.getConfigurationService().didUpdateBinding(new DidUpdateBindingParams(configScopeId, new BindingConfigurationDto(null, null, true)));
218217
backend.getConfigurationService().didUpdateBinding(new DidUpdateBindingParams(configScopeId, new BindingConfigurationDto(CONNECTION_ID, projectKey, true)));
219-
await().atMost(1, MINUTES).untilAsserted(() -> assertThat(analysisReadinessByConfigScopeId).containsEntry(configScopeId, true));
220218
}
221219

222220
private static SonarLintRpcClientDelegate newDummySonarLintClient() {

its/tests/src/test/java/its/SonarQubeDeveloperEditionTests.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -555,21 +555,17 @@ void shouldHonorServerSideSettings() {
555555

556556
rpcClientLogs.clear();
557557
didSynchronizeConfigurationScopes.clear();
558-
analysisReadinessByConfigScopeId.clear();
559558
// Override default file suffixes in global props so that input file is not considered as a Java file
560559
setSettingsMultiValue(null, "sonar.java.file.suffixes", ".foo");
561560
backend.getConfigurationService().didUpdateBinding(new DidUpdateBindingParams(configScopeId, new BindingConfigurationDto(CONNECTION_ID, projectKey, false)));
562-
await().untilAsserted(() -> assertThat(analysisReadinessByConfigScopeId).containsEntry(configScopeId, true));
563561
await().untilAsserted(() -> assertThat(rpcClientLogs.stream().anyMatch(s -> s.getMessage().equals("Stored project analyzer configuration"))).isTrue());
564562

565563
analyzeFileAndVerifyNoIssues(configScopeId, "sample-java", "src/main/java/foo/Foo.java");
566564

567565
rpcClientLogs.clear();
568-
analysisReadinessByConfigScopeId.clear();
569566
// Override default file suffixes in project props so that input file is considered as a Java file again
570567
setSettingsMultiValue(projectKey, "sonar.java.file.suffixes", ".java");
571568
backend.getConfigurationService().didUpdateBinding(new DidUpdateBindingParams(configScopeId, new BindingConfigurationDto(CONNECTION_ID, projectKey, true)));
572-
await().untilAsserted(() -> assertThat(analysisReadinessByConfigScopeId).containsEntry(configScopeId, true));
573569
await().untilAsserted(() -> assertThat(rpcClientLogs.stream().anyMatch(s -> s.getMessage().equals("Stored project analyzer configuration"))).isTrue());
574570

575571
rawIssues = analyzeFile(configScopeId, "sample-java", "src/main/java/foo/Foo.java");

medium-tests/src/test/java/mediumtest/analysis/AnalysisReadinessMediumTests.java

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,13 @@
2929
import org.junit.jupiter.api.extension.ExtendWith;
3030
import org.junit.jupiter.api.io.TempDir;
3131
import org.sonarsource.sonarlint.core.commons.LogTestStartAndEnd;
32+
import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingConfigurationDto;
33+
import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingMode;
34+
import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.DidUpdateBindingParams;
3235
import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.scope.ConfigurationScopeDto;
3336
import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.scope.DidAddConfigurationScopesParams;
3437
import org.sonarsource.sonarlint.core.rpc.protocol.backend.file.DidOpenFileParams;
38+
import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaisedFindingDto;
3539
import org.sonarsource.sonarlint.core.rpc.protocol.common.ClientFileDto;
3640
import org.sonarsource.sonarlint.core.rpc.protocol.common.IssueSeverity;
3741
import org.sonarsource.sonarlint.core.rpc.protocol.common.Language;
@@ -43,6 +47,7 @@
4347
import static org.awaitility.Awaitility.await;
4448
import static org.mockito.ArgumentMatchers.any;
4549
import static org.mockito.ArgumentMatchers.eq;
50+
import static org.mockito.Mockito.clearInvocations;
4651
import static org.mockito.Mockito.never;
4752
import static org.mockito.Mockito.timeout;
4853
import static org.mockito.Mockito.verify;
@@ -110,6 +115,74 @@ void it_should_change_readiness_and_analyze_xml_file_in_connected_mode(SonarLint
110115
assertThat(publishedIssues).containsOnlyKeys(fileUri);
111116
}
112117

118+
@SonarLintTest
119+
void it_should_reanalyse_open_files_after_unbinding(SonarLintTestHarness harness, @TempDir Path baseDir) {
120+
var filePath = createFile(baseDir, "pom.xml",
121+
"""
122+
<?xml version="1.0" encoding="UTF-8"?>
123+
<project>
124+
<modelVersion>4.0.0</modelVersion>
125+
<groupId>com.foo</groupId>
126+
<artifactId>bar</artifactId>
127+
<version>${pom.version}</version>
128+
</project>""");
129+
var fileUri = filePath.toUri();
130+
var server = harness.newFakeSonarQubeServer()
131+
.withPlugin(TestPlugin.XML)
132+
.withProject("projectKey", project -> project.withQualityProfile("qp"))
133+
.withProject("projectKey2", project -> project.withQualityProfile("qp2"))
134+
.withQualityProfile("qp", qualityProfile -> qualityProfile.withLanguage("xml").withActiveRule("xml:S3421", activeRule -> activeRule.withSeverity(IssueSeverity.MAJOR)))
135+
.withQualityProfile("qp2")
136+
.start();
137+
var client = harness.newFakeClient()
138+
.withInitialFs(CONFIG_SCOPE_ID, baseDir, List.of(new ClientFileDto(fileUri, baseDir.relativize(filePath), CONFIG_SCOPE_ID, false, null, filePath, null, null, true)))
139+
.build();
140+
var backend = harness.newBackend()
141+
.withSonarQubeConnection("connectionId", server.baseUrl())
142+
.withBoundConfigScope(CONFIG_SCOPE_ID, "connectionId", "projectKey")
143+
.withBackendCapability(FULL_SYNCHRONIZATION)
144+
.withExtraEnabledLanguagesInConnectedMode(Language.XML)
145+
.start(client);
146+
147+
verify(client, never()).didChangeAnalysisReadiness(Set.of(CONFIG_SCOPE_ID), true);
148+
149+
// File opened but not analyzed since analysis is not ready yet
150+
backend.getFileService().didOpenFile(new DidOpenFileParams(CONFIG_SCOPE_ID, fileUri));
151+
verify(client, never()).raiseIssues(eq(CONFIG_SCOPE_ID), any(), eq(false), any());
152+
153+
client.waitForSynchronization();
154+
155+
// analysis is ready
156+
await().atMost(1, TimeUnit.SECONDS)
157+
.untilAsserted(() -> verify(client).didChangeAnalysisReadiness(Set.of(CONFIG_SCOPE_ID), true));
158+
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> assertThat(client.getRaisedIssuesForScopeIdAsList(CONFIG_SCOPE_ID)).isNotEmpty());
159+
160+
var publishedIssues = getPublishedIssues(client, CONFIG_SCOPE_ID);
161+
assertThat(publishedIssues).containsOnlyKeys(fileUri);
162+
clearInvocations(client);
163+
164+
// bind to 2nd project
165+
backend.getConfigurationService()
166+
.didUpdateBinding(new DidUpdateBindingParams(CONFIG_SCOPE_ID, new BindingConfigurationDto("connectionId", "projectKey2", true), BindingMode.MANUAL, null));
167+
168+
// analysis becomes not ready after binding change
169+
verify(client, timeout(200)).didChangeAnalysisReadiness(Set.of(CONFIG_SCOPE_ID), false);
170+
171+
client.waitForSynchronization();
172+
173+
// analysis is ready, no issues are raised by the second QP
174+
await().atMost(1, TimeUnit.SECONDS)
175+
.untilAsserted(() -> verify(client).didChangeAnalysisReadiness(Set.of(CONFIG_SCOPE_ID), true));
176+
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> assertThat(client.getRaisedIssuesForScopeIdAsList(CONFIG_SCOPE_ID)).isEmpty());
177+
178+
// bind again to 1st project that should be ready
179+
backend.getConfigurationService()
180+
.didUpdateBinding(new DidUpdateBindingParams(CONFIG_SCOPE_ID, new BindingConfigurationDto("connectionId", "projectKey", true), BindingMode.MANUAL, null));
181+
182+
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> assertThat(client.getRaisedIssuesForScopeIdAsList(CONFIG_SCOPE_ID))
183+
.extracting(RaisedFindingDto::getRuleKey).containsExactly("xml:S3421"));
184+
}
185+
113186
private static Path createFile(Path folderPath, String fileName, String content) {
114187
var filePath = folderPath.resolve(fileName);
115188
try {

test-utils/src/main/java/org/sonarsource/sonarlint/core/test/utils/server/ServerFixture.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,6 +709,10 @@ public SonarQubeServerBuilder withProject(String projectKey, UnaryOperator<Serve
709709
return this;
710710
}
711711

712+
public SonarQubeServerBuilder withQualityProfile(String qualityProfileKey) {
713+
return withQualityProfile(qualityProfileKey, UnaryOperator.identity());
714+
}
715+
712716
public SonarQubeServerBuilder withQualityProfile(String qualityProfileKey, UnaryOperator<ServerQualityProfileBuilder> qualityProfileBuilder) {
713717
var builder = new ServerQualityProfileBuilder(null);
714718
this.qualityProfilesByKey.put(qualityProfileKey, qualityProfileBuilder.apply(builder));

0 commit comments

Comments
 (0)