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