diff options
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.java | 1306 |
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; + } + } + } +} + |