aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintClient.java
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintClient.java')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintClient.java1306
1 files changed, 1306 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintClient.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintClient.java
new file mode 100644
index 000000000..3dd424087
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintClient.java
@@ -0,0 +1,1306 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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/org/documents/epl-v10.php
+ *
+ * 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.android.ide.eclipse.adt.internal.lint;
+
+import static com.android.SdkConstants.DOT_JAR;
+import static com.android.SdkConstants.DOT_XML;
+import static com.android.SdkConstants.FD_NATIVE_LIBS;
+import static com.android.ide.eclipse.adt.AdtConstants.MARKER_LINT;
+import static com.android.ide.eclipse.adt.AdtUtils.workspacePathToFile;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.sdklib.IAndroidTarget;
+import com.android.tools.lint.checks.BuiltinIssueRegistry;
+import com.android.tools.lint.client.api.Configuration;
+import com.android.tools.lint.client.api.IssueRegistry;
+import com.android.tools.lint.client.api.JavaParser;
+import com.android.tools.lint.client.api.LintClient;
+import com.android.tools.lint.client.api.LintDriver;
+import com.android.tools.lint.client.api.XmlParser;
+import com.android.tools.lint.detector.api.ClassContext;
+import com.android.tools.lint.detector.api.Context;
+import com.android.tools.lint.detector.api.DefaultPosition;
+import com.android.tools.lint.detector.api.Detector;
+import com.android.tools.lint.detector.api.Issue;
+import com.android.tools.lint.detector.api.JavaContext;
+import com.android.tools.lint.detector.api.LintUtils;
+import com.android.tools.lint.detector.api.Location;
+import com.android.tools.lint.detector.api.Location.Handle;
+import com.android.tools.lint.detector.api.Position;
+import com.android.tools.lint.detector.api.Project;
+import com.android.tools.lint.detector.api.Severity;
+import com.android.tools.lint.detector.api.TextFormat;
+import com.android.tools.lint.detector.api.XmlContext;
+import com.android.utils.Pair;
+import com.android.utils.SdkUtils;
+import com.google.common.collect.Maps;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.jdt.core.IClasspathEntry;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.core.ITypeHierarchy;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jdt.internal.compiler.CompilationResult;
+import org.eclipse.jdt.internal.compiler.DefaultErrorHandlingPolicies;
+import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
+import org.eclipse.jdt.internal.compiler.batch.CompilationUnit;
+import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants;
+import org.eclipse.jdt.internal.compiler.impl.CompilerOptions;
+import org.eclipse.jdt.internal.compiler.parser.Parser;
+import org.eclipse.jdt.internal.compiler.problem.AbortCompilation;
+import org.eclipse.jdt.internal.compiler.problem.DefaultProblemFactory;
+import org.eclipse.jdt.internal.compiler.problem.ProblemReporter;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IRegion;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.editors.text.TextFileDocumentProvider;
+import org.eclipse.ui.ide.IDE;
+import org.eclipse.ui.texteditor.IDocumentProvider;
+import org.eclipse.wst.sse.core.StructuredModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
+import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.WeakHashMap;
+
+import lombok.ast.ecj.EcjTreeConverter;
+import lombok.ast.grammar.ParseProblem;
+import lombok.ast.grammar.Source;
+
+/**
+ * Eclipse implementation for running lint on workspace files and projects.
+ */
+@SuppressWarnings("restriction") // DOM model
+public class EclipseLintClient extends LintClient {
+ static final String MARKER_CHECKID_PROPERTY = "checkid"; //$NON-NLS-1$
+ private static final String MODEL_PROPERTY = "model"; //$NON-NLS-1$
+ private final List<? extends IResource> mResources;
+ private final IDocument mDocument;
+ private boolean mWasFatal;
+ private boolean mFatalOnly;
+ private EclipseJavaParser mJavaParser;
+ private boolean mCollectNodes;
+ private Map<Node, IMarker> mNodeMap;
+
+ /**
+ * Creates a new {@link EclipseLintClient}.
+ *
+ * @param registry the associated detector registry
+ * @param resources the associated resources (project, file or null)
+ * @param document the associated document, or null if the {@code resource}
+ * param is not a file
+ * @param fatalOnly whether only fatal issues should be reported (and therefore checked)
+ */
+ public EclipseLintClient(IssueRegistry registry, List<? extends IResource> resources,
+ IDocument document, boolean fatalOnly) {
+ mResources = resources;
+ mDocument = document;
+ mFatalOnly = fatalOnly;
+ }
+
+ /**
+ * Returns true if lint should only check fatal issues
+ *
+ * @return true if lint should only check fatal issues
+ */
+ public boolean isFatalOnly() {
+ return mFatalOnly;
+ }
+
+ /**
+ * Sets whether the lint client should store associated XML nodes for each
+ * reported issue
+ *
+ * @param collectNodes if true, collect node positions for errors in XML
+ * files, retrievable via the {@link #getIssueForNode} method
+ */
+ public void setCollectNodes(boolean collectNodes) {
+ mCollectNodes = collectNodes;
+ }
+
+ /**
+ * Returns one of the issues for the given node (there could be more than one)
+ *
+ * @param node the node to look up lint issues for
+ * @return the marker for one of the issues found for the given node
+ */
+ @Nullable
+ public IMarker getIssueForNode(@NonNull UiViewElementNode node) {
+ if (mNodeMap != null) {
+ return mNodeMap.get(node.getXmlNode());
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns a collection of nodes that have one or more lint warnings
+ * associated with them (retrievable via
+ * {@link #getIssueForNode(UiViewElementNode)})
+ *
+ * @return a collection of nodes, which should <b>not</b> be modified by the
+ * caller
+ */
+ @Nullable
+ public Collection<Node> getIssueNodes() {
+ if (mNodeMap != null) {
+ return mNodeMap.keySet();
+ }
+
+ return null;
+ }
+
+ // ----- Extends LintClient -----
+
+ @Override
+ public void log(@NonNull Severity severity, @Nullable Throwable exception,
+ @Nullable String format, @Nullable Object... args) {
+ if (exception == null) {
+ AdtPlugin.log(IStatus.WARNING, format, args);
+ } else {
+ AdtPlugin.log(exception, format, args);
+ }
+ }
+
+ @Override
+ public XmlParser getXmlParser() {
+ return new XmlParser() {
+ @Override
+ public Document parseXml(@NonNull XmlContext context) {
+ // Map File to IFile
+ IFile file = AdtUtils.fileToIFile(context.file);
+ if (file == null || !file.exists()) {
+ String path = context.file.getPath();
+ AdtPlugin.log(IStatus.ERROR, "Can't find file %1$s in workspace", path);
+ return null;
+ }
+
+ IStructuredModel model = null;
+ try {
+ IModelManager modelManager = StructuredModelManager.getModelManager();
+ if (modelManager == null) {
+ // This can happen if incremental lint is running right as Eclipse is
+ // shutting down
+ return null;
+ }
+ model = modelManager.getModelForRead(file);
+ if (model instanceof IDOMModel) {
+ context.setProperty(MODEL_PROPERTY, model);
+ IDOMModel domModel = (IDOMModel) model;
+ return domModel.getDocument();
+ }
+ } catch (IOException e) {
+ AdtPlugin.log(e, "Cannot read XML file");
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+
+ return null;
+ }
+
+ @Override
+ public @NonNull Location getLocation(@NonNull XmlContext context, @NonNull Node node) {
+ IStructuredModel model = (IStructuredModel) context.getProperty(MODEL_PROPERTY);
+ return new LazyLocation(context.file, model.getStructuredDocument(),
+ (IndexedRegion) node);
+ }
+
+ @Override
+ public @NonNull Location getLocation(@NonNull XmlContext context, @NonNull Node node,
+ int start, int end) {
+ IndexedRegion region = (IndexedRegion) node;
+ int nodeStart = region.getStartOffset();
+
+ IStructuredModel model = (IStructuredModel) context.getProperty(MODEL_PROPERTY);
+ // Get line number
+ LazyLocation location = new LazyLocation(context.file,
+ model.getStructuredDocument(), region);
+ int line = location.getStart().getLine();
+
+ Position startPos = new DefaultPosition(line, -1, nodeStart + start);
+ Position endPos = new DefaultPosition(line, -1, nodeStart + end);
+ return Location.create(context.file, startPos, endPos);
+ }
+
+ @Override
+ public int getNodeStartOffset(@NonNull XmlContext context, @NonNull Node node) {
+ IndexedRegion region = (IndexedRegion) node;
+ return region.getStartOffset();
+ }
+
+ @Override
+ public int getNodeEndOffset(@NonNull XmlContext context, @NonNull Node node) {
+ IndexedRegion region = (IndexedRegion) node;
+ return region.getEndOffset();
+ }
+
+ @Override
+ public @NonNull Handle createLocationHandle(final @NonNull XmlContext context,
+ final @NonNull Node node) {
+ IStructuredModel model = (IStructuredModel) context.getProperty(MODEL_PROPERTY);
+ return new LazyLocation(context.file, model.getStructuredDocument(),
+ (IndexedRegion) node);
+ }
+
+ @Override
+ public void dispose(@NonNull XmlContext context, @NonNull Document document) {
+ IStructuredModel model = (IStructuredModel) context.getProperty(MODEL_PROPERTY);
+ assert model != null : context.file;
+ if (model != null) {
+ model.releaseFromRead();
+ }
+ }
+
+ @Override
+ @NonNull
+ public Location getNameLocation(@NonNull XmlContext context, @NonNull Node node) {
+ return getLocation(context, node);
+ }
+
+ @Override
+ @NonNull
+ public Location getValueLocation(@NonNull XmlContext context, @NonNull Attr node) {
+ return getLocation(context, node);
+ }
+
+ };
+ }
+
+ @Override
+ public JavaParser getJavaParser(@Nullable Project project) {
+ if (mJavaParser == null) {
+ mJavaParser = new EclipseJavaParser();
+ }
+
+ return mJavaParser;
+ }
+
+ // Cache for {@link getProject}
+ private IProject mLastEclipseProject;
+ private Project mLastLintProject;
+
+ private IProject getProject(Project project) {
+ if (project == mLastLintProject) {
+ return mLastEclipseProject;
+ }
+
+ mLastLintProject = project;
+ mLastEclipseProject = null;
+
+ if (mResources != null) {
+ if (mResources.size() == 1) {
+ IProject p = mResources.get(0).getProject();
+ mLastEclipseProject = p;
+ return p;
+ }
+
+ IProject last = null;
+ for (IResource resource : mResources) {
+ IProject p = resource.getProject();
+ if (p != last) {
+ if (project.getDir().equals(AdtUtils.getAbsolutePath(p).toFile())) {
+ mLastEclipseProject = p;
+ return p;
+ }
+ last = p;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ @NonNull
+ public String getProjectName(@NonNull Project project) {
+ // Initialize the lint project's name to the name of the Eclipse project,
+ // which might differ from the directory name
+ IProject eclipseProject = getProject(project);
+ if (eclipseProject != null) {
+ return eclipseProject.getName();
+ }
+
+ return super.getProjectName(project);
+ }
+
+ @NonNull
+ @Override
+ public Configuration getConfiguration(@NonNull Project project, @Nullable LintDriver driver) {
+ return getConfigurationFor(project);
+ }
+
+ /**
+ * Same as {@link #getConfiguration(Project)}, but {@code project} can be
+ * null in which case the global configuration is returned.
+ *
+ * @param project the project to look up
+ * @return a corresponding configuration
+ */
+ @NonNull
+ public Configuration getConfigurationFor(@Nullable Project project) {
+ if (project != null) {
+ IProject eclipseProject = getProject(project);
+ if (eclipseProject != null) {
+ return ProjectLintConfiguration.get(this, eclipseProject, mFatalOnly);
+ }
+ }
+
+ return GlobalLintConfiguration.get();
+ }
+ @Override
+ public void report(@NonNull Context context, @NonNull Issue issue, @NonNull Severity s,
+ @Nullable Location location,
+ @NonNull String message, @NonNull TextFormat format) {
+ message = format.toText(message);
+ int severity = getMarkerSeverity(s);
+ IMarker marker = null;
+ if (location != null) {
+ Position startPosition = location.getStart();
+ if (startPosition == null) {
+ if (location.getFile() != null) {
+ IResource resource = AdtUtils.fileToResource(location.getFile());
+ if (resource != null && resource.isAccessible()) {
+ marker = BaseProjectHelper.markResource(resource, MARKER_LINT,
+ message, 0, severity);
+ }
+ }
+ } else {
+ Position endPosition = location.getEnd();
+ int line = startPosition.getLine() + 1; // Marker API is 1-based
+ IFile file = AdtUtils.fileToIFile(location.getFile());
+ if (file != null && file.isAccessible()) {
+ Pair<Integer, Integer> r = getRange(file, mDocument,
+ startPosition, endPosition);
+ int startOffset = r.getFirst();
+ int endOffset = r.getSecond();
+ marker = BaseProjectHelper.markResource(file, MARKER_LINT,
+ message, line, startOffset, endOffset, severity);
+ }
+ }
+ }
+
+ if (marker == null) {
+ marker = BaseProjectHelper.markResource(mResources.get(0), MARKER_LINT,
+ message, 0, severity);
+ }
+
+ if (marker != null) {
+ // Store marker id such that we can recognize it from the suppress quickfix
+ try {
+ marker.setAttribute(MARKER_CHECKID_PROPERTY, issue.getId());
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ if (s == Severity.FATAL) {
+ mWasFatal = true;
+ }
+
+ if (mCollectNodes && location != null && marker != null) {
+ if (location instanceof LazyLocation) {
+ LazyLocation l = (LazyLocation) location;
+ IndexedRegion region = l.mRegion;
+ if (region instanceof Node) {
+ Node node = (Node) region;
+ if (node instanceof Attr) {
+ node = ((Attr) node).getOwnerElement();
+ }
+ if (mNodeMap == null) {
+ mNodeMap = new WeakHashMap<Node, IMarker>();
+ }
+ IMarker prev = mNodeMap.get(node);
+ if (prev != null) {
+ // Only replace the node if this node has higher priority
+ int prevSeverity = prev.getAttribute(IMarker.SEVERITY, 0);
+ if (prevSeverity < severity) {
+ mNodeMap.put(node, marker);
+ }
+ } else {
+ mNodeMap.put(node, marker);
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ @Nullable
+ public File findResource(@NonNull String relativePath) {
+ // Look within the $ANDROID_SDK
+ String sdkFolder = AdtPrefs.getPrefs().getOsSdkFolder();
+ if (sdkFolder != null) {
+ File file = new File(sdkFolder, relativePath);
+ if (file.exists()) {
+ return file;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Clears any lint markers from the given resource (project, folder or file)
+ *
+ * @param resource the resource to remove markers from
+ */
+ public static void clearMarkers(@NonNull IResource resource) {
+ clearMarkers(Collections.singletonList(resource));
+ }
+
+ /** Clears any lint markers from the given list of resource (project, folder or file) */
+ static void clearMarkers(List<? extends IResource> resources) {
+ for (IResource resource : resources) {
+ try {
+ if (resource.isAccessible()) {
+ resource.deleteMarkers(MARKER_LINT, false, IResource.DEPTH_INFINITE);
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ IEditorPart activeEditor = AdtUtils.getActiveEditor();
+ LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(activeEditor);
+ if (delegate != null) {
+ delegate.getGraphicalEditor().getLayoutActionBar().updateErrorIndicator();
+ }
+ }
+
+ /**
+ * Removes all markers of the given id from the given resource.
+ *
+ * @param resource the resource to remove markers from (file or project, or
+ * null for all open projects)
+ * @param id the id for the issue whose markers should be deleted
+ */
+ public static void removeMarkers(IResource resource, String id) {
+ if (resource == null) {
+ IJavaProject[] androidProjects = BaseProjectHelper.getAndroidProjects(null);
+ for (IJavaProject project : androidProjects) {
+ IProject p = project.getProject();
+ if (p != null) {
+ // Recurse, but with a different parameter so it will not continue recursing
+ removeMarkers(p, id);
+ }
+ }
+ return;
+ }
+ IMarker[] markers = getMarkers(resource);
+ for (IMarker marker : markers) {
+ if (id.equals(getId(marker))) {
+ try {
+ marker.delete();
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the lint marker for the given resource (which may be a project, folder or file)
+ *
+ * @param resource the resource to be checked, typically a source file
+ * @return an array of markers, possibly empty but never null
+ */
+ public static IMarker[] getMarkers(IResource resource) {
+ try {
+ if (resource.isAccessible()) {
+ return resource.findMarkers(MARKER_LINT, false, IResource.DEPTH_INFINITE);
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+
+ return new IMarker[0];
+ }
+
+ private static int getMarkerSeverity(Severity severity) {
+ switch (severity) {
+ case INFORMATIONAL:
+ return IMarker.SEVERITY_INFO;
+ case WARNING:
+ return IMarker.SEVERITY_WARNING;
+ case FATAL:
+ case ERROR:
+ default:
+ return IMarker.SEVERITY_ERROR;
+ }
+ }
+
+ private static Pair<Integer, Integer> getRange(IFile file, IDocument doc,
+ Position startPosition, Position endPosition) {
+ int startOffset = startPosition.getOffset();
+ int endOffset = endPosition != null ? endPosition.getOffset() : -1;
+ if (endOffset != -1) {
+ // Attribute ranges often include trailing whitespace; trim this up
+ if (doc == null) {
+ IDocumentProvider provider = new TextFileDocumentProvider();
+ try {
+ provider.connect(file);
+ doc = provider.getDocument(file);
+ if (doc != null) {
+ return adjustOffsets(doc, startOffset, endOffset);
+ }
+ } catch (Exception e) {
+ AdtPlugin.log(e, "Can't find range information for %1$s", file.getName());
+ } finally {
+ provider.disconnect(file);
+ }
+ } else {
+ return adjustOffsets(doc, startOffset, endOffset);
+ }
+ }
+
+ return Pair.of(startOffset, startOffset);
+ }
+
+ /**
+ * Trim off any trailing space on the given offset range in the given
+ * document, and don't span multiple lines on ranges since it makes (for
+ * example) the XML editor just glow with yellow underlines for all the
+ * attributes etc. Highlighting just the element beginning gets the point
+ * across. It also makes it more obvious where there are warnings on both
+ * the overall element and on individual attributes since without this the
+ * warnings on attributes would just overlap with the whole-element
+ * highlighting.
+ */
+ private static Pair<Integer, Integer> adjustOffsets(IDocument doc, int startOffset,
+ int endOffset) {
+ int originalStart = startOffset;
+ int originalEnd = endOffset;
+
+ if (doc != null) {
+ while (endOffset > startOffset && endOffset < doc.getLength()) {
+ try {
+ if (!Character.isWhitespace(doc.getChar(endOffset - 1))) {
+ break;
+ } else {
+ endOffset--;
+ }
+ } catch (BadLocationException e) {
+ // Pass - we've already validated offset range above
+ break;
+ }
+ }
+
+ // Also don't span lines
+ int lineEnd = startOffset;
+ while (lineEnd < endOffset) {
+ try {
+ char c = doc.getChar(lineEnd);
+ if (c == '\n' || c == '\r') {
+ endOffset = lineEnd;
+ if (endOffset > 0 && doc.getChar(endOffset - 1) == '\r') {
+ endOffset--;
+ }
+ break;
+ }
+ } catch (BadLocationException e) {
+ // Pass - we've already validated offset range above
+ break;
+ }
+ lineEnd++;
+ }
+ }
+
+ if (startOffset >= endOffset) {
+ // Selecting nothing (for example, for the mangled CRLF delimiter issue selecting
+ // just the newline)
+ // In that case, use the real range
+ return Pair.of(originalStart, originalEnd);
+ }
+
+ return Pair.of(startOffset, endOffset);
+ }
+
+ /**
+ * Returns true if a fatal error was encountered
+ *
+ * @return true if a fatal error was encountered
+ */
+ public boolean hasFatalErrors() {
+ return mWasFatal;
+ }
+
+ /**
+ * Describe the issue for the given marker
+ *
+ * @param marker the marker to look up
+ * @return a full description of the corresponding issue, never null
+ */
+ public static String describe(IMarker marker) {
+ IssueRegistry registry = getRegistry();
+ String markerId = getId(marker);
+ Issue issue = registry.getIssue(markerId);
+ if (issue == null) {
+ return "";
+ }
+
+ String summary = issue.getBriefDescription(TextFormat.TEXT);
+ String explanation = issue.getExplanation(TextFormat.TEXT);
+
+ StringBuilder sb = new StringBuilder(summary.length() + explanation.length() + 20);
+ try {
+ sb.append((String) marker.getAttribute(IMarker.MESSAGE));
+ sb.append('\n').append('\n');
+ } catch (CoreException e) {
+ }
+ sb.append("Issue: ");
+ sb.append(summary);
+ sb.append('\n');
+ sb.append("Id: ");
+ sb.append(issue.getId());
+ sb.append('\n').append('\n');
+ sb.append(explanation);
+
+ if (issue.getMoreInfo() != null) {
+ sb.append('\n').append('\n');
+ sb.append(issue.getMoreInfo());
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Returns the id for the given marker
+ *
+ * @param marker the marker to look up
+ * @return the corresponding issue id, or null
+ */
+ public static String getId(IMarker marker) {
+ try {
+ return (String) marker.getAttribute(MARKER_CHECKID_PROPERTY);
+ } catch (CoreException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Shows the given marker in the editor
+ *
+ * @param marker the marker to be shown
+ */
+ public static void showMarker(IMarker marker) {
+ IRegion region = null;
+ try {
+ int start = marker.getAttribute(IMarker.CHAR_START, -1);
+ int end = marker.getAttribute(IMarker.CHAR_END, -1);
+ if (start >= 0 && end >= 0) {
+ region = new org.eclipse.jface.text.Region(start, end - start);
+ }
+
+ IResource resource = marker.getResource();
+ if (resource instanceof IFile) {
+ IEditorPart editor =
+ AdtPlugin.openFile((IFile) resource, region, true /* showEditorTab */);
+ if (editor != null) {
+ IDE.gotoMarker(editor, marker);
+ }
+ }
+ } catch (PartInitException ex) {
+ AdtPlugin.log(ex, null);
+ }
+ }
+
+ /**
+ * Show a dialog with errors for the given file
+ *
+ * @param shell the parent shell to attach the dialog to
+ * @param file the file to show the errors for
+ * @param editor the editor for the file, if known
+ */
+ public static void showErrors(
+ @NonNull Shell shell,
+ @NonNull IFile file,
+ @Nullable IEditorPart editor) {
+ LintListDialog dialog = new LintListDialog(shell, file, editor);
+ dialog.open();
+ }
+
+ @Override
+ public @NonNull String readFile(@NonNull File f) {
+ // Map File to IFile
+ IFile file = AdtUtils.fileToIFile(f);
+ if (file == null || !file.exists()) {
+ String path = f.getPath();
+ AdtPlugin.log(IStatus.ERROR, "Can't find file %1$s in workspace", path);
+ return readPlainFile(f);
+ }
+
+ if (SdkUtils.endsWithIgnoreCase(file.getName(), DOT_XML)) {
+ IStructuredModel model = null;
+ try {
+ IModelManager modelManager = StructuredModelManager.getModelManager();
+ model = modelManager.getModelForRead(file);
+ return model.getStructuredDocument().get();
+ } catch (IOException e) {
+ AdtPlugin.log(e, "Cannot read XML file");
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ } finally {
+ if (model != null) {
+ // TODO: This may be too early...
+ model.releaseFromRead();
+ }
+ }
+ }
+
+ return readPlainFile(f);
+ }
+
+ private String readPlainFile(File file) {
+ try {
+ return LintUtils.getEncodedString(this, file);
+ } catch (IOException e) {
+ return ""; //$NON-NLS-1$
+ }
+ }
+
+ private Map<Project, ClassPathInfo> mProjectInfo;
+
+ @Override
+ @NonNull
+ protected ClassPathInfo getClassPath(@NonNull Project project) {
+ ClassPathInfo info;
+ if (mProjectInfo == null) {
+ mProjectInfo = Maps.newHashMap();
+ info = null;
+ } else {
+ info = mProjectInfo.get(project);
+ }
+
+ if (info == null) {
+ List<File> sources = null;
+ List<File> classes = null;
+ List<File> libraries = null;
+
+ IProject p = getProject(project);
+ if (p != null) {
+ try {
+ IJavaProject javaProject = BaseProjectHelper.getJavaProject(p);
+
+ // Output path
+ File file = workspacePathToFile(javaProject.getOutputLocation());
+ classes = Collections.singletonList(file);
+
+ // Source path
+ IClasspathEntry[] entries = javaProject.getRawClasspath();
+ sources = new ArrayList<File>(entries.length);
+ libraries = new ArrayList<File>(entries.length);
+ for (int i = 0; i < entries.length; i++) {
+ IClasspathEntry entry = entries[i];
+ int kind = entry.getEntryKind();
+
+ if (kind == IClasspathEntry.CPE_VARIABLE) {
+ entry = JavaCore.getResolvedClasspathEntry(entry);
+ if (entry == null) {
+ // It's possible that the variable is no longer valid; ignore
+ continue;
+ }
+ kind = entry.getEntryKind();
+ }
+
+ if (kind == IClasspathEntry.CPE_SOURCE) {
+ sources.add(workspacePathToFile(entry.getPath()));
+ } else if (kind == IClasspathEntry.CPE_LIBRARY) {
+ libraries.add(entry.getPath().toFile());
+ }
+ // Note that we ignore IClasspathEntry.CPE_CONTAINER:
+ // Normal Android Eclipse projects supply both
+ // AdtConstants.CONTAINER_FRAMEWORK
+ // and
+ // AdtConstants.CONTAINER_LIBRARIES
+ // here. We ignore the framework classes for obvious reasons,
+ // but we also ignore the library container because lint will
+ // process the libraries differently. When Eclipse builds a
+ // project, it gets the .jar output of the library projects
+ // from this container, which means it doesn't have to process
+ // the library sources. Lint on the other hand wants to process
+ // the source code, so instead it actually looks at the
+ // project.properties file to find the libraries, and then it
+ // iterates over all the library projects in turn and analyzes
+ // those separately (but passing the main project for context,
+ // such that the including project's manifest declarations
+ // are used for data like minSdkVersion level).
+ //
+ // Note that this container will also contain *other*
+ // libraries (Java libraries, not library projects) that we
+ // *should* include. However, we can't distinguish these
+ // class path entries from the library project jars,
+ // so instead of looking at these, we simply listFiles() in
+ // the libs/ folder after processing the classpath info
+ }
+
+ // Add in libraries
+ File libs = new File(project.getDir(), FD_NATIVE_LIBS);
+ if (libs.isDirectory()) {
+ File[] jars = libs.listFiles();
+ if (jars != null) {
+ for (File jar : jars) {
+ if (SdkUtils.endsWith(jar.getPath(), DOT_JAR)) {
+ libraries.add(jar);
+ }
+ }
+ }
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ if (sources == null) {
+ sources = super.getClassPath(project).getSourceFolders();
+ }
+ if (classes == null) {
+ classes = super.getClassPath(project).getClassFolders();
+ }
+ if (libraries == null) {
+ libraries = super.getClassPath(project).getLibraries();
+ }
+
+
+ // No test folders in Eclipse:
+ // https://bugs.eclipse.org/bugs/show_bug.cgi?id=224708
+ List<File> tests = Collections.emptyList();
+
+ info = new ClassPathInfo(sources, classes, libraries, tests);
+ mProjectInfo.put(project, info);
+ }
+
+ return info;
+ }
+
+ /**
+ * Returns the registry of issues to check from within Eclipse.
+ *
+ * @return the issue registry to use to access detectors and issues
+ */
+ public static IssueRegistry getRegistry() {
+ return new EclipseLintIssueRegistry();
+ }
+
+ @Override
+ public @NonNull Class<? extends Detector> replaceDetector(
+ @NonNull Class<? extends Detector> detectorClass) {
+ return detectorClass;
+ }
+
+ @Override
+ @NonNull
+ public IAndroidTarget[] getTargets() {
+ Sdk sdk = Sdk.getCurrent();
+ if (sdk != null) {
+ return sdk.getTargets();
+ } else {
+ return new IAndroidTarget[0];
+ }
+ }
+
+ private boolean mSearchForSuperClasses;
+
+ /**
+ * Sets whether this client should search for super types on its own. This
+ * is typically not needed when doing a full lint run (because lint will
+ * look at all classes and libraries), but is useful during incremental
+ * analysis when lint is only looking at a subset of classes. In that case,
+ * we want to use Eclipse's data structures for super classes.
+ *
+ * @param search whether to use a custom Eclipse search for super class
+ * names
+ */
+ public void setSearchForSuperClasses(boolean search) {
+ mSearchForSuperClasses = search;
+ }
+
+ /**
+ * Whether this lint client is searching for super types. See
+ * {@link #setSearchForSuperClasses(boolean)} for details.
+ *
+ * @return whether the client will search for super types
+ */
+ public boolean getSearchForSuperClasses() {
+ return mSearchForSuperClasses;
+ }
+
+ @Override
+ @Nullable
+ public String getSuperClass(@NonNull Project project, @NonNull String name) {
+ if (!mSearchForSuperClasses) {
+ // Super type search using the Eclipse index is potentially slow, so
+ // only do this when necessary
+ return null;
+ }
+
+ IProject eclipseProject = getProject(project);
+ if (eclipseProject == null) {
+ return null;
+ }
+
+ try {
+ IJavaProject javaProject = BaseProjectHelper.getJavaProject(eclipseProject);
+ if (javaProject == null) {
+ return null;
+ }
+
+ String typeFqcn = ClassContext.getFqcn(name);
+ IType type = javaProject.findType(typeFqcn);
+ if (type != null) {
+ ITypeHierarchy hierarchy = type.newSupertypeHierarchy(new NullProgressMonitor());
+ IType superType = hierarchy.getSuperclass(type);
+ if (superType != null) {
+ String key = superType.getKey();
+ if (!key.isEmpty()
+ && key.charAt(0) == 'L'
+ && key.charAt(key.length() - 1) == ';') {
+ return key.substring(1, key.length() - 1);
+ } else {
+ String fqcn = superType.getFullyQualifiedName();
+ return ClassContext.getInternalName(fqcn);
+ }
+ }
+ }
+ } catch (JavaModelException e) {
+ log(Severity.INFORMATIONAL, e, null);
+ } catch (CoreException e) {
+ log(Severity.INFORMATIONAL, e, null);
+ }
+
+ return null;
+ }
+
+ @Override
+ @Nullable
+ public Boolean isSubclassOf(
+ @NonNull Project project,
+ @NonNull String name, @NonNull
+ String superClassName) {
+ if (!mSearchForSuperClasses) {
+ // Super type search using the Eclipse index is potentially slow, so
+ // only do this when necessary
+ return null;
+ }
+
+ IProject eclipseProject = getProject(project);
+ if (eclipseProject == null) {
+ return null;
+ }
+
+ try {
+ IJavaProject javaProject = BaseProjectHelper.getJavaProject(eclipseProject);
+ if (javaProject == null) {
+ return null;
+ }
+
+ String typeFqcn = ClassContext.getFqcn(name);
+ IType type = javaProject.findType(typeFqcn);
+ if (type != null) {
+ ITypeHierarchy hierarchy = type.newSupertypeHierarchy(new NullProgressMonitor());
+ IType[] allSupertypes = hierarchy.getAllSuperclasses(type);
+ if (allSupertypes != null) {
+ String target = 'L' + superClassName + ';';
+ for (IType superType : allSupertypes) {
+ if (target.equals(superType.getKey())) {
+ return Boolean.TRUE;
+ }
+ }
+ return Boolean.FALSE;
+ }
+ }
+ } catch (JavaModelException e) {
+ log(Severity.INFORMATIONAL, e, null);
+ } catch (CoreException e) {
+ log(Severity.INFORMATIONAL, e, null);
+ }
+
+ return null;
+ }
+
+ private static class LazyLocation extends Location implements Location.Handle {
+ private final IStructuredDocument mDocument;
+ private final IndexedRegion mRegion;
+ private Position mStart;
+ private Position mEnd;
+
+ public LazyLocation(File file, IStructuredDocument document, IndexedRegion region) {
+ super(file, null /*start*/, null /*end*/);
+ mDocument = document;
+ mRegion = region;
+ }
+
+ @Override
+ public Position getStart() {
+ if (mStart == null) {
+ int line = -1;
+ int column = -1;
+ int offset = mRegion.getStartOffset();
+
+ if (mRegion instanceof org.w3c.dom.Text && mDocument != null) {
+ // For text nodes, skip whitespace prefix, if any
+ for (int i = offset;
+ i < mRegion.getEndOffset() && i < mDocument.getLength(); i++) {
+ try {
+ char c = mDocument.getChar(i);
+ if (!Character.isWhitespace(c)) {
+ offset = i;
+ break;
+ }
+ } catch (BadLocationException e) {
+ break;
+ }
+ }
+ }
+
+ if (mDocument != null && offset < mDocument.getLength()) {
+ line = mDocument.getLineOfOffset(offset);
+ column = -1;
+ try {
+ int lineOffset = mDocument.getLineOffset(line);
+ column = offset - lineOffset;
+ } catch (BadLocationException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ mStart = new DefaultPosition(line, column, offset);
+ }
+
+ return mStart;
+ }
+
+ @Override
+ public Position getEnd() {
+ if (mEnd == null) {
+ mEnd = new DefaultPosition(-1, -1, mRegion.getEndOffset());
+ }
+
+ return mEnd;
+ }
+
+ @Override
+ public @NonNull Location resolve() {
+ return this;
+ }
+ }
+
+ private static class EclipseJavaParser extends JavaParser {
+ private static final boolean USE_ECLIPSE_PARSER = true;
+ private final Parser mParser;
+
+ EclipseJavaParser() {
+ if (USE_ECLIPSE_PARSER) {
+ CompilerOptions options = new CompilerOptions();
+ // Always using JDK 7 rather than basing it on project metadata since we
+ // don't do compilation error validation in lint (we leave that to the IDE's
+ // error parser or the command line build's compilation step); we want an
+ // AST that is as tolerant as possible.
+ options.complianceLevel = ClassFileConstants.JDK1_7;
+ options.sourceLevel = ClassFileConstants.JDK1_7;
+ options.targetJDK = ClassFileConstants.JDK1_7;
+ options.parseLiteralExpressionsAsConstants = true;
+ ProblemReporter problemReporter = new ProblemReporter(
+ DefaultErrorHandlingPolicies.exitOnFirstError(),
+ options,
+ new DefaultProblemFactory());
+ mParser = new Parser(problemReporter, options.parseLiteralExpressionsAsConstants);
+ mParser.javadocParser.checkDocComment = false;
+ } else {
+ mParser = null;
+ }
+ }
+
+ @Override
+ public void prepareJavaParse(@NonNull List<JavaContext> contexts) {
+ // TODO: Use batch compiler from lint-cli.jar
+ }
+
+ @Override
+ public lombok.ast.Node parseJava(@NonNull JavaContext context) {
+ if (USE_ECLIPSE_PARSER) {
+ // Use Eclipse's compiler
+ EcjTreeConverter converter = new EcjTreeConverter();
+ String code = context.getContents();
+
+ CompilationUnit sourceUnit = new CompilationUnit(code.toCharArray(),
+ context.file.getName(), "UTF-8"); //$NON-NLS-1$
+ CompilationResult compilationResult = new CompilationResult(sourceUnit, 0, 0, 0);
+ CompilationUnitDeclaration unit = null;
+ try {
+ unit = mParser.parse(sourceUnit, compilationResult);
+ } catch (AbortCompilation e) {
+ // No need to report Java parsing errors while running in Eclipse.
+ // Eclipse itself will already provide problem markers for these files,
+ // so all this achieves is creating "multiple annotations on this line"
+ // tooltips instead.
+ return null;
+ }
+ if (unit == null) {
+ return null;
+ }
+
+ try {
+ converter.visit(code, unit);
+ List<? extends lombok.ast.Node> nodes = converter.getAll();
+
+ // There could be more than one node when there are errors; pick out the
+ // compilation unit node
+ for (lombok.ast.Node node : nodes) {
+ if (node instanceof lombok.ast.CompilationUnit) {
+ return node;
+ }
+ }
+
+ return null;
+ } catch (Throwable t) {
+ AdtPlugin.log(t, "Failed converting ECJ parse tree to Lombok for file %1$s",
+ context.file.getPath());
+ return null;
+ }
+ } else {
+ // Use Lombok for now
+ Source source = new Source(context.getContents(), context.file.getName());
+ List<lombok.ast.Node> nodes = source.getNodes();
+
+ // Don't analyze files containing errors
+ List<ParseProblem> problems = source.getProblems();
+ if (problems != null && problems.size() > 0) {
+ /* Silently ignore the errors. There are still some bugs in Lombok/Parboiled
+ * (triggered if you run lint on the AOSP framework directory for example),
+ * and having these show up as fatal errors when it's really a tool bug
+ * is bad. To make matters worse, the error messages aren't clear:
+ * http://code.google.com/p/projectlombok/issues/detail?id=313
+ for (ParseProblem problem : problems) {
+ lombok.ast.Position position = problem.getPosition();
+ Location location = Location.create(context.file,
+ context.getContents(), position.getStart(), position.getEnd());
+ String message = problem.getMessage();
+ context.report(
+ IssueRegistry.PARSER_ERROR, location,
+ message,
+ null);
+
+ }
+ */
+ return null;
+ }
+
+ // There could be more than one node when there are errors; pick out the
+ // compilation unit node
+ for (lombok.ast.Node node : nodes) {
+ if (node instanceof lombok.ast.CompilationUnit) {
+ return node;
+ }
+ }
+ return null;
+ }
+ }
+
+ @Override
+ public @NonNull Location getLocation(@NonNull JavaContext context,
+ @NonNull lombok.ast.Node node) {
+ lombok.ast.Position position = node.getPosition();
+ return Location.create(context.file, context.getContents(),
+ position.getStart(), position.getEnd());
+ }
+
+ @Override
+ public @NonNull Handle createLocationHandle(@NonNull JavaContext context,
+ @NonNull lombok.ast.Node node) {
+ return new LocationHandle(context.file, node);
+ }
+
+ @Override
+ public void dispose(@NonNull JavaContext context,
+ @NonNull lombok.ast.Node compilationUnit) {
+ }
+
+ @Override
+ @Nullable
+ public ResolvedNode resolve(@NonNull JavaContext context,
+ @NonNull lombok.ast.Node node) {
+ return null;
+ }
+
+ @Override
+ @Nullable
+ public TypeDescriptor getType(@NonNull JavaContext context,
+ @NonNull lombok.ast.Node node) {
+ return null;
+ }
+
+ /* Handle for creating positions cheaply and returning full fledged locations later */
+ private class LocationHandle implements Handle {
+ private File mFile;
+ private lombok.ast.Node mNode;
+ private Object mClientData;
+
+ public LocationHandle(File file, lombok.ast.Node node) {
+ mFile = file;
+ mNode = node;
+ }
+
+ @Override
+ public @NonNull Location resolve() {
+ lombok.ast.Position pos = mNode.getPosition();
+ return Location.create(mFile, null /*contents*/, pos.getStart(), pos.getEnd());
+ }
+
+ @Override
+ public void setClientData(@Nullable Object clientData) {
+ mClientData = clientData;
+ }
+
+ @Override
+ @Nullable
+ public Object getClientData() {
+ return mClientData;
+ }
+ }
+ }
+}
+