• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.eclipse.org/org/documents/epl-v10.php
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.ide.eclipse.adt.internal.lint;
17 
18 import static com.android.SdkConstants.DOT_JAR;
19 import static com.android.SdkConstants.DOT_XML;
20 import static com.android.SdkConstants.FD_NATIVE_LIBS;
21 import static com.android.ide.eclipse.adt.AdtConstants.MARKER_LINT;
22 import static com.android.ide.eclipse.adt.AdtUtils.workspacePathToFile;
23 
24 import com.android.annotations.NonNull;
25 import com.android.annotations.Nullable;
26 import com.android.ide.eclipse.adt.AdtPlugin;
27 import com.android.ide.eclipse.adt.AdtUtils;
28 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
29 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
30 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
31 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
32 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
33 import com.android.sdklib.IAndroidTarget;
34 import com.android.tools.lint.checks.BuiltinIssueRegistry;
35 import com.android.tools.lint.client.api.Configuration;
36 import com.android.tools.lint.client.api.IDomParser;
37 import com.android.tools.lint.client.api.IJavaParser;
38 import com.android.tools.lint.client.api.IssueRegistry;
39 import com.android.tools.lint.client.api.LintClient;
40 import com.android.tools.lint.detector.api.ClassContext;
41 import com.android.tools.lint.detector.api.Context;
42 import com.android.tools.lint.detector.api.DefaultPosition;
43 import com.android.tools.lint.detector.api.Detector;
44 import com.android.tools.lint.detector.api.Issue;
45 import com.android.tools.lint.detector.api.JavaContext;
46 import com.android.tools.lint.detector.api.LintUtils;
47 import com.android.tools.lint.detector.api.Location;
48 import com.android.tools.lint.detector.api.Location.Handle;
49 import com.android.tools.lint.detector.api.Position;
50 import com.android.tools.lint.detector.api.Project;
51 import com.android.tools.lint.detector.api.Severity;
52 import com.android.tools.lint.detector.api.XmlContext;
53 import com.android.utils.Pair;
54 import com.android.utils.SdkUtils;
55 import com.google.common.collect.Maps;
56 
57 import org.eclipse.core.resources.IFile;
58 import org.eclipse.core.resources.IMarker;
59 import org.eclipse.core.resources.IProject;
60 import org.eclipse.core.resources.IResource;
61 import org.eclipse.core.runtime.CoreException;
62 import org.eclipse.core.runtime.IStatus;
63 import org.eclipse.core.runtime.NullProgressMonitor;
64 import org.eclipse.jdt.core.IClasspathEntry;
65 import org.eclipse.jdt.core.IJavaProject;
66 import org.eclipse.jdt.core.IType;
67 import org.eclipse.jdt.core.ITypeHierarchy;
68 import org.eclipse.jdt.core.JavaCore;
69 import org.eclipse.jdt.core.JavaModelException;
70 import org.eclipse.jdt.internal.compiler.CompilationResult;
71 import org.eclipse.jdt.internal.compiler.DefaultErrorHandlingPolicies;
72 import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
73 import org.eclipse.jdt.internal.compiler.batch.CompilationUnit;
74 import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants;
75 import org.eclipse.jdt.internal.compiler.impl.CompilerOptions;
76 import org.eclipse.jdt.internal.compiler.parser.Parser;
77 import org.eclipse.jdt.internal.compiler.problem.AbortCompilation;
78 import org.eclipse.jdt.internal.compiler.problem.DefaultProblemFactory;
79 import org.eclipse.jdt.internal.compiler.problem.ProblemReporter;
80 import org.eclipse.jface.text.BadLocationException;
81 import org.eclipse.jface.text.IDocument;
82 import org.eclipse.jface.text.IRegion;
83 import org.eclipse.swt.widgets.Shell;
84 import org.eclipse.ui.IEditorPart;
85 import org.eclipse.ui.PartInitException;
86 import org.eclipse.ui.editors.text.TextFileDocumentProvider;
87 import org.eclipse.ui.ide.IDE;
88 import org.eclipse.ui.texteditor.IDocumentProvider;
89 import org.eclipse.wst.sse.core.StructuredModelManager;
90 import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
91 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
92 import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
93 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
94 import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
95 import org.w3c.dom.Attr;
96 import org.w3c.dom.Document;
97 import org.w3c.dom.Node;
98 
99 import java.io.File;
100 import java.io.IOException;
101 import java.util.ArrayList;
102 import java.util.Collection;
103 import java.util.Collections;
104 import java.util.List;
105 import java.util.Map;
106 import java.util.WeakHashMap;
107 
108 import lombok.ast.TypeReference;
109 import lombok.ast.ecj.EcjTreeConverter;
110 import lombok.ast.grammar.ParseProblem;
111 import lombok.ast.grammar.Source;
112 
113 /**
114  * Eclipse implementation for running lint on workspace files and projects.
115  */
116 @SuppressWarnings("restriction") // DOM model
117 public class EclipseLintClient extends LintClient implements IDomParser {
118     static final String MARKER_CHECKID_PROPERTY = "checkid";    //$NON-NLS-1$
119     private static final String MODEL_PROPERTY = "model";       //$NON-NLS-1$
120     private final List<? extends IResource> mResources;
121     private final IDocument mDocument;
122     private boolean mWasFatal;
123     private boolean mFatalOnly;
124     private EclipseJavaParser mJavaParser;
125     private boolean mCollectNodes;
126     private Map<Node, IMarker> mNodeMap;
127 
128     /**
129      * Creates a new {@link EclipseLintClient}.
130      *
131      * @param registry the associated detector registry
132      * @param resources the associated resources (project, file or null)
133      * @param document the associated document, or null if the {@code resource}
134      *            param is not a file
135      * @param fatalOnly whether only fatal issues should be reported (and therefore checked)
136      */
EclipseLintClient(IssueRegistry registry, List<? extends IResource> resources, IDocument document, boolean fatalOnly)137     public EclipseLintClient(IssueRegistry registry, List<? extends IResource> resources,
138             IDocument document, boolean fatalOnly) {
139         mResources = resources;
140         mDocument = document;
141         mFatalOnly = fatalOnly;
142     }
143 
144     /**
145      * Returns true if lint should only check fatal issues
146      *
147      * @return true if lint should only check fatal issues
148      */
isFatalOnly()149     public boolean isFatalOnly() {
150         return mFatalOnly;
151     }
152 
153     /**
154      * Sets whether the lint client should store associated XML nodes for each
155      * reported issue
156      *
157      * @param collectNodes if true, collect node positions for errors in XML
158      *            files, retrievable via the {@link #getIssueForNode} method
159      */
setCollectNodes(boolean collectNodes)160     public void setCollectNodes(boolean collectNodes) {
161         mCollectNodes = collectNodes;
162     }
163 
164     /**
165      * Returns one of the issues for the given node (there could be more than one)
166      *
167      * @param node the node to look up lint issues for
168      * @return the marker for one of the issues found for the given node
169      */
170     @Nullable
getIssueForNode(@onNull UiViewElementNode node)171     public IMarker getIssueForNode(@NonNull UiViewElementNode node) {
172         if (mNodeMap != null) {
173             return mNodeMap.get(node.getXmlNode());
174         }
175 
176         return null;
177     }
178 
179     /**
180      * Returns a collection of nodes that have one or more lint warnings
181      * associated with them (retrievable via
182      * {@link #getIssueForNode(UiViewElementNode)})
183      *
184      * @return a collection of nodes, which should <b>not</b> be modified by the
185      *         caller
186      */
187     @Nullable
getIssueNodes()188     public Collection<Node> getIssueNodes() {
189         if (mNodeMap != null) {
190             return mNodeMap.keySet();
191         }
192 
193         return null;
194     }
195 
196     // ----- Extends LintClient -----
197 
198     @Override
log(@onNull Severity severity, @Nullable Throwable exception, @Nullable String format, @Nullable Object... args)199     public void log(@NonNull Severity severity, @Nullable Throwable exception,
200             @Nullable String format, @Nullable Object... args) {
201         if (exception == null) {
202             AdtPlugin.log(IStatus.WARNING, format, args);
203         } else {
204             AdtPlugin.log(exception, format, args);
205         }
206     }
207 
208     @Override
getDomParser()209     public IDomParser getDomParser() {
210         return this;
211     }
212 
213     @Override
getJavaParser()214     public IJavaParser getJavaParser() {
215         if (mJavaParser == null) {
216             mJavaParser = new EclipseJavaParser();
217         }
218 
219         return mJavaParser;
220     }
221 
222     // ----- Implements IDomParser -----
223 
224     @Override
parseXml(@onNull XmlContext context)225     public Document parseXml(@NonNull XmlContext context) {
226         // Map File to IFile
227         IFile file = AdtUtils.fileToIFile(context.file);
228         if (file == null || !file.exists()) {
229             String path = context.file.getPath();
230             AdtPlugin.log(IStatus.ERROR, "Can't find file %1$s in workspace", path);
231             return null;
232         }
233 
234         IStructuredModel model = null;
235         try {
236             IModelManager modelManager = StructuredModelManager.getModelManager();
237             if (modelManager == null) {
238                 // This can happen if incremental lint is running right as Eclipse is shutting down
239                 return null;
240             }
241             model = modelManager.getModelForRead(file);
242             if (model instanceof IDOMModel) {
243                 context.setProperty(MODEL_PROPERTY, model);
244                 IDOMModel domModel = (IDOMModel) model;
245                 return domModel.getDocument();
246             }
247         } catch (IOException e) {
248             AdtPlugin.log(e, "Cannot read XML file");
249         } catch (CoreException e) {
250             AdtPlugin.log(e, null);
251         }
252 
253         return null;
254     }
255 
256     // Cache for {@link getProject}
257     private IProject mLastEclipseProject;
258     private Project mLastLintProject;
259 
getProject(Project project)260     private IProject getProject(Project project) {
261         if (project == mLastLintProject) {
262             return mLastEclipseProject;
263         }
264 
265         mLastLintProject = project;
266         mLastEclipseProject = null;
267 
268         if (mResources != null) {
269             if (mResources.size() == 1) {
270                 IProject p = mResources.get(0).getProject();
271                 mLastEclipseProject = p;
272                 return p;
273             }
274 
275             IProject last = null;
276             for (IResource resource : mResources) {
277                 IProject p = resource.getProject();
278                 if (p != last) {
279                     if (project.getDir().equals(AdtUtils.getAbsolutePath(p).toFile())) {
280                         mLastEclipseProject = p;
281                         return p;
282                     }
283                     last = p;
284                 }
285             }
286         }
287 
288         return null;
289     }
290 
291     @Override
292     @NonNull
getProjectName(@onNull Project project)293     public String getProjectName(@NonNull Project project) {
294         // Initialize the lint project's name to the name of the Eclipse project,
295         // which might differ from the directory name
296         IProject eclipseProject = getProject(project);
297         if (eclipseProject != null) {
298             return eclipseProject.getName();
299         }
300 
301         return super.getProjectName(project);
302     }
303 
304     @NonNull
305     @Override
getConfiguration(@onNull Project project)306     public Configuration getConfiguration(@NonNull Project project) {
307         return getConfigurationFor(project);
308     }
309 
310     /**
311      * Same as {@link #getConfiguration(Project)}, but {@code project} can be
312      * null in which case the global configuration is returned.
313      *
314      * @param project the project to look up
315      * @return a corresponding configuration
316      */
317     @NonNull
getConfigurationFor(@ullable Project project)318     public Configuration getConfigurationFor(@Nullable Project project) {
319         if (project != null) {
320             IProject eclipseProject = getProject(project);
321             if (eclipseProject != null) {
322                 return ProjectLintConfiguration.get(this, eclipseProject, mFatalOnly);
323             }
324         }
325 
326         return GlobalLintConfiguration.get();
327     }
328     @Override
report(@onNull Context context, @NonNull Issue issue, @NonNull Severity s, @Nullable Location location, @NonNull String message, @Nullable Object data)329     public void report(@NonNull Context context, @NonNull Issue issue, @NonNull Severity s,
330             @Nullable Location location,
331             @NonNull String message, @Nullable Object data) {
332         int severity = getMarkerSeverity(s);
333         IMarker marker = null;
334         if (location != null) {
335             Position startPosition = location.getStart();
336             if (startPosition == null) {
337                 if (location.getFile() != null) {
338                     IResource resource = AdtUtils.fileToResource(location.getFile());
339                     if (resource != null && resource.isAccessible()) {
340                         marker = BaseProjectHelper.markResource(resource, MARKER_LINT,
341                                 message, 0, severity);
342                     }
343                 }
344             } else {
345                 Position endPosition = location.getEnd();
346                 int line = startPosition.getLine() + 1; // Marker API is 1-based
347                 IFile file = AdtUtils.fileToIFile(location.getFile());
348                 if (file != null && file.isAccessible()) {
349                     Pair<Integer, Integer> r = getRange(file, mDocument,
350                             startPosition, endPosition);
351                     int startOffset = r.getFirst();
352                     int endOffset = r.getSecond();
353                     marker = BaseProjectHelper.markResource(file, MARKER_LINT,
354                             message, line, startOffset, endOffset, severity);
355                 }
356             }
357         }
358 
359         if (marker == null) {
360             marker = BaseProjectHelper.markResource(mResources.get(0), MARKER_LINT,
361                         message, 0, severity);
362         }
363 
364         if (marker != null) {
365             // Store marker id such that we can recognize it from the suppress quickfix
366             try {
367                 marker.setAttribute(MARKER_CHECKID_PROPERTY, issue.getId());
368             } catch (CoreException e) {
369                 AdtPlugin.log(e, null);
370             }
371         }
372 
373         if (s == Severity.FATAL) {
374             mWasFatal = true;
375         }
376 
377         if (mCollectNodes && location != null && marker != null) {
378             if (location instanceof LazyLocation) {
379                 LazyLocation l = (LazyLocation) location;
380                 IndexedRegion region = l.mRegion;
381                 if (region instanceof Node) {
382                     Node node = (Node) region;
383                     if (node instanceof Attr) {
384                         node = ((Attr) node).getOwnerElement();
385                     }
386                     if (mNodeMap == null) {
387                         mNodeMap = new WeakHashMap<Node, IMarker>();
388                     }
389                     IMarker prev = mNodeMap.get(node);
390                     if (prev != null) {
391                         // Only replace the node if this node has higher priority
392                         int prevSeverity = prev.getAttribute(IMarker.SEVERITY, 0);
393                         if (prevSeverity < severity) {
394                             mNodeMap.put(node, marker);
395                         }
396                     } else {
397                         mNodeMap.put(node, marker);
398                     }
399                 }
400             }
401         }
402     }
403 
404     @Override
405     @Nullable
findResource(@onNull String relativePath)406     public File findResource(@NonNull String relativePath) {
407         // Look within the $ANDROID_SDK
408         String sdkFolder = AdtPrefs.getPrefs().getOsSdkFolder();
409         if (sdkFolder != null) {
410             File file = new File(sdkFolder, relativePath);
411             if (file.exists()) {
412                 return file;
413             }
414         }
415 
416         return null;
417     }
418 
419     /**
420      * Clears any lint markers from the given resource (project, folder or file)
421      *
422      * @param resource the resource to remove markers from
423      */
clearMarkers(@onNull IResource resource)424     public static void clearMarkers(@NonNull IResource resource) {
425         clearMarkers(Collections.singletonList(resource));
426     }
427 
428     /** Clears any lint markers from the given list of resource (project, folder or file) */
clearMarkers(List<? extends IResource> resources)429     static void clearMarkers(List<? extends IResource> resources) {
430         for (IResource resource : resources) {
431             try {
432                 if (resource.isAccessible()) {
433                     resource.deleteMarkers(MARKER_LINT, false, IResource.DEPTH_INFINITE);
434                 }
435             } catch (CoreException e) {
436                 AdtPlugin.log(e, null);
437             }
438         }
439 
440         IEditorPart activeEditor = AdtUtils.getActiveEditor();
441         LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(activeEditor);
442         if (delegate != null) {
443             delegate.getGraphicalEditor().getLayoutActionBar().updateErrorIndicator();
444         }
445     }
446 
447     /**
448      * Removes all markers of the given id from the given resource.
449      *
450      * @param resource the resource to remove markers from (file or project, or
451      *            null for all open projects)
452      * @param id the id for the issue whose markers should be deleted
453      */
removeMarkers(IResource resource, String id)454     public static void removeMarkers(IResource resource, String id) {
455         if (resource == null) {
456             IJavaProject[] androidProjects = BaseProjectHelper.getAndroidProjects(null);
457             for (IJavaProject project : androidProjects) {
458                 IProject p = project.getProject();
459                 if (p != null) {
460                     // Recurse, but with a different parameter so it will not continue recursing
461                     removeMarkers(p, id);
462                 }
463             }
464             return;
465         }
466         IMarker[] markers = getMarkers(resource);
467         for (IMarker marker : markers) {
468             if (id.equals(getId(marker))) {
469                 try {
470                     marker.delete();
471                 } catch (CoreException e) {
472                     AdtPlugin.log(e, null);
473                 }
474             }
475         }
476     }
477 
478     /**
479      * Returns the lint marker for the given resource (which may be a project, folder or file)
480      *
481      * @param resource the resource to be checked, typically a source file
482      * @return an array of markers, possibly empty but never null
483      */
getMarkers(IResource resource)484     public static IMarker[] getMarkers(IResource resource) {
485         try {
486             if (resource.isAccessible()) {
487                 return resource.findMarkers(MARKER_LINT, false, IResource.DEPTH_INFINITE);
488             }
489         } catch (CoreException e) {
490             AdtPlugin.log(e, null);
491         }
492 
493         return new IMarker[0];
494     }
495 
getMarkerSeverity(Severity severity)496     private static int getMarkerSeverity(Severity severity) {
497         switch (severity) {
498             case INFORMATIONAL:
499                 return IMarker.SEVERITY_INFO;
500             case WARNING:
501                 return IMarker.SEVERITY_WARNING;
502             case FATAL:
503             case ERROR:
504             default:
505                 return IMarker.SEVERITY_ERROR;
506         }
507     }
508 
getRange(IFile file, IDocument doc, Position startPosition, Position endPosition)509     private static Pair<Integer, Integer> getRange(IFile file, IDocument doc,
510             Position startPosition, Position endPosition) {
511         int startOffset = startPosition.getOffset();
512         int endOffset = endPosition != null ? endPosition.getOffset() : -1;
513         if (endOffset != -1) {
514             // Attribute ranges often include trailing whitespace; trim this up
515             if (doc == null) {
516                 IDocumentProvider provider = new TextFileDocumentProvider();
517                 try {
518                     provider.connect(file);
519                     doc = provider.getDocument(file);
520                     if (doc != null) {
521                         return adjustOffsets(doc, startOffset, endOffset);
522                     }
523                 } catch (Exception e) {
524                     AdtPlugin.log(e, "Can't find range information for %1$s", file.getName());
525                 } finally {
526                     provider.disconnect(file);
527                 }
528             } else {
529                 return adjustOffsets(doc, startOffset, endOffset);
530             }
531         }
532 
533         return Pair.of(startOffset, startOffset);
534     }
535 
536     /**
537      * Trim off any trailing space on the given offset range in the given
538      * document, and don't span multiple lines on ranges since it makes (for
539      * example) the XML editor just glow with yellow underlines for all the
540      * attributes etc. Highlighting just the element beginning gets the point
541      * across. It also makes it more obvious where there are warnings on both
542      * the overall element and on individual attributes since without this the
543      * warnings on attributes would just overlap with the whole-element
544      * highlighting.
545      */
adjustOffsets(IDocument doc, int startOffset, int endOffset)546     private static Pair<Integer, Integer> adjustOffsets(IDocument doc, int startOffset,
547             int endOffset) {
548         int originalStart = startOffset;
549         int originalEnd = endOffset;
550 
551         if (doc != null) {
552             while (endOffset > startOffset && endOffset < doc.getLength()) {
553                 try {
554                     if (!Character.isWhitespace(doc.getChar(endOffset - 1))) {
555                         break;
556                     } else {
557                         endOffset--;
558                     }
559                 } catch (BadLocationException e) {
560                     // Pass - we've already validated offset range above
561                     break;
562                 }
563             }
564 
565             // Also don't span lines
566             int lineEnd = startOffset;
567             while (lineEnd < endOffset) {
568                 try {
569                     char c = doc.getChar(lineEnd);
570                     if (c == '\n' || c == '\r') {
571                         endOffset = lineEnd;
572                         if (endOffset > 0 && doc.getChar(endOffset - 1) == '\r') {
573                             endOffset--;
574                         }
575                         break;
576                     }
577                 } catch (BadLocationException e) {
578                     // Pass - we've already validated offset range above
579                     break;
580                 }
581                 lineEnd++;
582             }
583         }
584 
585         if (startOffset >= endOffset) {
586             // Selecting nothing (for example, for the mangled CRLF delimiter issue selecting
587             // just the newline)
588             // In that case, use the real range
589             return Pair.of(originalStart, originalEnd);
590         }
591 
592         return Pair.of(startOffset, endOffset);
593     }
594 
595     /**
596      * Returns true if a fatal error was encountered
597      *
598      * @return true if a fatal error was encountered
599      */
hasFatalErrors()600     public boolean hasFatalErrors() {
601         return mWasFatal;
602     }
603 
604     /**
605      * Describe the issue for the given marker
606      *
607      * @param marker the marker to look up
608      * @return a full description of the corresponding issue, never null
609      */
describe(IMarker marker)610     public static String describe(IMarker marker) {
611         IssueRegistry registry = getRegistry();
612         String markerId = getId(marker);
613         Issue issue = registry.getIssue(markerId);
614         if (issue == null) {
615             return "";
616         }
617 
618         String summary = issue.getDescription(Issue.OutputFormat.TEXT);
619         String explanation = issue.getExplanation(Issue.OutputFormat.TEXT);
620 
621         StringBuilder sb = new StringBuilder(summary.length() + explanation.length() + 20);
622         try {
623             sb.append((String) marker.getAttribute(IMarker.MESSAGE));
624             sb.append('\n').append('\n');
625         } catch (CoreException e) {
626         }
627         sb.append("Issue: ");
628         sb.append(summary);
629         sb.append('\n');
630         sb.append("Id: ");
631         sb.append(issue.getId());
632         sb.append('\n').append('\n');
633         sb.append(explanation);
634 
635         if (issue.getMoreInfo() != null) {
636             sb.append('\n').append('\n');
637             sb.append(issue.getMoreInfo());
638         }
639 
640         return sb.toString();
641     }
642 
643     /**
644      * Returns the id for the given marker
645      *
646      * @param marker the marker to look up
647      * @return the corresponding issue id, or null
648      */
getId(IMarker marker)649     public static String getId(IMarker marker) {
650         try {
651             return (String) marker.getAttribute(MARKER_CHECKID_PROPERTY);
652         } catch (CoreException e) {
653             return null;
654         }
655     }
656 
657     /**
658      * Shows the given marker in the editor
659      *
660      * @param marker the marker to be shown
661      */
showMarker(IMarker marker)662     public static void showMarker(IMarker marker) {
663         IRegion region = null;
664         try {
665             int start = marker.getAttribute(IMarker.CHAR_START, -1);
666             int end = marker.getAttribute(IMarker.CHAR_END, -1);
667             if (start >= 0 && end >= 0) {
668                 region = new org.eclipse.jface.text.Region(start, end - start);
669             }
670 
671             IResource resource = marker.getResource();
672             if (resource instanceof IFile) {
673                 IEditorPart editor =
674                         AdtPlugin.openFile((IFile) resource, region, true /* showEditorTab */);
675                 if (editor != null) {
676                     IDE.gotoMarker(editor, marker);
677                 }
678             }
679         } catch (PartInitException ex) {
680             AdtPlugin.log(ex, null);
681         }
682     }
683 
684     /**
685      * Show a dialog with errors for the given file
686      *
687      * @param shell the parent shell to attach the dialog to
688      * @param file the file to show the errors for
689      * @param editor the editor for the file, if known
690      */
showErrors( @onNull Shell shell, @NonNull IFile file, @Nullable IEditorPart editor)691     public static void showErrors(
692             @NonNull Shell shell,
693             @NonNull IFile file,
694             @Nullable IEditorPart editor) {
695         LintListDialog dialog = new LintListDialog(shell, file, editor);
696         dialog.open();
697     }
698 
699     @Override
readFile(@onNull File f)700     public @NonNull String readFile(@NonNull File f) {
701         // Map File to IFile
702         IFile file = AdtUtils.fileToIFile(f);
703         if (file == null || !file.exists()) {
704             String path = f.getPath();
705             AdtPlugin.log(IStatus.ERROR, "Can't find file %1$s in workspace", path);
706             return readPlainFile(f);
707         }
708 
709         if (SdkUtils.endsWithIgnoreCase(file.getName(), DOT_XML)) {
710             IStructuredModel model = null;
711             try {
712                 IModelManager modelManager = StructuredModelManager.getModelManager();
713                 model = modelManager.getModelForRead(file);
714                 return model.getStructuredDocument().get();
715             } catch (IOException e) {
716                 AdtPlugin.log(e, "Cannot read XML file");
717             } catch (CoreException e) {
718                 AdtPlugin.log(e, null);
719             } finally {
720                 if (model != null) {
721                     // TODO: This may be too early...
722                     model.releaseFromRead();
723                 }
724             }
725         }
726 
727         return readPlainFile(f);
728     }
729 
readPlainFile(File file)730     private String readPlainFile(File file) {
731         try {
732             return LintUtils.getEncodedString(this, file);
733         } catch (IOException e) {
734             return ""; //$NON-NLS-1$
735         }
736     }
737 
738     @Override
getLocation(@onNull XmlContext context, @NonNull Node node)739     public @NonNull Location getLocation(@NonNull XmlContext context, @NonNull Node node) {
740         IStructuredModel model = (IStructuredModel) context.getProperty(MODEL_PROPERTY);
741         return new LazyLocation(context.file, model.getStructuredDocument(), (IndexedRegion) node);
742     }
743 
744     @Override
getLocation(@onNull XmlContext context, @NonNull Node node, int start, int end)745     public @NonNull Location getLocation(@NonNull XmlContext context, @NonNull Node node,
746             int start, int end) {
747         IndexedRegion region = (IndexedRegion) node;
748         int nodeStart = region.getStartOffset();
749 
750         IStructuredModel model = (IStructuredModel) context.getProperty(MODEL_PROPERTY);
751         // Get line number
752         LazyLocation location = new LazyLocation(context.file, model.getStructuredDocument(),
753                 region);
754         int line = location.getStart().getLine();
755 
756         Position startPos = new DefaultPosition(line, -1, nodeStart + start);
757         Position endPos = new DefaultPosition(line, -1, nodeStart + end);
758         return Location.create(context.file, startPos, endPos);
759     }
760 
761     @Override
createLocationHandle(final @NonNull XmlContext context, final @NonNull Node node)762     public @NonNull Handle createLocationHandle(final @NonNull XmlContext context,
763             final @NonNull Node node) {
764         IStructuredModel model = (IStructuredModel) context.getProperty(MODEL_PROPERTY);
765         return new LazyLocation(context.file, model.getStructuredDocument(), (IndexedRegion) node);
766     }
767 
768     private Map<Project, ClassPathInfo> mProjectInfo;
769 
770     @Override
771     @NonNull
getClassPath(@onNull Project project)772     protected ClassPathInfo getClassPath(@NonNull Project project) {
773         ClassPathInfo info;
774         if (mProjectInfo == null) {
775             mProjectInfo = Maps.newHashMap();
776             info = null;
777         } else {
778             info = mProjectInfo.get(project);
779         }
780 
781         if (info == null) {
782             List<File> sources = null;
783             List<File> classes = null;
784             List<File> libraries = null;
785 
786             IProject p = getProject(project);
787             if (p != null) {
788                 try {
789                     IJavaProject javaProject = BaseProjectHelper.getJavaProject(p);
790 
791                     // Output path
792                     File file = workspacePathToFile(javaProject.getOutputLocation());
793                     classes = Collections.singletonList(file);
794 
795                     // Source path
796                     IClasspathEntry[] entries = javaProject.getRawClasspath();
797                     sources = new ArrayList<File>(entries.length);
798                     libraries = new ArrayList<File>(entries.length);
799                     for (int i = 0; i < entries.length; i++) {
800                         IClasspathEntry entry = entries[i];
801                         int kind = entry.getEntryKind();
802 
803                         if (kind == IClasspathEntry.CPE_VARIABLE) {
804                             entry = JavaCore.getResolvedClasspathEntry(entry);
805                             if (entry == null) {
806                                 // It's possible that the variable is no longer valid; ignore
807                                 continue;
808                             }
809                             kind = entry.getEntryKind();
810                         }
811 
812                         if (kind == IClasspathEntry.CPE_SOURCE) {
813                             sources.add(workspacePathToFile(entry.getPath()));
814                         } else if (kind == IClasspathEntry.CPE_LIBRARY) {
815                             libraries.add(entry.getPath().toFile());
816                         }
817                         // Note that we ignore IClasspathEntry.CPE_CONTAINER:
818                         // Normal Android Eclipse projects supply both
819                         //   AdtConstants.CONTAINER_FRAMEWORK
820                         // and
821                         //   AdtConstants.CONTAINER_LIBRARIES
822                         // here. We ignore the framework classes for obvious reasons,
823                         // but we also ignore the library container because lint will
824                         // process the libraries differently. When Eclipse builds a
825                         // project, it gets the .jar output of the library projects
826                         // from this container, which means it doesn't have to process
827                         // the library sources. Lint on the other hand wants to process
828                         // the source code, so instead it actually looks at the
829                         // project.properties file to find the libraries, and then it
830                         // iterates over all the library projects in turn and analyzes
831                         // those separately (but passing the main project for context,
832                         // such that the including project's manifest declarations
833                         // are used for data like minSdkVersion level).
834                         //
835                         // Note that this container will also contain *other*
836                         // libraries (Java libraries, not library projects) that we
837                         // *should* include. However, we can't distinguish these
838                         // class path entries from the library project jars,
839                         // so instead of looking at these, we simply listFiles() in
840                         // the libs/ folder after processing the classpath info
841                     }
842 
843                     // Add in libraries
844                     File libs = new File(project.getDir(), FD_NATIVE_LIBS);
845                     if (libs.isDirectory()) {
846                         File[] jars = libs.listFiles();
847                         if (jars != null) {
848                             for (File jar : jars) {
849                                 if (SdkUtils.endsWith(jar.getPath(), DOT_JAR)) {
850                                     libraries.add(jar);
851                                 }
852                             }
853                         }
854                     }
855                 } catch (CoreException e) {
856                     AdtPlugin.log(e, null);
857                 }
858             }
859 
860             if (sources == null) {
861                 sources = super.getClassPath(project).getSourceFolders();
862             }
863             if (classes == null) {
864                 classes = super.getClassPath(project).getClassFolders();
865             }
866             if (libraries == null) {
867                 libraries = super.getClassPath(project).getLibraries();
868             }
869 
870             info = new ClassPathInfo(sources, classes, libraries);
871             mProjectInfo.put(project, info);
872         }
873 
874         return info;
875     }
876 
877     /**
878      * Returns the registry of issues to check from within Eclipse.
879      *
880      * @return the issue registry to use to access detectors and issues
881      */
getRegistry()882     public static IssueRegistry getRegistry() {
883         return new BuiltinIssueRegistry();
884     }
885 
886     @Override
replaceDetector( @onNull Class<? extends Detector> detectorClass)887     public @NonNull Class<? extends Detector> replaceDetector(
888             @NonNull Class<? extends Detector> detectorClass) {
889         return detectorClass;
890     }
891 
892     @Override
dispose(@onNull XmlContext context, @NonNull Document document)893     public void dispose(@NonNull XmlContext context, @NonNull Document document) {
894         IStructuredModel model = (IStructuredModel) context.getProperty(MODEL_PROPERTY);
895         assert model != null : context.file;
896         if (model != null) {
897             model.releaseFromRead();
898         }
899     }
900 
901     @Override
902     @NonNull
getTargets()903     public IAndroidTarget[] getTargets() {
904         return Sdk.getCurrent().getTargets();
905     }
906 
907     private boolean mSearchForSuperClasses;
908 
909     /**
910      * Sets whether this client should search for super types on its own. This
911      * is typically not needed when doing a full lint run (because lint will
912      * look at all classes and libraries), but is useful during incremental
913      * analysis when lint is only looking at a subset of classes. In that case,
914      * we want to use Eclipse's data structures for super classes.
915      *
916      * @param search whether to use a custom Eclipse search for super class
917      *            names
918      */
setSearchForSuperClasses(boolean search)919     public void setSearchForSuperClasses(boolean search) {
920         mSearchForSuperClasses = search;
921     }
922 
923     /**
924      * Whether this lint client is searching for super types. See
925      * {@link #setSearchForSuperClasses(boolean)} for details.
926      *
927      * @return whether the client will search for super types
928      */
getSearchForSuperClasses()929     public boolean getSearchForSuperClasses() {
930         return mSearchForSuperClasses;
931     }
932 
933     @Override
934     @Nullable
getSuperClass(@onNull Project project, @NonNull String name)935     public String getSuperClass(@NonNull Project project, @NonNull String name) {
936         if (!mSearchForSuperClasses) {
937             // Super type search using the Eclipse index is potentially slow, so
938             // only do this when necessary
939             return null;
940         }
941 
942         IProject eclipseProject = getProject(project);
943         if (eclipseProject == null) {
944             return null;
945         }
946 
947         try {
948             IJavaProject javaProject = BaseProjectHelper.getJavaProject(eclipseProject);
949             if (javaProject == null) {
950                 return null;
951             }
952 
953             String typeFqcn = ClassContext.getFqcn(name);
954             IType type = javaProject.findType(typeFqcn);
955             if (type != null) {
956                 ITypeHierarchy hierarchy = type.newSupertypeHierarchy(new NullProgressMonitor());
957                 IType superType = hierarchy.getSuperclass(type);
958                 if (superType != null) {
959                     String key = superType.getKey();
960                     if (!key.isEmpty()
961                             && key.charAt(0) == 'L'
962                             && key.charAt(key.length() - 1) == ';') {
963                         return key.substring(1, key.length() - 1);
964                     } else {
965                         String fqcn = superType.getFullyQualifiedName();
966                         return ClassContext.getInternalName(fqcn);
967                     }
968                 }
969             }
970         } catch (JavaModelException e) {
971             log(Severity.INFORMATIONAL, e, null);
972         } catch (CoreException e) {
973             log(Severity.INFORMATIONAL, e, null);
974         }
975 
976         return null;
977     }
978 
979     @Override
980     @Nullable
isSubclassOf( @onNull Project project, @NonNull String name, @NonNull String superClassName)981     public Boolean isSubclassOf(
982             @NonNull Project project,
983             @NonNull String name, @NonNull
984             String superClassName) {
985         if (!mSearchForSuperClasses) {
986             // Super type search using the Eclipse index is potentially slow, so
987             // only do this when necessary
988             return null;
989         }
990 
991         IProject eclipseProject = getProject(project);
992         if (eclipseProject == null) {
993             return null;
994         }
995 
996         try {
997             IJavaProject javaProject = BaseProjectHelper.getJavaProject(eclipseProject);
998             if (javaProject == null) {
999                 return null;
1000             }
1001 
1002             String typeFqcn = ClassContext.getFqcn(name);
1003             IType type = javaProject.findType(typeFqcn);
1004             if (type != null) {
1005                 ITypeHierarchy hierarchy = type.newSupertypeHierarchy(new NullProgressMonitor());
1006                 IType[] allSupertypes = hierarchy.getAllSuperclasses(type);
1007                 if (allSupertypes != null) {
1008                     String target = 'L' + superClassName + ';';
1009                     for (IType superType : allSupertypes) {
1010                         if (target.equals(superType.getKey())) {
1011                             return Boolean.TRUE;
1012                         }
1013                     }
1014                     return Boolean.FALSE;
1015                 }
1016             }
1017         } catch (JavaModelException e) {
1018             log(Severity.INFORMATIONAL, e, null);
1019         } catch (CoreException e) {
1020             log(Severity.INFORMATIONAL, e, null);
1021         }
1022 
1023         return null;
1024     }
1025 
1026     private static class LazyLocation extends Location implements Location.Handle {
1027         private final IStructuredDocument mDocument;
1028         private final IndexedRegion mRegion;
1029         private Position mStart;
1030         private Position mEnd;
1031 
LazyLocation(File file, IStructuredDocument document, IndexedRegion region)1032         public LazyLocation(File file, IStructuredDocument document, IndexedRegion region) {
1033             super(file, null /*start*/, null /*end*/);
1034             mDocument = document;
1035             mRegion = region;
1036         }
1037 
1038         @Override
getStart()1039         public Position getStart() {
1040             if (mStart == null) {
1041                 int line = -1;
1042                 int column = -1;
1043                 int offset = mRegion.getStartOffset();
1044 
1045                 if (mRegion instanceof org.w3c.dom.Text && mDocument != null) {
1046                     // For text nodes, skip whitespace prefix, if any
1047                     for (int i = offset;
1048                             i < mRegion.getEndOffset() && i < mDocument.getLength(); i++) {
1049                         try {
1050                             char c = mDocument.getChar(i);
1051                             if (!Character.isWhitespace(c)) {
1052                                 offset = i;
1053                                 break;
1054                             }
1055                         } catch (BadLocationException e) {
1056                             break;
1057                         }
1058                     }
1059                 }
1060 
1061                 if (mDocument != null && offset < mDocument.getLength()) {
1062                     line = mDocument.getLineOfOffset(offset);
1063                     column = -1;
1064                     try {
1065                         int lineOffset = mDocument.getLineOffset(line);
1066                         column = offset - lineOffset;
1067                     } catch (BadLocationException e) {
1068                         AdtPlugin.log(e, null);
1069                     }
1070                 }
1071 
1072                 mStart = new DefaultPosition(line, column, offset);
1073             }
1074 
1075             return mStart;
1076         }
1077 
1078         @Override
getEnd()1079         public Position getEnd() {
1080             if (mEnd == null) {
1081                 mEnd = new DefaultPosition(-1, -1, mRegion.getEndOffset());
1082             }
1083 
1084             return mEnd;
1085         }
1086 
1087         @Override
resolve()1088         public @NonNull Location resolve() {
1089             return this;
1090         }
1091     }
1092 
1093     private static class EclipseJavaParser implements IJavaParser {
1094         private static final boolean USE_ECLIPSE_PARSER = true;
1095         private final Parser mParser;
1096 
EclipseJavaParser()1097         EclipseJavaParser() {
1098             if (USE_ECLIPSE_PARSER) {
1099                 CompilerOptions options = new CompilerOptions();
1100                 // Read settings from project? Note that this doesn't really matter because
1101                 // we will only be parsing, not actually compiling.
1102                 options.complianceLevel = ClassFileConstants.JDK1_6;
1103                 options.sourceLevel = ClassFileConstants.JDK1_6;
1104                 options.targetJDK = ClassFileConstants.JDK1_6;
1105                 options.parseLiteralExpressionsAsConstants = true;
1106                 ProblemReporter problemReporter = new ProblemReporter(
1107                         DefaultErrorHandlingPolicies.exitOnFirstError(),
1108                         options,
1109                         new DefaultProblemFactory());
1110                 mParser = new Parser(problemReporter, options.parseLiteralExpressionsAsConstants);
1111                 mParser.javadocParser.checkDocComment = false;
1112             } else {
1113                 mParser = null;
1114             }
1115         }
1116 
1117         @Override
parseJava(@onNull JavaContext context)1118         public lombok.ast.Node parseJava(@NonNull JavaContext context) {
1119             if (USE_ECLIPSE_PARSER) {
1120                 // Use Eclipse's compiler
1121                 EcjTreeConverter converter = new EcjTreeConverter();
1122                 String code = context.getContents();
1123 
1124                 CompilationUnit sourceUnit = new CompilationUnit(code.toCharArray(),
1125                         context.file.getName(), "UTF-8"); //$NON-NLS-1$
1126                 CompilationResult compilationResult = new CompilationResult(sourceUnit, 0, 0, 0);
1127                 CompilationUnitDeclaration unit = null;
1128                 try {
1129                     unit = mParser.parse(sourceUnit, compilationResult);
1130                 } catch (AbortCompilation e) {
1131                     // No need to report Java parsing errors while running in Eclipse.
1132                     // Eclipse itself will already provide problem markers for these files,
1133                     // so all this achieves is creating "multiple annotations on this line"
1134                     // tooltips instead.
1135                     return null;
1136                 }
1137                 if (unit == null) {
1138                     return null;
1139                 }
1140 
1141                 try {
1142                     converter.visit(code, unit);
1143                     List<? extends lombok.ast.Node> nodes = converter.getAll();
1144 
1145                     // There could be more than one node when there are errors; pick out the
1146                     // compilation unit node
1147                     for (lombok.ast.Node node : nodes) {
1148                         if (node instanceof lombok.ast.CompilationUnit) {
1149                             return node;
1150                         }
1151                     }
1152 
1153                     return null;
1154                 } catch (Throwable t) {
1155                     AdtPlugin.log(t, "Failed converting ECJ parse tree to Lombok for file %1$s",
1156                             context.file.getPath());
1157                     return null;
1158                 }
1159             } else {
1160                 // Use Lombok for now
1161                 Source source = new Source(context.getContents(), context.file.getName());
1162                 List<lombok.ast.Node> nodes = source.getNodes();
1163 
1164                 // Don't analyze files containing errors
1165                 List<ParseProblem> problems = source.getProblems();
1166                 if (problems != null && problems.size() > 0) {
1167                     /* Silently ignore the errors. There are still some bugs in Lombok/Parboiled
1168                      * (triggered if you run lint on the AOSP framework directory for example),
1169                      * and having these show up as fatal errors when it's really a tool bug
1170                      * is bad. To make matters worse, the error messages aren't clear:
1171                      * http://code.google.com/p/projectlombok/issues/detail?id=313
1172                     for (ParseProblem problem : problems) {
1173                         lombok.ast.Position position = problem.getPosition();
1174                         Location location = Location.create(context.file,
1175                                 context.getContents(), position.getStart(), position.getEnd());
1176                         String message = problem.getMessage();
1177                         context.report(
1178                                 IssueRegistry.PARSER_ERROR, location,
1179                                 message,
1180                                 null);
1181 
1182                     }
1183                     */
1184                     return null;
1185                 }
1186 
1187                 // There could be more than one node when there are errors; pick out the
1188                 // compilation unit node
1189                 for (lombok.ast.Node node : nodes) {
1190                     if (node instanceof lombok.ast.CompilationUnit) {
1191                         return node;
1192                     }
1193                 }
1194                 return null;
1195             }
1196         }
1197 
1198         @Override
getLocation(@onNull JavaContext context, @NonNull lombok.ast.Node node)1199         public @NonNull Location getLocation(@NonNull JavaContext context,
1200                 @NonNull lombok.ast.Node node) {
1201             lombok.ast.Position position = node.getPosition();
1202             return Location.create(context.file, context.getContents(),
1203                     position.getStart(), position.getEnd());
1204         }
1205 
1206         @Override
createLocationHandle(@onNull JavaContext context, @NonNull lombok.ast.Node node)1207         public @NonNull Handle createLocationHandle(@NonNull JavaContext context,
1208                 @NonNull lombok.ast.Node node) {
1209             return new LocationHandle(context.file, node);
1210         }
1211 
1212         @Override
dispose(@onNull JavaContext context, @NonNull lombok.ast.Node compilationUnit)1213         public void dispose(@NonNull JavaContext context,
1214                 @NonNull lombok.ast.Node compilationUnit) {
1215         }
1216 
1217         @Override
1218         @Nullable
resolve(@onNull JavaContext context, @NonNull lombok.ast.Node node)1219         public lombok.ast.Node resolve(@NonNull JavaContext context,
1220                 @NonNull lombok.ast.Node node) {
1221             return null;
1222         }
1223 
1224         @Override
1225         @Nullable
getType(@onNull JavaContext context, @NonNull lombok.ast.Node node)1226         public TypeReference getType(@NonNull JavaContext context, @NonNull lombok.ast.Node node) {
1227             return null;
1228         }
1229 
1230         /* Handle for creating positions cheaply and returning full fledged locations later */
1231         private class LocationHandle implements Handle {
1232             private File mFile;
1233             private lombok.ast.Node mNode;
1234             private Object mClientData;
1235 
LocationHandle(File file, lombok.ast.Node node)1236             public LocationHandle(File file, lombok.ast.Node node) {
1237                 mFile = file;
1238                 mNode = node;
1239             }
1240 
1241             @Override
resolve()1242             public @NonNull Location resolve() {
1243                 lombok.ast.Position pos = mNode.getPosition();
1244                 return Location.create(mFile, null /*contents*/, pos.getStart(), pos.getEnd());
1245             }
1246 
1247             @Override
setClientData(@ullable Object clientData)1248             public void setClientData(@Nullable Object clientData) {
1249                 mClientData = clientData;
1250             }
1251 
1252             @Override
1253             @Nullable
getClientData()1254             public Object getClientData() {
1255                 return mClientData;
1256             }
1257         }
1258     }
1259 }
1260 
1261