Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.sonarlint.languageserver.SonarLintLanguageServer Maven / Gradle / Ivy
Go to download
SonarLint Language Server to provide SonarLint functionalities
/*
* SonarLint Language Server
* Copyright (C) 2009-2019 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.sonarlint.languageserver;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import com.google.gson.Gson;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.CodeActionParams;
import org.eclipse.lsp4j.CodeLens;
import org.eclipse.lsp4j.CodeLensParams;
import org.eclipse.lsp4j.Command;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.CompletionList;
import org.eclipse.lsp4j.CompletionParams;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.DiagnosticRelatedInformation;
import org.eclipse.lsp4j.DiagnosticSeverity;
import org.eclipse.lsp4j.DidChangeConfigurationParams;
import org.eclipse.lsp4j.DidChangeTextDocumentParams;
import org.eclipse.lsp4j.DidChangeWatchedFilesParams;
import org.eclipse.lsp4j.DidChangeWorkspaceFoldersParams;
import org.eclipse.lsp4j.DidCloseTextDocumentParams;
import org.eclipse.lsp4j.DidOpenTextDocumentParams;
import org.eclipse.lsp4j.DidSaveTextDocumentParams;
import org.eclipse.lsp4j.DocumentFormattingParams;
import org.eclipse.lsp4j.DocumentHighlight;
import org.eclipse.lsp4j.DocumentOnTypeFormattingParams;
import org.eclipse.lsp4j.DocumentRangeFormattingParams;
import org.eclipse.lsp4j.DocumentSymbol;
import org.eclipse.lsp4j.DocumentSymbolParams;
import org.eclipse.lsp4j.ExecuteCommandOptions;
import org.eclipse.lsp4j.ExecuteCommandParams;
import org.eclipse.lsp4j.Hover;
import org.eclipse.lsp4j.InitializeParams;
import org.eclipse.lsp4j.InitializeResult;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.LocationLink;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.PublishDiagnosticsParams;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.ReferenceParams;
import org.eclipse.lsp4j.RenameParams;
import org.eclipse.lsp4j.SaveOptions;
import org.eclipse.lsp4j.ServerCapabilities;
import org.eclipse.lsp4j.SignatureHelp;
import org.eclipse.lsp4j.SymbolInformation;
import org.eclipse.lsp4j.TextDocumentPositionParams;
import org.eclipse.lsp4j.TextDocumentSyncKind;
import org.eclipse.lsp4j.TextDocumentSyncOptions;
import org.eclipse.lsp4j.TextEdit;
import org.eclipse.lsp4j.WorkspaceEdit;
import org.eclipse.lsp4j.WorkspaceFolder;
import org.eclipse.lsp4j.WorkspaceFoldersChangeEvent;
import org.eclipse.lsp4j.WorkspaceFoldersOptions;
import org.eclipse.lsp4j.WorkspaceServerCapabilities;
import org.eclipse.lsp4j.WorkspaceSymbolParams;
import org.eclipse.lsp4j.jsonrpc.Launcher;
import org.eclipse.lsp4j.jsonrpc.ResponseErrorException;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.jsonrpc.messages.ResponseError;
import org.eclipse.lsp4j.services.LanguageServer;
import org.eclipse.lsp4j.services.TextDocumentService;
import org.eclipse.lsp4j.services.WorkspaceService;
import org.sonar.api.internal.apachecommons.lang.StringUtils;
import org.sonarsource.sonarlint.core.client.api.common.RuleDetails;
import org.sonarsource.sonarlint.core.client.api.common.analysis.AnalysisResults;
import org.sonarsource.sonarlint.core.client.api.common.analysis.ClientInputFile;
import org.sonarsource.sonarlint.core.client.api.common.analysis.Issue;
import org.sonarsource.sonarlint.core.client.api.common.analysis.Issue.Flow;
import org.sonarsource.sonarlint.core.client.api.common.analysis.IssueListener;
import org.sonarsource.sonarlint.core.client.api.common.analysis.IssueLocation;
import org.sonarsource.sonarlint.core.client.api.connected.ConnectedAnalysisConfiguration;
import org.sonarsource.sonarlint.core.client.api.connected.ConnectedSonarLintEngine;
import org.sonarsource.sonarlint.core.client.api.connected.ProjectBinding;
import org.sonarsource.sonarlint.core.client.api.connected.ServerConfiguration;
import org.sonarsource.sonarlint.core.client.api.exceptions.GlobalStorageUpdateRequiredException;
import org.sonarsource.sonarlint.core.client.api.exceptions.ProjectNotFoundException;
import org.sonarsource.sonarlint.core.client.api.exceptions.StorageException;
import org.sonarsource.sonarlint.core.client.api.standalone.StandaloneAnalysisConfiguration;
import org.sonarsource.sonarlint.core.client.api.standalone.StandaloneSonarLintEngine;
import org.sonarsource.sonarlint.core.client.api.util.FileUtils;
import org.sonarsource.sonarlint.core.telemetry.TelemetryPathManager;
import static java.util.Collections.singleton;
import static java.util.Objects.nonNull;
import static org.apache.commons.lang.StringUtils.isBlank;
import static org.sonarlint.languageserver.UserSettings.CONNECTED_MODE_PROJECT_PROP;
import static org.sonarlint.languageserver.UserSettings.CONNECTED_MODE_SERVERS_PROP;
import static org.sonarlint.languageserver.UserSettings.TYPESCRIPT_LOCATION;
public class SonarLintLanguageServer implements LanguageServer, WorkspaceService, TextDocumentService {
private static final String USER_AGENT = "SonarLint Language Server";
private static final String TYPESCRIPT_PATH_PROP = "sonar.typescript.internal.typescriptLocation";
private static final String SONARLINT_CONFIGURATION_NAMESPACE = "sonarlint";
private static final String SONARLINT_SOURCE = SONARLINT_CONFIGURATION_NAMESPACE;
private static final String SONARLINT_OPEN_RULE_DESCRIPTION_COMMAND = "SonarLint.OpenRuleDesc";
private static final String SONARLINT_DEACTIVATE_RULE_COMMAND = "SonarLint.DeactivateRule";
static final String SONARLINT_UPDATE_SERVER_STORAGE_COMMAND = "SonarLint.UpdateServerStorage";
static final String SONARLINT_UPDATE_PROJECT_BINDING_COMMAND = "SonarLint.UpdateProjectBinding";
static final String SONARLINT_REFRESH_DIAGNOSTICS_COMMAND = "SonarLint.RefreshDiagnostics";
private static final List SONARLINT_COMMANDS = Arrays.asList(
SONARLINT_UPDATE_SERVER_STORAGE_COMMAND,
SONARLINT_UPDATE_PROJECT_BINDING_COMMAND,
SONARLINT_REFRESH_DIAGNOSTICS_COMMAND);
private final SonarLintLanguageClient client;
private final Future> backgroundProcess;
private final LanguageClientLogOutput logOutput;
private final ClientLogger logger;
private final Map languageIdPerFileURI = new HashMap<>();
private final SonarLintTelemetry telemetry = new SonarLintTelemetry();
private UserSettings userSettings = new UserSettings();
private final List workspaceFolders = new ArrayList<>();
private final Map workspaceBindings = new HashMap<>();
private final Map workspaceTrackers = new HashMap<>();
private final EngineCache engineCache;
private final ServerInfoCache serverInfoCache;
private ServerProjectBinding binding;
private final ServerIssueTrackingLogger serverIssueTrackingLogger = new ServerIssueTrackingLogger();
SonarLintLanguageServer(InputStream inputStream, OutputStream outputStream,
BiFunction engineCacheFactory,
Function loggerFactory) {
Launcher launcher = Launcher.createLauncher(this,
SonarLintLanguageClient.class,
inputStream,
outputStream,
true, null);
this.client = launcher.getRemoteProxy();
this.logOutput = new LanguageClientLogOutput(client);
backgroundProcess = launcher.startListening();
this.logger = loggerFactory.apply(this.client);
this.engineCache = engineCacheFactory.apply(logOutput, logger);
this.serverInfoCache = new ServerInfoCache(logger);
}
static SonarLintLanguageServer bySocket(int port, Collection analyzers) throws IOException {
Socket socket = new Socket("localhost", port);
BiFunction engineCacheFactory = (logOutput, logger) -> {
StandaloneEngineFactory standaloneEngineFactory = new StandaloneEngineFactory(analyzers, logOutput, logger);
ConnectedEngineFactory connectedEngineFactory = new ConnectedEngineFactory(logOutput, logger);
return new DefaultEngineCache(standaloneEngineFactory, connectedEngineFactory);
};
Function loggerFactory = DefaultClientLogger::new;
return new SonarLintLanguageServer(socket.getInputStream(), socket.getOutputStream(), engineCacheFactory, loggerFactory);
}
@Override
public CompletableFuture initialize(InitializeParams params) {
workspaceFolders.addAll(parseWorkspaceFolders(params.getWorkspaceFolders(), params.getRootUri()));
workspaceFolders.sort(Comparator.reverseOrder());
Map options = UserSettings.parseToMap(params.getInitializationOptions());
userSettings = new UserSettings(options);
String productKey = (String) options.get("productKey");
// deprecated, will be ignored when productKey present
String telemetryStorage = (String) options.get("telemetryStorage");
String productName = (String) options.get("productName");
String productVersion = (String) options.get("productVersion");
String typeScriptPath = (String) options.get(TYPESCRIPT_LOCATION);
engineCache.putExtraProperty(TYPESCRIPT_PATH_PROP, typeScriptPath);
serverInfoCache.replace(options.get(CONNECTED_MODE_SERVERS_PROP));
updateBinding((Map, ?>) options.get(CONNECTED_MODE_PROJECT_PROP));
telemetry.init(getStoragePath(productKey, telemetryStorage), productName, productVersion, this::usesConnectedMode, this::usesSonarCloud);
telemetry.optOut(userSettings.disableTelemetry);
InitializeResult result = new InitializeResult();
ServerCapabilities c = new ServerCapabilities();
c.setTextDocumentSync(getTextDocumentSyncOptions());
c.setCodeActionProvider(true);
c.setExecuteCommandProvider(new ExecuteCommandOptions(SONARLINT_COMMANDS));
c.setWorkspace(getWorkspaceServerCapabilities());
result.setCapabilities(c);
return CompletableFuture.completedFuture(result);
}
private boolean usesConnectedMode() {
// TODO check if this is correct
return !serverInfoCache.isEmpty();
}
private boolean usesSonarCloud() {
// TODO check if this is correct
return serverInfoCache.containsSonarCloud();
}
private static WorkspaceServerCapabilities getWorkspaceServerCapabilities() {
WorkspaceFoldersOptions options = new WorkspaceFoldersOptions();
options.setSupported(true);
options.setChangeNotifications(true);
WorkspaceServerCapabilities capabilities = new WorkspaceServerCapabilities();
capabilities.setWorkspaceFolders(options);
return capabilities;
}
private static TextDocumentSyncOptions getTextDocumentSyncOptions() {
TextDocumentSyncOptions textDocumentSyncOptions = new TextDocumentSyncOptions();
textDocumentSyncOptions.setOpenClose(true);
textDocumentSyncOptions.setChange(TextDocumentSyncKind.Full);
textDocumentSyncOptions.setSave(new SaveOptions(true));
return textDocumentSyncOptions;
}
private void handleUpdateServerStorageCommand(@Nullable List arguments) {
engineCache.clearConnectedEngines();
serverInfoCache.replace(arguments);
serverInfoCache.forEach((serverId, serverInfo) -> {
ConnectedSonarLintEngine engine = engineCache.getOrCreateConnectedEngine(serverInfo);
if (engine == null) {
logger.warn("Could not start server: " + serverId);
} else {
updateServerStorage(engine, serverInfo);
}
});
}
private void updateServerStorage(ConnectedSonarLintEngine engine, ServerInfo serverInfo) {
String serverId = serverInfo.serverId;
logger.debug("Updating global storage of server " + serverId + ", may take some time...");
ServerConfiguration serverConfig = getServerConfiguration(serverInfo);
engine.update(serverConfig, null);
logger.debug("Successfully updated global storage of server " + serverId);
}
private static ServerConfiguration getServerConfiguration(ServerInfo serverInfo) {
return ServerConfiguration.builder()
.url(serverInfo.serverUrl)
.token(serverInfo.token)
.organizationKey(serverInfo.organizationKey)
.userAgent(USER_AGENT)
.build();
}
private void updateBinding(@Nullable Map, ?> connectedModeProject) {
binding = null;
if (connectedModeProject == null) {
return;
}
Map map = (Map) connectedModeProject;
if (map.isEmpty()) {
return;
}
String serverId = map.get("serverId");
String projectKey = map.get("projectKey");
if (isBlank(serverId) || isBlank(projectKey)) {
logger.error(ClientLogger.ErrorType.INCOMPLETE_BINDING);
return;
}
ServerInfo serverInfo = serverInfoCache.get(serverId);
if (serverInfo == null) {
logger.error(ClientLogger.ErrorType.INVALID_BINDING_SERVER);
return;
}
ConnectedSonarLintEngine engine = engineCache.getOrCreateConnectedEngine(serverInfo);
if (engine == null) {
logger.error(ClientLogger.ErrorType.START_CONNECTED_ENGINE_FAILED);
return;
}
binding = new ServerProjectBinding(serverId, projectKey);
if (updateProjectStorage(engine, serverInfo)) {
updateIssueTrackers(engine, serverInfo);
} else {
binding = null;
}
}
private void updateIssueTrackers(ConnectedSonarLintEngine engine, ServerInfo serverInfo) {
workspaceBindings.clear();
workspaceTrackers.clear();
workspaceFolders.forEach(folderRoot -> {
Collection ideFilePaths = FileUtils.allRelativePathsForFilesInTree(Paths.get(folderRoot));
ProjectBinding projectBinding = engine.calculatePathPrefixes(binding.projectKey, ideFilePaths);
workspaceBindings.put(folderRoot, projectBinding);
logger.debug(String.format("Resolved sqPathPrefix:%s / idePathPrefix:%s / for folder %s",
projectBinding.sqPathPrefix(),
projectBinding.idePathPrefix(),
folderRoot));
workspaceTrackers.put(folderRoot,
new ServerIssueTracker(engine, getServerConfiguration(serverInfo), projectBinding, serverIssueTrackingLogger));
});
}
private boolean updateProjectStorage(ConnectedSonarLintEngine engine, ServerInfo serverInfo) {
ServerConfiguration serverConfig = getServerConfiguration(serverInfo);
try {
engine.updateProject(serverConfig, binding.projectKey, null);
return true;
} catch (ProjectNotFoundException e) {
logger.error(ClientLogger.ErrorType.PROJECT_NOT_FOUND);
} catch (Exception e) {
logger.warn(e.getMessage());
}
return false;
}
// visible for testing
static List parseWorkspaceFolders(@Nullable List workspaceFolders, @Nullable String rootUri) {
if (workspaceFolders != null && !workspaceFolders.isEmpty()) {
return toList(workspaceFolders);
}
// rootURI is null when no folder is open (like opening a single file in VSCode)
if (rootUri != null) {
return Collections.singletonList(rootUri);
}
return Collections.emptyList();
}
private static List toList(List workspaceFolders) {
return workspaceFolders.stream()
.filter(f -> f.getUri().startsWith("file:/"))
.map(f -> normalizeUriString(f.getUri()))
.collect(Collectors.toList());
}
static String normalizeUriString(String uriString) {
return Paths.get(URI.create(uriString)).toFile().toString();
}
// visible for testing
static Path getStoragePath(@Nullable String productKey, @Nullable String telemetryStorage) {
if (productKey != null) {
if (telemetryStorage != null) {
TelemetryPathManager.migrate(productKey, Paths.get(telemetryStorage));
}
return TelemetryPathManager.getPath(productKey);
}
return telemetryStorage != null ? Paths.get(telemetryStorage) : null;
}
@Override
public CompletableFuture shutdown() {
engineCache.stopStandaloneEngine();
engineCache.clearConnectedEngines();
telemetry.stop();
return CompletableFuture.completedFuture("Stopped");
}
@Override
public void exit() {
backgroundProcess.cancel(true);
}
@Override
public TextDocumentService getTextDocumentService() {
return this;
}
@Override
public CompletableFuture, CompletionList>> completion(CompletionParams completionParams) {
return null;
}
@Override
public CompletableFuture resolveCompletionItem(CompletionItem unresolved) {
return null;
}
@Override
public CompletableFuture hover(TextDocumentPositionParams position) {
return null;
}
@Override
public CompletableFuture signatureHelp(TextDocumentPositionParams position) {
return null;
}
@Override
public CompletableFuture, List extends LocationLink>>> definition(TextDocumentPositionParams position) {
return null;
}
@Override
public CompletableFuture> references(ReferenceParams params) {
return null;
}
@Override
public CompletableFuture> documentHighlight(TextDocumentPositionParams position) {
return null;
}
@Override
public CompletableFuture>> documentSymbol(DocumentSymbolParams params) {
return null;
}
@Override
public CompletableFuture>> codeAction(CodeActionParams params) {
List> commands = new ArrayList<>();
boolean standaloneMode = this.binding == null;
try {
for (Diagnostic d : params.getContext().getDiagnostics()) {
if (SONARLINT_SOURCE.equals(d.getSource())) {
String ruleKey = d.getCode();
List ruleDescriptionParams = getOpenRuleDescriptionParams(ruleKey);
if (!ruleDescriptionParams.isEmpty()) {
commands.add(Either.forLeft(
new Command(String.format("Open description of SonarLint rule '%s'", ruleKey),
SONARLINT_OPEN_RULE_DESCRIPTION_COMMAND,
ruleDescriptionParams)));
}
if (standaloneMode) {
commands.add(Either.forLeft(
new Command(String.format("Deactivate rule '%s'", ruleKey),
SONARLINT_DEACTIVATE_RULE_COMMAND,
Collections.singletonList(ruleKey))));
}
}
}
} catch (Exception e) {
logger.error("Unable to get code actions", e);
return completeExceptionally(-1, e.getMessage());
}
return CompletableFuture.completedFuture(commands);
}
private List getOpenRuleDescriptionParams(String ruleKey) {
RuleDetails ruleDetails;
if (binding == null) {
ruleDetails = engineCache.getOrCreateStandaloneEngine().getRuleDetails(ruleKey)
.orElseThrow(() -> new IllegalArgumentException("Unknow rule with key: " + ruleKey));
} else {
ServerInfo serverInfo = serverInfoCache.get(binding.serverId);
ConnectedSonarLintEngine engine = engineCache.getOrCreateConnectedEngine(serverInfo);
if (engine != null) {
ruleDetails = engine.getRuleDetails(ruleKey);
} else {
return Collections.emptyList();
}
}
String ruleName = ruleDetails.getName();
String htmlDescription = getHtmlDescription(ruleDetails);
String type = ruleDetails.getType();
String severity = ruleDetails.getSeverity();
return Arrays.asList(ruleKey, ruleName, htmlDescription, type, severity);
}
@Override
public CompletableFuture> codeLens(CodeLensParams params) {
return null;
}
@Override
public CompletableFuture resolveCodeLens(CodeLens unresolved) {
return null;
}
@Override
public CompletableFuture> formatting(DocumentFormattingParams params) {
return null;
}
@Override
public CompletableFuture> rangeFormatting(DocumentRangeFormattingParams params) {
return null;
}
@Override
public CompletableFuture> onTypeFormatting(DocumentOnTypeFormattingParams params) {
return null;
}
@Override
public CompletableFuture rename(RenameParams params) {
return null;
}
@Override
public void didOpen(DidOpenTextDocumentParams params) {
URI uri = parseURI(params.getTextDocument().getUri());
languageIdPerFileURI.put(uri, params.getTextDocument().getLanguageId());
analyze(uri, params.getTextDocument().getText(), true);
}
@Override
public void didChange(DidChangeTextDocumentParams params) {
URI uri = parseURI(params.getTextDocument().getUri());
analyze(uri, params.getContentChanges().get(0).getText(), false);
}
@Override
public void didClose(DidCloseTextDocumentParams params) {
URI uri = parseURI(params.getTextDocument().getUri());
languageIdPerFileURI.remove(uri);
// Clear issues
client.publishDiagnostics(newPublishDiagnostics(uri));
}
@Override
public void didSave(DidSaveTextDocumentParams params) {
String content = params.getText();
if (content != null) {
URI uri = parseURI(params.getTextDocument().getUri());
analyze(uri, params.getText(), false);
}
}
private static URI parseURI(String uriStr) {
URI uri;
try {
uri = new URI(uriStr);
} catch (URISyntaxException e) {
throw new IllegalStateException(e.getMessage(), e);
}
return uri;
}
// visible for testing
void analyze(URI uri, String content, boolean shouldFetchServerIssues) {
if (!uri.toString().startsWith("file:/")) {
logger.warn("URI is not a file, analysis not supported");
return;
}
Map files = new HashMap<>();
files.put(uri, newPublishDiagnostics(uri));
AnalysisWrapper analysisWrapper = getAnalysisWrapper();
if (analysisWrapper.isExcludedByServerSideExclusions(uri)) {
logger.debug("Skip analysis of excluded file: " + uri);
return;
}
IssueListener issueListener = issue -> {
ClientInputFile inputFile = issue.getInputFile();
if (inputFile != null) {
URI uri1 = inputFile.getClientObject();
PublishDiagnosticsParams publish = files.computeIfAbsent(uri1, SonarLintLanguageServer::newPublishDiagnostics);
convert(issue).ifPresent(publish.getDiagnostics()::add);
}
};
try {
Path baseDir = findBaseDir(uri);
AnalysisResultsWrapper analysisResults = analysisWrapper.analyze(baseDir, uri, content, issueListener, shouldFetchServerIssues);
telemetry.analysisDoneOnSingleFile(StringUtils.substringAfterLast(uri.toString(), "."), analysisResults.analysisTime);
// Ignore files with parsing error
analysisResults.results.failedAnalysisFiles().stream()
.map(ClientInputFile::getClientObject)
.forEach(files::remove);
} catch (Exception e) {
logger.error(ClientLogger.ErrorType.ANALYSIS_FAILED, e);
}
files.values().forEach(client::publishDiagnostics);
}
private AnalysisWrapper getAnalysisWrapper() {
return getConnectedEngine()
.map(e -> (AnalysisWrapper) new ConnectedAnalysisWrapper(e, binding.projectKey))
.orElse(new StandaloneAnalysisWrapper());
}
private Optional getConnectedEngine() {
return Optional.ofNullable(binding)
.map(b -> serverInfoCache.get(b.serverId))
.filter(Objects::nonNull)
.map(engineCache::getOrCreateConnectedEngine);
}
static class AnalysisResultsWrapper {
private final AnalysisResults results;
private final int analysisTime;
AnalysisResultsWrapper(AnalysisResults results, int analysisTime) {
this.results = results;
this.analysisTime = analysisTime;
}
}
interface AnalysisWrapper {
AnalysisResultsWrapper analyze(Path baseDir, URI uri, String content, IssueListener issueListener, boolean shouldFetchServerIssues);
boolean isExcludedByServerSideExclusions(URI fileUri);
}
class StandaloneAnalysisWrapper implements AnalysisWrapper {
@Override
public boolean isExcludedByServerSideExclusions(URI fileUri) {
return false;
}
@Override
public AnalysisResultsWrapper analyze(Path baseDir, URI uri, String content, IssueListener issueListener, boolean shouldFetchServerIssues) {
StandaloneAnalysisConfiguration configuration = StandaloneAnalysisConfiguration.builder()
.setBaseDir(baseDir)
.addInputFiles(new DefaultClientInputFile(uri, getFileRelativePath(baseDir, uri), content, isTest(uri), languageIdPerFileURI.get(uri)))
.putAllExtraProperties(userSettings.analyzerProperties)
.addExcludedRules(userSettings.excludedRules)
.build();
logger.debug("Analysis triggered on " + uri + " with configuration: \n" + configuration.toString());
long start = System.currentTimeMillis();
StandaloneSonarLintEngine engine = engineCache.getOrCreateStandaloneEngine();
AnalysisResults analysisResults = engine.analyze(configuration, issueListener, logOutput, null);
int analysisTime = (int) (System.currentTimeMillis() - start);
return new AnalysisResultsWrapper(analysisResults, analysisTime);
}
}
class ConnectedAnalysisWrapper implements AnalysisWrapper {
private final ConnectedSonarLintEngine engine;
private final String projectKey;
ConnectedAnalysisWrapper(ConnectedSonarLintEngine engine, String projectKey) {
this.engine = engine;
this.projectKey = projectKey;
}
@Override
public boolean isExcludedByServerSideExclusions(URI fileUri) {
Path baseDir = findBaseDir(fileUri);
ProjectBinding projectBinding = workspaceBindings.getOrDefault(fileUri.toString(), new ProjectBinding(projectKey, "", ""));
return !engine.getExcludedFiles(projectBinding,
singleton(fileUri),
uri -> getFileRelativePath(baseDir, uri),
SonarLintLanguageServer.this::isTest)
.isEmpty();
}
@Override
public AnalysisResultsWrapper analyze(Path baseDir, URI uri, String content, IssueListener issueListener, boolean shouldFetchServerIssues) {
ConnectedAnalysisConfiguration configuration = ConnectedAnalysisConfiguration.builder()
.setProjectKey(projectKey)
.setBaseDir(baseDir)
.addInputFile(new DefaultClientInputFile(uri, getFileRelativePath(baseDir, uri), content, isTest(uri), languageIdPerFileURI.get(uri)))
.putAllExtraProperties(userSettings.analyzerProperties)
.build();
if (!userSettings.excludedRules.isEmpty()) {
logger.debug("Local rules settings are ignored, using quality profile from server");
}
logger.debug("Analysis triggered on " + uri + " with configuration: \n" + configuration.toString());
List issues = new LinkedList<>();
IssueListener collector = issues::add;
ServerInfo serverInfo = serverInfoCache.get(binding.serverId);
long start = System.currentTimeMillis();
AnalysisResults analysisResults;
try {
analysisResults = analyze(configuration, collector);
} catch (GlobalStorageUpdateRequiredException e) {
updateServerStorage(engine, serverInfo);
updateProjectStorage(engine, serverInfo);
analysisResults = analyze(configuration, collector);
} catch (StorageException e) {
updateProjectStorage(engine, serverInfo);
analysisResults = analyze(configuration, collector);
}
String filePath = FileUtils.toSonarQubePath(getFileRelativePath(baseDir, uri));
ServerIssueTracker serverIssueTracker = workspaceTrackers.get(baseDir.toString());
serverIssueTracker.matchAndTrack(filePath, issues, issueListener, shouldFetchServerIssues);
int analysisTime = (int) (System.currentTimeMillis() - start);
return new AnalysisResultsWrapper(analysisResults, analysisTime);
}
private AnalysisResults analyze(ConnectedAnalysisConfiguration configuration, IssueListener issueListener) {
return engine.analyze(configuration, issueListener, logOutput, null);
}
}
private boolean isTest(URI uri) {
return userSettings.testMatcher.matches(Paths.get(uri));
}
private class ServerIssueTrackingLogger implements org.sonarsource.sonarlint.core.tracking.Logger {
@Override
public void error(String message, Exception e) {
logger.error(message, e);
}
@Override
public void debug(String message, Exception e) {
logger.debug(message);
}
@Override
public void debug(String message) {
logger.debug(message);
}
}
// visible for testing
Path findBaseDir(URI uri) {
return findBaseDir(workspaceFolders, uri);
}
private static String getFileRelativePath(Path baseDir, URI uri) {
return baseDir.relativize(Paths.get(uri)).toString();
}
// visible for testing
static Path findBaseDir(List workspaceFolders, URI uri) {
Path inputFilePath = Paths.get(uri);
if (!workspaceFolders.isEmpty()) {
String uriString = inputFilePath.toString();
for (String folder : workspaceFolders) {
if (uriString.startsWith(folder)) {
return Paths.get(folder);
}
}
}
return inputFilePath.getParent();
}
static Optional convert(Issue issue) {
if (issue.getStartLine() != null) {
Range range = position(issue);
Diagnostic diagnostic = new Diagnostic();
DiagnosticSeverity severity = severity(issue.getSeverity());
diagnostic.setSeverity(severity);
diagnostic.setRange(range);
diagnostic.setCode(issue.getRuleKey());
diagnostic.setMessage(issue.getMessage() + " (" + issue.getRuleKey() + ")");
diagnostic.setSource(SONARLINT_SOURCE);
List flows = issue.flows();
// If multiple flows with more than 1 location, keep only the first flow
if (flows.size() > 1 && flows.stream().anyMatch(f -> f.locations().size() > 1)) {
flows = Collections.singletonList(flows.get(0));
}
diagnostic.setRelatedInformation(flows
.stream()
.flatMap(f -> f.locations().stream())
// Message is mandatory in lsp
.filter(l -> nonNull(l.getMessage()))
// Ignore global issue locations
.filter(l -> nonNull(l.getInputFile()))
.map(l -> {
DiagnosticRelatedInformation rel = new DiagnosticRelatedInformation();
rel.setMessage(l.getMessage());
rel.setLocation(new Location(l.getInputFile().uri().toString(), position(l)));
return rel;
}).collect(Collectors.toList()));
return Optional.of(diagnostic);
}
return Optional.empty();
}
private static DiagnosticSeverity severity(String severity) {
switch (severity.toUpperCase(Locale.ENGLISH)) {
case "BLOCKER":
case "CRITICAL":
return DiagnosticSeverity.Error;
case "MAJOR":
return DiagnosticSeverity.Warning;
case "MINOR":
return DiagnosticSeverity.Information;
case "INFO":
default:
return DiagnosticSeverity.Hint;
}
}
private static Range position(Issue issue) {
return new Range(
new Position(
issue.getStartLine() - 1,
issue.getStartLineOffset()),
new Position(
issue.getEndLine() - 1,
issue.getEndLineOffset()));
}
private static Range position(IssueLocation location) {
return new Range(
new Position(
location.getStartLine() - 1,
location.getStartLineOffset()),
new Position(
location.getEndLine() - 1,
location.getEndLineOffset()));
}
private static PublishDiagnosticsParams newPublishDiagnostics(URI newUri) {
PublishDiagnosticsParams p = new PublishDiagnosticsParams();
p.setDiagnostics(new ArrayList<>());
p.setUri(newUri.toString());
return p;
}
@Override
public WorkspaceService getWorkspaceService() {
return this;
}
@Override
public CompletableFuture executeCommand(ExecuteCommandParams params) {
try {
List args = params.getArguments();
switch (params.getCommand()) {
case SONARLINT_UPDATE_SERVER_STORAGE_COMMAND:
List list = args == null ? null : args.stream().map(UserSettings::parseToMap).collect(Collectors.toList());
handleUpdateServerStorageCommand(list);
break;
case SONARLINT_UPDATE_PROJECT_BINDING_COMMAND:
Map map = args == null || args.isEmpty() ? null : UserSettings.parseToMap(args.get(0));
updateBinding(map);
break;
case SONARLINT_REFRESH_DIAGNOSTICS_COMMAND:
Gson gson = new Gson();
Set docsToRefresh = args == null ?
Collections.emptySet() :
args.stream().map(arg -> gson.fromJson(arg.toString(), Document.class)).collect(Collectors.toSet());
docsToRefresh.forEach(doc -> analyze(parseURI(doc.uri), doc.text, false));
break;
default:
return completeExceptionally(-1, "Unsupported command: " + params.getCommand());
}
} catch (Exception e) {
String message = "Unable to process command '" + params.getCommand() + "'";
logger.error(message, e);
return completeExceptionally(-1, message + ": " + e.getMessage());
}
return CompletableFuture.completedFuture(null);
}
private static CompletableFuture completeExceptionally(int code, String message) {
CompletableFuture exceptionalResult = new CompletableFuture<>();
ResponseError responseError = new ResponseError();
responseError.setCode(code);
responseError.setMessage(message);
exceptionalResult.completeExceptionally(new ResponseErrorException(responseError));
return exceptionalResult;
}
// visible for testing
static String getHtmlDescription(RuleDetails ruleDetails) {
String htmlDescription = ruleDetails.getHtmlDescription();
String extendedDescription = ruleDetails.getExtendedDescription();
if (!extendedDescription.isEmpty()) {
htmlDescription += "" + extendedDescription + "
";
}
return htmlDescription;
}
@Override
public CompletableFuture> symbol(WorkspaceSymbolParams params) {
return null;
}
@Override
public void didChangeConfiguration(DidChangeConfigurationParams params) {
Map settings = UserSettings.parseToMap(params.getSettings());
Map entries = (Map) settings.get(SONARLINT_CONFIGURATION_NAMESPACE);
userSettings = new UserSettings(entries);
telemetry.optOut(userSettings.disableTelemetry);
}
@Override
public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) {
// No watched files
}
@Override
public void didChangeWorkspaceFolders(DidChangeWorkspaceFoldersParams params) {
WorkspaceFoldersChangeEvent event = params.getEvent();
workspaceFolders.removeAll(toList(event.getRemoved()));
workspaceFolders.addAll(toList(event.getAdded()));
workspaceFolders.sort(Comparator.reverseOrder());
if (binding != null) {
ServerInfo serverInfo = serverInfoCache.get(binding.serverId);
if (serverInfo != null) {
ConnectedSonarLintEngine engine = engineCache.getOrCreateConnectedEngine(serverInfo);
if (engine != null) {
updateIssueTrackers(engine, serverInfo);
}
}
}
}
static class ServerProjectBinding {
final String serverId;
final String projectKey;
ServerProjectBinding(String serverId, String projectKey) {
this.serverId = serverId;
this.projectKey = projectKey;
}
}
static class Document {
final String uri;
final String text;
public Document(String uri, String text) {
this.uri = uri;
this.text = text;
}
}
}