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