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