blob: 3dd424087ac8337b88524f61a5068c155fe9f97d [file] [log] [blame]
/*
* 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;
}
}
}
}