/*
* SonarLint for Eclipse
* Copyright (C) 2015-2017 SonarSource SA
* sonarlint@sonarsource.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.eclipse.core.internal.jobs;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.ISchedulingRule;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.Position;
import org.sonarlint.eclipse.core.SonarLintLogger;
import org.sonarlint.eclipse.core.analysis.IAnalysisConfigurator;
import org.sonarlint.eclipse.core.analysis.IFileLanguageProvider;
import org.sonarlint.eclipse.core.analysis.IPostAnalysisContext;
import org.sonarlint.eclipse.core.configurator.ProjectConfigurationRequest;
import org.sonarlint.eclipse.core.configurator.ProjectConfigurator;
import org.sonarlint.eclipse.core.internal.PreferencesUtils;
import org.sonarlint.eclipse.core.internal.SonarLintCorePlugin;
import org.sonarlint.eclipse.core.internal.TriggerType;
import org.sonarlint.eclipse.core.internal.markers.MarkerUtils;
import org.sonarlint.eclipse.core.internal.markers.TextRange;
import org.sonarlint.eclipse.core.internal.resources.SonarLintProperty;
import org.sonarlint.eclipse.core.internal.server.IServer;
import org.sonarlint.eclipse.core.internal.server.Server;
import org.sonarlint.eclipse.core.internal.tracking.IssueTracker;
import org.sonarlint.eclipse.core.internal.tracking.RawIssueTrackable;
import org.sonarlint.eclipse.core.internal.tracking.ServerIssueTrackable;
import org.sonarlint.eclipse.core.internal.tracking.ServerIssueUpdater;
import org.sonarlint.eclipse.core.internal.tracking.Trackable;
import org.sonarlint.eclipse.core.resource.ISonarLintFile;
import org.sonarlint.eclipse.core.resource.ISonarLintIssuable;
import org.sonarlint.eclipse.core.resource.ISonarLintProject;
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.connected.ConnectedAnalysisConfiguration;
import org.sonarsource.sonarlint.core.client.api.connected.ConnectedSonarLintEngine;
import org.sonarsource.sonarlint.core.client.api.connected.ServerConfiguration;
import org.sonarsource.sonarlint.core.client.api.connected.ServerIssue;
import org.sonarsource.sonarlint.core.client.api.standalone.StandaloneAnalysisConfiguration;
import org.sonarsource.sonarlint.core.client.api.util.FileUtils;
import static org.sonarlint.eclipse.core.internal.utils.StringUtils.trimToNull;
public class AnalyzeProjectJob extends AbstractSonarProjectJob {
private final List<SonarLintProperty> extraProps;
private final AnalyzeProjectRequest request;
public AnalyzeProjectJob(AnalyzeProjectRequest request) {
super(jobTitle(request), request.getProject());
this.request = request;
this.extraProps = PreferencesUtils.getExtraPropertiesForLocalAnalysis(request.getProject());
}
private static String jobTitle(AnalyzeProjectRequest request) {
if (request.getFiles() == null) {
return "SonarLint analysis of project " + request.getProject().getName();
}
if (request.getFiles().size() == 1) {
return "SonarLint analysis of file " + request.getFiles().iterator().next().getFile().getName();
}
return "SonarLint analysis of project " + request.getProject().getName() + " (" + request.getFiles().size() + " files)";
}
private final class AnalysisThread extends Thread {
private final Map<ISonarLintIssuable, List<Issue>> issuesPerResource;
private final StandaloneAnalysisConfiguration config;
private final ISonarLintProject project;
private final IServer server;
@Nullable
private volatile AnalysisResults result;
private AnalysisThread(@Nullable IServer server, Map<ISonarLintIssuable, List<Issue>> issuesPerResource, StandaloneAnalysisConfiguration config, ISonarLintProject project) {
super("SonarLint analysis");
this.server = server;
this.issuesPerResource = issuesPerResource;
this.config = config;
this.project = project;
}
@Override
public void run() {
result = AnalyzeProjectJob.this.run(server, config, project, issuesPerResource);
}
@CheckForNull
public AnalysisResults getResult() {
return result;
}
}
@Override
protected IStatus doRun(final IProgressMonitor monitor) {
if (monitor.isCanceled()) {
return Status.CANCEL_STATUS;
}
long startTime = System.currentTimeMillis();
SonarLintLogger.get().debug("Trigger: " + request.getTriggerType().name());
SonarLintLogger.get().info(this.getName() + "...");
// Analyze
Path analysisWorkDir = null;
try {
// Configure
Map<String, String> mergedExtraProps = new LinkedHashMap<>();
final Map<ISonarLintFile, IDocument> filesToAnalyze = request.getFiles().stream().collect(HashMap::new, (m, fWithDoc) -> m.put(fWithDoc.getFile(), fWithDoc.getDocument()),
HashMap::putAll);
Collection<ProjectConfigurator> usedDeprecatedConfigurators = configureDeprecated(getProject(), filesToAnalyze.keySet(), mergedExtraProps, monitor);
analysisWorkDir = Files.createTempDirectory(getProject().getWorkingDir(), "sonarlint");
List<ClientInputFile> inputFiles = buildInputFiles(analysisWorkDir, filesToAnalyze);
Collection<IAnalysisConfigurator> usedConfigurators = configure(getProject(), inputFiles, mergedExtraProps, analysisWorkDir, monitor);
for (SonarLintProperty sonarProperty : extraProps) {
mergedExtraProps.put(sonarProperty.getName(), sonarProperty.getValue());
}
if (!inputFiles.isEmpty()) {
runAnalysisAndUpdateMarkers(filesToAnalyze, monitor, mergedExtraProps, inputFiles, analysisWorkDir);
}
analysisCompleted(usedDeprecatedConfigurators, usedConfigurators, mergedExtraProps, monitor);
SonarLintCorePlugin.getAnalysisListenerManager().notifyListeners();
SonarLintLogger.get().debug(String.format("Done in %d ms", System.currentTimeMillis() - startTime));
} catch (Exception e) {
SonarLintLogger.get().error("Error during execution of SonarLint analysis", e);
return new Status(Status.WARNING, SonarLintCorePlugin.PLUGIN_ID, "Error when executing SonarLint analysis", e);
} finally {
if (analysisWorkDir != null) {
FileUtils.deleteRecursively(analysisWorkDir);
}
}
return monitor.isCanceled() ? Status.CANCEL_STATUS : Status.OK_STATUS;
}
private void runAnalysisAndUpdateMarkers(Map<ISonarLintFile, IDocument> docPerFiles, final IProgressMonitor monitor, Map<String, String> mergedExtraProps,
List<ClientInputFile> inputFiles, Path analysisWorkDir) throws CoreException {
StandaloneAnalysisConfiguration config;
IPath projectLocation = getProject().getResource().getLocation();
// In some unfrequent cases the project may be virtual and don't have physical location
// so fallback to use analysis work dir
Path projectBaseDir = projectLocation != null ? projectLocation.toFile().toPath() : analysisWorkDir;
Server server;
if (getProject().isBound()) {
server = (Server) SonarLintCorePlugin.getServersManager().getServer(getProjectConfig().getServerId());
if (server == null) {
throw new IllegalStateException(
"Project '" + getProject().getName() + "' is bound to an unknow server: '" + getProjectConfig().getServerId() + "'. Please fix project binding.");
}
SonarLintLogger.get().debug("Connected mode (using configuration of '" + getProjectConfig().getModuleKey() + "' in server '" + getProjectConfig().getServerId() + "')");
config = new ConnectedAnalysisConfiguration(trimToNull(getProjectConfig().getModuleKey()), projectBaseDir, getProject().getWorkingDir(), inputFiles, mergedExtraProps);
} else {
server = null;
SonarLintLogger.get().debug("Standalone mode (project not bound)");
config = new StandaloneAnalysisConfiguration(projectBaseDir, getProject().getWorkingDir(), inputFiles, mergedExtraProps);
}
Map<ISonarLintIssuable, List<Issue>> issuesPerResource = new LinkedHashMap<>();
request.getFiles().forEach(fileWithDoc -> issuesPerResource.put(fileWithDoc.getFile(), new ArrayList<>()));
AnalysisResults result = runAndCheckCancellation(server, config, issuesPerResource, monitor);
if (!monitor.isCanceled() && result != null) {
updateMarkers(server, docPerFiles, issuesPerResource, result, request.getTriggerType(), monitor);
}
}
private static List<ClientInputFile> buildInputFiles(Path tempDirectory, final Map<ISonarLintFile, IDocument> filesToAnalyze) {
List<ClientInputFile> inputFiles = new ArrayList<>(filesToAnalyze.size());
String allTestPattern = PreferencesUtils.getTestFileRegexps();
String[] testPatterns = allTestPattern.split(",");
final List<PathMatcher> pathMatchersForTests = createMatchersForTests(testPatterns);
for (final Map.Entry<ISonarLintFile, IDocument> fileWithDoc : filesToAnalyze.entrySet()) {
ISonarLintFile file = fileWithDoc.getKey();
String language = tryDetectLanguage(file);
ClientInputFile inputFile = new EclipseInputFile(pathMatchersForTests, file, tempDirectory, fileWithDoc.getValue(), language);
inputFiles.add(inputFile);
}
return inputFiles;
}
@CheckForNull
private static String tryDetectLanguage(ISonarLintFile file) {
String language = null;
for (IFileLanguageProvider languageProvider : SonarLintCorePlugin.getExtensionTracker().getLanguageProviders()) {
String detectedLanguage = languageProvider.language(file);
if (detectedLanguage != null) {
if (language == null) {
language = detectedLanguage;
} else if (!language.equals(detectedLanguage)) {
SonarLintLogger.get().error("Conflicting languages detected for file " + file.getName() + ". " + language + " and " + detectedLanguage);
}
}
}
return language;
}
private static List<PathMatcher> createMatchersForTests(String[] testPatterns) {
final List<PathMatcher> pathMatchersForTests = new ArrayList<>();
FileSystem fs = FileSystems.getDefault();
for (String testPattern : testPatterns) {
pathMatchersForTests.add(fs.getPathMatcher("glob:" + testPattern));
}
return pathMatchersForTests;
}
private static Collection<ProjectConfigurator> configureDeprecated(final ISonarLintProject project, Collection<ISonarLintFile> filesToAnalyze,
final Map<String, String> extraProperties,
final IProgressMonitor monitor) {
Collection<ProjectConfigurator> usedConfigurators = new ArrayList<>();
if (project.getResource() instanceof IProject) {
ProjectConfigurationRequest configuratorRequest = new ProjectConfigurationRequest((IProject) project.getResource(),
filesToAnalyze.stream()
.map(f -> (f.getResource() instanceof IFile) ? (IFile) f.getResource() : null)
.filter(Objects::nonNull)
.collect(Collectors.toList()),
extraProperties);
Collection<ProjectConfigurator> configurators = SonarLintCorePlugin.getExtensionTracker().getConfigurators();
for (ProjectConfigurator configurator : configurators) {
if (configurator.canConfigure((IProject) project.getResource())) {
configurator.configure(configuratorRequest, monitor);
usedConfigurators.add(configurator);
}
}
}
return usedConfigurators;
}
private static Collection<IAnalysisConfigurator> configure(final ISonarLintProject project, List<ClientInputFile> filesToAnalyze,
final Map<String, String> extraProperties, Path tempDir, final IProgressMonitor monitor) {
Collection<IAnalysisConfigurator> usedConfigurators = new ArrayList<>();
Collection<IAnalysisConfigurator> configurators = SonarLintCorePlugin.getExtensionTracker().getAnalysisConfigurators();
DefaultPreAnalysisContext context = new DefaultPreAnalysisContext(project, extraProperties, filesToAnalyze, tempDir);
for (IAnalysisConfigurator configurator : configurators) {
if (configurator.canConfigure(project)) {
configurator.configure(context, monitor);
usedConfigurators.add(configurator);
}
}
return usedConfigurators;
}
private void updateMarkers(@Nullable Server server, Map<ISonarLintFile, IDocument> docPerFile, Map<ISonarLintIssuable, List<Issue>> issuesPerResource, AnalysisResults result,
TriggerType triggerType, final IProgressMonitor monitor)
throws CoreException {
Set<ISonarLintFile> failedFiles = result.failedAnalysisFiles().stream().map(ClientInputFile::<ISonarLintFile>getClientObject).collect(Collectors.toSet());
Map<ISonarLintIssuable, List<Issue>> successfulFiles = issuesPerResource.entrySet().stream()
.filter(e -> !failedFiles.contains(e.getKey()))
// TODO handle non-file-level issues
.filter(e -> e.getKey() instanceof ISonarLintFile)
.collect(Collectors.toMap(Entry::getKey, Entry::getValue));
trackIssues(server, docPerFile, successfulFiles, triggerType, monitor);
}
private void trackIssues(@Nullable Server server, Map<ISonarLintFile, IDocument> docPerFile, Map<ISonarLintIssuable, List<Issue>> rawIssuesPerResource, TriggerType triggerType,
final IProgressMonitor monitor)
throws CoreException {
String localModuleKey = getProject().getName();
for (Map.Entry<ISonarLintIssuable, List<Issue>> entry : rawIssuesPerResource.entrySet()) {
if (monitor.isCanceled()) {
return;
}
ISonarLintFile resource = (ISonarLintFile) entry.getKey();
IDocument documentOrNull = docPerFile.get((ISonarLintFile) resource);
final IDocument documentNotNull;
if (documentOrNull == null) {
documentNotNull = resource.getDocument();
} else {
documentNotNull = documentOrNull;
}
List<Issue> rawIssues = entry.getValue();
List<Trackable> trackables = rawIssues.stream().map(issue -> transform(issue, resource, documentNotNull)).collect(Collectors.toList());
IssueTracker issueTracker = SonarLintCorePlugin.getOrCreateIssueTracker(getProject(), localModuleKey);
String relativePath = resource.getProjectRelativePath();
Collection<Trackable> tracked = issueTracker.matchAndTrackAsNew(relativePath, trackables);
if (server != null && shouldUpdateServerIssuesSync(triggerType)) {
tracked = trackServerIssuesSync(server, resource, tracked);
}
ISchedulingRule markerRule = ResourcesPlugin.getWorkspace().getRuleFactory().markerRule(resource.getResource());
try {
getJobManager().beginRule(markerRule, monitor);
SonarLintMarkerUpdater.createOrUpdateMarkers(resource, documentNotNull, tracked, triggerType, documentOrNull != null);
} finally {
getJobManager().endRule(markerRule);
}
// Now that markerId are set, store issues in cache
issueTracker.updateCache(relativePath, tracked);
}
if (server != null && shouldUpdateServerIssuesAsync(triggerType)) {
trackServerIssuesAsync(server, rawIssuesPerResource.keySet(), docPerFile, triggerType);
}
}
private static boolean shouldUpdateServerIssuesSync(TriggerType trigger) {
return trigger != TriggerType.EDITOR_CHANGE && trigger != TriggerType.EDITOR_OPEN;
}
/**
* To not have a delay when opening editor, server issues will be processed asynchronously
*/
private static boolean shouldUpdateServerIssuesAsync(TriggerType trigger) {
return trigger == TriggerType.EDITOR_OPEN;
}
private static RawIssueTrackable transform(Issue issue, ISonarLintFile resource, IDocument document) {
Integer startLine = issue.getStartLine();
if (startLine == null) {
return new RawIssueTrackable(issue);
}
TextRange textRange = new TextRange(startLine, issue.getStartLineOffset(), issue.getEndLine(), issue.getEndLineOffset());
String textRangeContent = readTextRangeContent(resource, document, textRange);
String lineContent = readLineContent(resource, document, startLine);
return new RawIssueTrackable(issue, textRange, textRangeContent, lineContent);
}
@CheckForNull
private static String readTextRangeContent(ISonarLintFile resource, IDocument document, TextRange textRange) {
Position position = MarkerUtils.getPosition(document, textRange);
if (position != null) {
try {
return document.get(position.getOffset(), position.getLength());
} catch (BadLocationException e) {
SonarLintLogger.get().error("failed to get text range content of resource " + resource.getName(), e);
}
}
return null;
}
@CheckForNull
private static String readLineContent(ISonarLintFile resource, IDocument document, int startLine) {
Position position = MarkerUtils.getPosition(document, startLine);
if (position != null) {
try {
return document.get(position.getOffset(), position.getLength());
} catch (BadLocationException e) {
SonarLintLogger.get().error("Failed to get line content of file " + resource.getName(), e);
}
}
return null;
}
private Collection<Trackable> trackServerIssuesSync(Server server, ISonarLintFile resource, Collection<Trackable> tracked) {
ServerConfiguration serverConfiguration = server.getConfig();
ConnectedSonarLintEngine engine = server.getEngine();
List<ServerIssue> serverIssues = ServerIssueUpdater.fetchServerIssues(serverConfiguration, engine, getProjectConfig().getModuleKey(), resource);
Collection<Trackable> serverIssuesTrackable = serverIssues.stream().map(ServerIssueTrackable::new).collect(Collectors.toList());
return IssueTracker.matchAndTrackServerIssues(serverIssuesTrackable, tracked);
}
private void trackServerIssuesAsync(Server server, Collection<ISonarLintIssuable> resources, Map<ISonarLintFile, IDocument> docPerFile, TriggerType triggerType) {
ServerConfiguration serverConfiguration = server.getConfig();
ConnectedSonarLintEngine engine = server.getEngine();
String localModuleKey = getProject().getName();
SonarLintCorePlugin.getInstance().getServerIssueUpdater().updateAsync(serverConfiguration, engine, getProject(), localModuleKey, getProjectConfig().getModuleKey(), resources,
docPerFile, triggerType);
}
private static void analysisCompleted(Collection<ProjectConfigurator> usedDeprecatedConfigurators, Collection<IAnalysisConfigurator> usedConfigurators,
Map<String, String> properties, final IProgressMonitor monitor) {
Map<String, String> unmodifiableMap = Collections.unmodifiableMap(properties);
for (ProjectConfigurator p : usedDeprecatedConfigurators) {
p.analysisComplete(unmodifiableMap, monitor);
}
IPostAnalysisContext context = new IPostAnalysisContext() {
@Override
public ISonarLintProject getProject() {
return getProject();
}
@Override
public Map<String, String> getAnalysisProperties() {
return unmodifiableMap;
}
};
for (IAnalysisConfigurator p : usedConfigurators) {
p.analysisComplete(context, monitor);
}
}
@CheckForNull
public AnalysisResults runAndCheckCancellation(@Nullable IServer server, final StandaloneAnalysisConfiguration config,
final Map<ISonarLintIssuable, List<Issue>> issuesPerResource,
final IProgressMonitor monitor) {
SonarLintLogger.get().debug("Starting analysis with configuration:\n" + config.toString());
AnalysisThread t = new AnalysisThread(server, issuesPerResource, config, getProject());
t.setDaemon(true);
t.setUncaughtExceptionHandler((th, ex) -> SonarLintLogger.get().error("Error during analysis", ex));
t.start();
waitForThread(monitor, t);
return t.getResult();
}
private static void waitForThread(final IProgressMonitor monitor, Thread t) {
while (t.isAlive()) {
if (monitor.isCanceled()) {
t.interrupt();
try {
t.join(5000);
} catch (InterruptedException e) {
// just quit
}
if (t.isAlive()) {
SonarLintLogger.get().error("Unable to properly terminate SonarLint analysis");
}
break;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// Here we don't care
}
}
}
// Visible for testing
@CheckForNull
public AnalysisResults run(@Nullable IServer server, final StandaloneAnalysisConfiguration analysisConfig, final ISonarLintProject project,
final Map<ISonarLintIssuable, List<Issue>> issuesPerResource) {
SonarLintIssueListener issueListener = new SonarLintIssueListener(project, issuesPerResource);
AnalysisResults result;
if (server != null) {
result = server.runAnalysis((ConnectedAnalysisConfiguration) analysisConfig, issueListener);
} else {
StandaloneSonarLintClientFacade facadeToUse = SonarLintCorePlugin.getInstance().getDefaultSonarLintClientFacade();
result = facadeToUse.runAnalysis(analysisConfig, issueListener);
}
SonarLintLogger.get().info("Found " + issueListener.getIssueCount() + " issue(s)");
return result;
}
}