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