/*
* Copyright (c) 2013, the Dart project authors.
*
* Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.eclipse.org/legal/epl-v10.html
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.dart.tools.core.internal.builder;
import com.google.dart.engine.error.AnalysisError;
import com.google.dart.engine.error.ErrorCode;
import com.google.dart.engine.error.ErrorSeverity;
import com.google.dart.engine.error.ErrorType;
import com.google.dart.engine.utilities.source.LineInfo;
import com.google.dart.tools.core.DartCore;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import java.util.ArrayList;
/**
* Instances of {@code AnalysisMarkerManager} queue {@link AnalysisError}s from sources such as
* {@link AnalysisWorker} and translate those errors into Eclipse markers on a separate thread.
* There is a single instance accessible via {@link #getInstance()} for use during normal execution,
* but other instances can be created for testing purposes.
* <p>
* Typically the {@link AnalysisWorker} repeatedly calls
* {@link #queueErrors(IResource, LineInfo, AnalysisError[])} until all errors have been queued,
* then calls {@link #done()} to indicate that, at least for the time being, all errors have been
* queued.
* <p>
* When the workspace is shutdown, {@link #stop()} should be called to gracefully exit the
* background process if it is running.
*
* @coverage dart.tools.core.builder
*/
public class AnalysisMarkerManager {
/**
* Errors to be translated into markers
*/
private static final class ErrorResult implements Result {
final IResource resource;
final LineInfo lineInfo;
final AnalysisError[] errors;
ErrorResult(IResource resource, LineInfo lineInfo, AnalysisError[] errors) {
this.resource = resource;
this.lineInfo = lineInfo;
this.errors = errors;
}
@Override
public IResource getResource() {
return resource;
}
@Override
public void showErrors() throws CoreException {
if (!resource.isAccessible()) {
return;
}
resource.deleteMarkers(DartCore.DART_PROBLEM_MARKER_TYPE, true, IResource.DEPTH_ZERO);
resource.deleteMarkers(DartCore.DART_TASK_MARKER_TYPE, true, IResource.DEPTH_ZERO);
resource.deleteMarkers(DartCore.ANGULAR_WARNING_MARKER_TYPE, true, IResource.DEPTH_ZERO);
// Ignore if user requested to don't analyze resource.
if (!DartCore.isAnalyzed(resource)) {
return;
}
// Show errors first, then warnings, followed by everything else
// while limiting the total number of markers added to MAX_ERROR_COUNT
int errorCount = 0;
errorCount = showErrors(errorCount, ErrorSeverity.ERROR, IMarker.SEVERITY_ERROR);
errorCount = showErrors(errorCount, ErrorSeverity.WARNING, IMarker.SEVERITY_WARNING);
errorCount = showErrors(errorCount, ErrorSeverity.INFO, IMarker.SEVERITY_INFO);
if (errorCount >= MAX_ERROR_COUNT) {
IMarker marker = resource.createMarker(DartCore.DART_PROBLEM_MARKER_TYPE);
marker.setAttribute(IMarker.SEVERITY, IMarker.SEVERITY_WARNING);
marker.setAttribute(IMarker.LINE_NUMBER, 1);
marker.setAttribute(IMarker.MESSAGE, "There are more then " + MAX_ERROR_COUNT
+ " errors; not showing any more...");
}
}
private int showErrors(int errorCount, ErrorSeverity errorSeverity, int markerSeverity)
throws CoreException {
for (AnalysisError error : errors) {
ErrorCode errorCode = error.getErrorCode();
if (errorCode.getErrorSeverity() != errorSeverity) {
continue;
}
// TODO(scheglov) Analysis Server: restore LineInfo
int lineNum = lineInfo != null ? lineInfo.getLocation(error.getOffset()).getLineNumber()
: -1;
boolean isHint = errorCode.getType() == ErrorType.HINT;
String markerType = DartCore.DART_PROBLEM_MARKER_TYPE;
if (errorCode.getType() == ErrorType.ANGULAR) {
markerType = DartCore.ANGULAR_WARNING_MARKER_TYPE;
markerSeverity = IMarker.SEVERITY_WARNING;
} else if (errorCode.getType() == ErrorType.TODO) {
markerType = DartCore.DART_TASK_MARKER_TYPE;
} else if (isHint) {
markerType = DartCore.DART_HINT_MARKER_TYPE;
}
IMarker marker = resource.createMarker(markerType);
marker.setAttribute(IMarker.SEVERITY, markerSeverity);
marker.setAttribute(IMarker.CHAR_START, error.getOffset());
marker.setAttribute(IMarker.CHAR_END, error.getOffset() + error.getLength());
marker.setAttribute(IMarker.LINE_NUMBER, lineNum);
marker.setAttribute(ERROR_CODE, encodeErrorCode(errorCode));
marker.setAttribute(IMarker.MESSAGE, error.getMessage());
marker.setAttribute(DartCore.MARKER_ATTR_CORRECTION, error.getCorrection());
if (isHint) {
marker.setAttribute(IMarker.PRIORITY, IMarker.PRIORITY_HIGH);
}
errorCount++;
if (errorCount >= MAX_ERROR_COUNT) {
break;
}
}
return errorCount;
}
}
/**
* Add/remove marker indicating that a particular project has an SDK associated with it
*/
private final class HasSdkResult implements Result {
private final IProject project;
private final boolean hasSdk;
public HasSdkResult(IProject project, boolean hasSdk) {
this.project = project;
this.hasSdk = hasSdk;
}
@Override
public IResource getResource() {
return project;
}
@Override
public void showErrors() throws CoreException {
if (!project.isAccessible()) {
return;
}
project.deleteMarkers(DartCore.DART_PROBLEM_MARKER_TYPE, true, IResource.DEPTH_ZERO);
if (!hasSdk) {
IMarker marker = project.createMarker(DartCore.DART_PROBLEM_MARKER_TYPE);
marker.setAttribute(IMarker.SEVERITY, IMarker.SEVERITY_ERROR);
marker.setAttribute(IMarker.CHAR_START, 0);
marker.setAttribute(IMarker.CHAR_END, 0);
marker.setAttribute(IMarker.LINE_NUMBER, 1);
//TODO (danrubel): replace with real error code
marker.setAttribute(ERROR_CODE, 0);
//TODO (danrubel): improve error message to indicate action to install SDK
marker.setAttribute(IMarker.MESSAGE, "Missing Dart SDK");
//TODO (danrubel): Quick Fix ?
}
}
}
/**
* Results to be translated into markers
*/
private interface Result {
/**
* The resource for which markers are being added / removed.
*
* @return the resource
*/
IResource getResource();
/**
* Set markers on the specified resource to represent the cached analysis errors
*/
void showErrors() throws CoreException;
}
private static final int MAX_ERROR_COUNT = 500;
private static final String ERROR_CODE = "errorCode";
/**
* The singleton used for translating {@link AnalysisError}s into Eclipse markers.
*/
private static final AnalysisMarkerManager INSTANCE = new AnalysisMarkerManager(
ResourcesPlugin.getWorkspace());
/**
* Extract {@link ErrorCode} form the given {@link IMarker}.
*
* @return the {@link ErrorCode}, may be {@code null}.
*/
public static ErrorCode getErrorCode(IMarker marker) {
if (marker == null) {
return null;
}
String encoding = marker.getAttribute(ERROR_CODE, null);
if (encoding == null) {
return null;
}
return decodeErrorCode(encoding);
}
/**
* Answer the singleton used for translating {@link AnalysisError}s into Eclipse markers.
*
* @return the marker manager (not {@code null})
*/
public static AnalysisMarkerManager getInstance() {
return INSTANCE;
}
/**
* @return the {@link ErrorCode} enumeration constant for string from
* {@link #encodeErrorCode(ErrorCode)}.
*/
private static ErrorCode decodeErrorCode(String encoding) {
try {
String className = StringUtils.substringBeforeLast(encoding, ".");
String fieldName = StringUtils.substringAfterLast(encoding, ".");
Class<?> errorCodeClass = Class.forName(className);
return (ErrorCode) errorCodeClass.getField(fieldName).get(null);
} catch (Throwable e) {
return null;
}
}
/**
* @return the encoding of the given {@link ErrorCode} enumeration, good for passing it to
* {@link #decodeErrorCode(String)}.
*/
private static String encodeErrorCode(ErrorCode errorCode) {
return errorCode.getClass().getCanonicalName() + "." + ((Enum<?>) errorCode).name();
}
/**
* The workspace used to batch translation of errors to Eclipse markers (not {@code null}).
*/
private final IWorkspace workspace;
/**
* The progress monitor used for canceling the background process.
*/
private final NullProgressMonitor monitor = new NullProgressMonitor();
/**
* Synchronize against this object before accessing private fields and method in this class.
*/
private final Object lock = new Object();
/**
* A queue of results to be displayed.
* <p>
* Note: Only access this field while synchronized on {@link #lock}.
*/
private ArrayList<Result> results;
/**
* The background thread that translates {@link AnalysisError}s into Eclipse markers or
* {@code null} if either {@link #translateErrors()} has not been called or background processing
* is complete and there are no new errors to translate.
* <p>
* Note: Only access this field while synchronized on {@link #lock}.
*/
private Thread updateThread;
/**
* {@code true} if no call to {@link #queueErrors(IResource, LineInfo, AnalysisError[])} was made
* since the last call to {@link #done()}.
* <p>
* Note: Only access this field while synchronized on {@link #lock}.
*/
private boolean done;
/**
* Used exclusively by the background thread during translation. Should not be accessed in any
* other code.
*/
private ArrayList<Result> resultsBeingTranslated;
/**
* Construct a new instance for translating errors to markers using the specified workspace.
*/
public AnalysisMarkerManager(IWorkspace workspace) {
this.workspace = workspace;
}
/**
* Call this to clear markers and remove resource from error queue
*/
public void clearMarkers(IResource resource) {
//TODO(keertip): remove resource from queue
try {
if (resource.isAccessible()) {
if (resource instanceof IContainer) {
resource.deleteMarkers(null, false, IResource.DEPTH_INFINITE);
} else {
resource.deleteMarkers(null, false, IResource.DEPTH_ZERO);
}
}
} catch (Exception e) {
DartCore.logError(e);
}
}
/**
* Signal the background process to convert errors to markers, if it is not doing so already.
*/
public void done() {
synchronized (lock) {
done = true;
lock.notifyAll();
}
}
/**
* Queue the specified errors for later translation to Eclipse markers.
*
* @param resource the resource on which the errors should be displayed (not {@code null})
* @param lineInfo the line information (not {@code null})
* @param errors the errors to be translated (not {@code null}, contains no {@code null}s)
*/
public void queueErrors(IResource resource, LineInfo lineInfo, AnalysisError[] errors) {
queueResult(new ErrorResult(resource, lineInfo, errors));
}
/**
* Queue the specified information about whether the project has a Dart SDK associated with it so
* that the information can be translated into an Eclipse marker at a later time.
*
* @param resource the resource (not {@code null})
* @param hasSdk {@code true} if there is a Dart SDK, else {@code false}
*/
public void queueHasDartSdk(IResource resource, boolean hasSdk) {
IProject project = resource.getProject();
// workspace root getProject() returns null
if (project != null) {
queueResult(new HasSdkResult(project, hasSdk));
}
}
/**
* Call this method to cancel the background thread.
*/
public void stop() {
monitor.setCanceled(true);
}
/**
* Wait up to the specified number of milliseconds for the markers to be translated.
*
* @param milliseconds the number of milliseconds to wait for the markers to be translated
* @return {@code true} if all markers were translated, else {@code false}
*/
public boolean waitForMarkers(long milliseconds) {
synchronized (lock) {
long end = System.currentTimeMillis() + milliseconds;
while (updateThread != null) {
long delta = end - System.currentTimeMillis();
if (delta <= 0) {
return false;
}
try {
lock.wait(delta);
} catch (InterruptedException e) {
//$FALL-THROUGH$
}
}
return true;
}
}
/**
* Queue the specified result for later translation to Eclipse markers.
*
* @param result the result to be translated (not {@code null})
*/
private void queueResult(Result result) {
synchronized (lock) {
done = false;
// queue the errors to be translated
if (results == null) {
results = new ArrayList<Result>();
}
results.add(result);
// kick off a background thread if one has not already been started
if (updateThread == null) {
updateThread = new Thread(getClass().getSimpleName()) {
@Override
public void run() {
translateErrors();
}
};
updateThread.start();
}
}
}
/**
* Call this on the background thread to translate errors into Eclipse markers.
*/
private void translateErrors() {
while (true) {
synchronized (lock) {
// If not done, then wait up to 1 second or until signaled
if (!done) {
try {
lock.wait(1000);
} catch (InterruptedException e) {
//$FALL-THROUGH$
}
}
// Exit if nothing to translate
if (results == null) {
lock.notifyAll();
updateThread = null;
return;
}
// Grab the current collection of results to be translated
resultsBeingTranslated = results;
results = null;
}
// Batch translation of the errors
IWorkspaceRunnable op = new IWorkspaceRunnable() {
@Override
public void run(IProgressMonitor monitor) {
for (Result result : resultsBeingTranslated) {
if (monitor.isCanceled()) {
//TODO (danrubel): Investigate pushing remaining work back on the queue
// or serializing it on shutdown
break;
}
try {
result.showErrors();
} catch (CoreException e) {
DartCore.logError("Failed to show errors for " + result.getResource(), e);
}
}
resultsBeingTranslated = null;
}
};
try {
workspace.run(op, workspace.getRoot(), IWorkspace.AVOID_UPDATE, monitor);
} catch (CoreException e) {
DartCore.logError("Exception translating analysis errors to markers", e);
} catch (NullPointerException e) {
// Suppress this error if we are shutting down causing the workspace is in an invalid state
if (!monitor.isCanceled()) {
throw e;
}
}
}
}
}