/* * 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.IDomParser; import com.android.tools.lint.client.api.IJavaParser; import com.android.tools.lint.client.api.IssueRegistry; import com.android.tools.lint.client.api.LintClient; 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.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 implements IDomParser { 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 IDomParser getDomParser() { return this; } @Override public IJavaParser getJavaParser() { if (mJavaParser == null) { mJavaParser = new EclipseJavaParser(); } return mJavaParser; } // ----- Implements IDomParser ----- @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; } // 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, @Nullable Object data) { 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.getDescription(); String explanation = issue.getExplanationAsSimpleText(); 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$ } } @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 @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); } 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 BuiltinIssueRegistry(); } @Override public @NonNull Class<? extends Detector> replaceDetector( @NonNull Class<? extends Detector> detectorClass) { return detectorClass; } @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 IAndroidTarget[] getTargets() { return Sdk.getCurrent().getTargets(); } 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 implements IJavaParser { private static final boolean USE_ECLIPSE_PARSER = true; private final Parser mParser; EclipseJavaParser() { if (USE_ECLIPSE_PARSER) { CompilerOptions options = new CompilerOptions(); // Read settings from project? Note that this doesn't really matter because // we will only be parsing, not actually compiling. options.complianceLevel = ClassFileConstants.JDK1_6; options.sourceLevel = ClassFileConstants.JDK1_6; options.targetJDK = ClassFileConstants.JDK1_6; 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 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) { } /* 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; } } } }