Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* SonarQube Ide VisualStudio Roslyn Plugin
* Copyright (C) 2025-2025 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonarsource.sonarlint.visualstudio.roslyn;

import org.sonar.api.Startable;
import org.sonarsource.api.sonarlint.SonarLintSide;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

@SonarLintSide(lifespan = SonarLintSide.INSTANCE)
public class AnalysisCancellationService implements Startable {
private final ScheduledExecutorService scheduledExecutorService;

public AnalysisCancellationService() {
scheduledExecutorService = Executors.newScheduledThreadPool(5);
}

public void registerAnalysis(AnalysisTracker analysisTracker) {
AnalysisPollingRunnable task = new AnalysisPollingRunnable(analysisTracker);
task.selfFuture = scheduledExecutorService.scheduleAtFixedRate(task, 100, 100, TimeUnit.MILLISECONDS);
}

@Override
public void start() {
// do nothing, executor created in constructor
}

@Override
public void stop() {
scheduledExecutorService.shutdownNow();
}

private static class AnalysisPollingRunnable implements Runnable{
private final AnalysisTracker analysisCompletion;
ScheduledFuture<?> selfFuture;

public AnalysisPollingRunnable(AnalysisTracker analysisCompletion) {
this.analysisCompletion = analysisCompletion;
}

@Override
public void run() {
if (analysisCompletion.cancelIfNeeded()) {
selfFuture.cancel(false);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* SonarQube Ide VisualStudio Roslyn Plugin
* Copyright (C) 2025-2025 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonarsource.sonarlint.visualstudio.roslyn;

import java.io.Closeable;
import java.util.UUID;

public interface AnalysisTracker extends Closeable {
UUID getAnalysisId();

boolean cancelIfNeeded();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* SonarQube Ide VisualStudio Roslyn Plugin
* Copyright (C) 2025-2025 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonarsource.sonarlint.visualstudio.roslyn;

import org.sonar.api.batch.sensor.SensorContext;
import org.sonarsource.sonarlint.visualstudio.roslyn.http.HttpAnalysisRequestHandler;

import java.util.UUID;

public class AnalysisTrackerImpl implements AnalysisTracker {
private final UUID analysisId;
private boolean isCompleted;
private SensorContext sensorContext;
private final HttpAnalysisRequestHandler handler;

public AnalysisTrackerImpl(SensorContext sensorContext, HttpAnalysisRequestHandler handler, AnalysisCancellationService analysisCancellationService) {
this.sensorContext = sensorContext;
this.handler = handler;
this.analysisId = UUID.randomUUID();
analysisCancellationService.registerAnalysis(this);
}

@Override
public UUID getAnalysisId() {
return analysisId;
}

@Override
public synchronized boolean cancelIfNeeded() {
if (isCompleted) {
return true;
}

if (sensorContext.isCancelled()) {
handler.cancelAnalysis(analysisId);
setCompletedState();
return true;
}

return false;
}

@Override
public synchronized void close() {
setCompletedState();
}

private synchronized void setCompletedState() {
isCompleted = true;
sensorContext = null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* SonarQube Ide VisualStudio Roslyn Plugin
* Copyright (C) 2025-2025 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonarsource.sonarlint.visualstudio.roslyn;

import org.sonar.api.batch.rule.ActiveRule;
import org.sonar.api.batch.sensor.SensorContext;
import org.sonarsource.api.sonarlint.SonarLintSide;
import org.sonarsource.sonarlint.visualstudio.roslyn.http.AnalyzerInfoDto;
import org.sonarsource.sonarlint.visualstudio.roslyn.http.HttpAnalysisRequestHandler;
import org.sonarsource.sonarlint.visualstudio.roslyn.protocol.RoslynIssue;

import java.util.Collection;
import java.util.Map;

@SonarLintSide
public class RemoteAnalysisService {

private final AnalysisCancellationService analysisCancellationService;
private final HttpAnalysisRequestHandler httpAnalysisRequestHandler;
private final SensorContext sensorContext;

public RemoteAnalysisService(
AnalysisCancellationService analysisCancellationService,
HttpAnalysisRequestHandler httpAnalysisRequestHandler,
SensorContext sensorContext) {
this.analysisCancellationService = analysisCancellationService;
this.httpAnalysisRequestHandler = httpAnalysisRequestHandler;
this.sensorContext = sensorContext;
}

public Collection<RoslynIssue> analyze(
Collection<String> inputFiles,
Collection<ActiveRule> activeRules,
Map<String, String> analysisProperties,
AnalyzerInfoDto analyzerInfo) {
try (var tracker = new AnalysisTrackerImpl(sensorContext, httpAnalysisRequestHandler, analysisCancellationService)) {
return httpAnalysisRequestHandler.analyze(inputFiles, activeRules, analysisProperties, analyzerInfo, tracker.getAnalysisId());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.sonar.api.SonarProduct;
import org.sonarsource.sonarlint.visualstudio.roslyn.http.HttpAnalysisRequestHandler;
import org.sonarsource.sonarlint.visualstudio.roslyn.http.HttpClientHandler;
import org.sonarsource.sonarlint.visualstudio.roslyn.http.HttpClientProvider;
import org.sonarsource.sonarlint.visualstudio.roslyn.http.JsonRequestBuilder;

public class SqvsRoslynPlugin implements Plugin {
Expand All @@ -33,9 +34,12 @@ public void define(Context context) {
context.addExtensions(
SqvsRoslynSensor.class,
JsonRequestBuilder.class,
HttpClientProvider.class,
HttpClientHandler.class,
AnalysisCancellationService.class,
InstanceConfigurationProvider.class,
AnalysisPropertiesProvider.class,
RemoteAnalysisService.class,
HttpAnalysisRequestHandler.class);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,23 +36,24 @@
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonarsource.sonarlint.visualstudio.roslyn.http.AnalyzerInfoDto;
import org.sonarsource.sonarlint.visualstudio.roslyn.http.HttpAnalysisRequestHandler;
import org.sonarsource.sonarlint.visualstudio.roslyn.protocol.RoslynIssue;
import org.sonarsource.sonarlint.visualstudio.roslyn.protocol.RoslynIssueLocation;
import org.sonarsource.sonarlint.visualstudio.roslyn.protocol.RoslynIssueQuickFix;

public class SqvsRoslynSensor implements Sensor {

private static final Logger LOG = Loggers.get(SqvsRoslynSensor.class);
private final HttpAnalysisRequestHandler httpRequestHandler;
private final InstanceConfigurationProvider instanceConfigurationProvider;
private final AnalysisPropertiesProvider analysisPropertiesProvider;
private final RemoteAnalysisService remoteAnalysisService;

public SqvsRoslynSensor(HttpAnalysisRequestHandler httpRequestHandler, InstanceConfigurationProvider instanceConfigurationProvider,
AnalysisPropertiesProvider analysisPropertiesProvider) {
this.httpRequestHandler = httpRequestHandler;
public SqvsRoslynSensor(
InstanceConfigurationProvider instanceConfigurationProvider,
AnalysisPropertiesProvider analysisPropertiesProvider,
RemoteAnalysisService remoteAnalysisService) {
this.instanceConfigurationProvider = instanceConfigurationProvider;
this.analysisPropertiesProvider = analysisPropertiesProvider;
this.remoteAnalysisService = remoteAnalysisService;
}

private static void handle(SensorContext context, RoslynIssue roslynIssue) {
Expand Down Expand Up @@ -140,14 +141,15 @@ private void analyze(SensorContext context, FilePredicate predicate) {
var activeRules = getActiveRules(context);
var analysisProperties = analysisPropertiesProvider.getAnalysisProperties();
var analyzerInfo = getAnalyzerInfo();
var roslynIssues = httpRequestHandler.analyze(inputFiles, activeRules, analysisProperties, analyzerInfo);
var roslynIssues = remoteAnalysisService.analyze(inputFiles, activeRules, analysisProperties, analyzerInfo);
for (var roslynIssue : roslynIssues) {
try {
handle(context, roslynIssue);
} catch (Exception exception) {
LOG.error(String.format("Issue %s can not be saved due to ", roslynIssue.getRuleId()), exception.fillInStackTrace());
}
}

}

private Collection<String> getFilePaths(SensorContext context, FilePredicate predicate) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@
import java.util.Collection;
import java.util.Map;

public record AnalysisRequestDto(@SerializedName("FileNames") Collection<String> fileNames, @SerializedName("ActiveRules") Collection<ActiveRuleDto> activeRules,
@SerializedName("AnalysisProperties") Map<String, String> analysisProperties, @SerializedName("AnalyzerInfo") AnalyzerInfoDto analyzerInfo) {

public record AnalysisRequestDto(
@SerializedName("FileNames") Collection<String> fileNames,
@SerializedName("ActiveRules") Collection<ActiveRuleDto> activeRules,
@SerializedName("AnalysisProperties") Map<String, String> analysisProperties,
@SerializedName("AnalyzerInfo") AnalyzerInfoDto analyzerInfo,
@SerializedName("AnalysisId") java.util.UUID analysisId) {
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* SonarQube Ide VisualStudio Roslyn Plugin
* Copyright (C) 2025-2025 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonarsource.sonarlint.visualstudio.roslyn.http;

import com.google.gson.annotations.SerializedName;

public record CancellationRequestDto(@SerializedName("AnalysisId") java.util.UUID analysisId) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,32 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.UUID;

import org.sonar.api.batch.rule.ActiveRule;
import org.sonar.api.scanner.ScannerSide;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonarsource.api.sonarlint.SonarLintSide;
import org.sonarsource.sonarlint.visualstudio.roslyn.protocol.RoslynIssue;

@ScannerSide
@SonarLintSide
public class HttpAnalysisRequestHandler {
private static final Logger LOG = Loggers.get(HttpAnalysisRequestHandler.class);
private final HttpClientHandler httpClientFactory;
private final HttpClientHandler httpClientHandler;

public HttpAnalysisRequestHandler(HttpClientHandler httpClientFactory) {
this.httpClientFactory = httpClientFactory;
public HttpAnalysisRequestHandler(HttpClientHandler httpClientHandler) {
this.httpClientHandler = httpClientHandler;
}

public Collection<RoslynIssue> analyze(Collection<String> fileNames, Collection<ActiveRule> activeRules, Map<String, String> analysisProperties, AnalyzerInfoDto analyzerInfo) {
public Collection<RoslynIssue> analyze(
Collection<String> fileNames,
Collection<ActiveRule> activeRules,
Map<String, String> analysisProperties,
AnalyzerInfoDto analyzerInfo,
UUID analysisId) {
Collection<RoslynIssue> roslynIssues = new ArrayList<>();
try {
var response = httpClientFactory.sendRequest(fileNames, activeRules, analysisProperties, analyzerInfo);
var response = httpClientHandler.sendAnalyzeRequest(fileNames, activeRules, analysisProperties, analyzerInfo, analysisId);
if (response.statusCode() != HttpURLConnection.HTTP_OK) {
LOG.error("Response from server is {}.", response.statusCode());
return roslynIssues;
Expand All @@ -66,4 +71,18 @@ public Collection<RoslynIssue> analyze(Collection<String> fileNames, Collection<

return roslynIssues;
}

public void cancelAnalysis(UUID analysisId) {
var requestFuture = httpClientHandler.sendCancelRequest(analysisId);

requestFuture.exceptionally(e -> {
LOG.error("Failed to cancel analysis due to: {}", e.getMessage(), e);
return null;
}).thenApply(response -> {
if (response != null && response.statusCode() != HttpURLConnection.HTTP_OK) {
LOG.error("Response from cancel request is {}.", response.statusCode());
}
return null;
});
}
}
Loading