| /* |
| * 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.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) { |
| 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(); |
| } |
| |
| info = new ClassPathInfo(sources, classes, libraries); |
| 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; |
| } |
| } |
| } |
| } |
| |