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.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      */
    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      */
    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      */
    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
    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
    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
    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
    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