Home | History | Annotate | Download | only in api
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
      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 
     17 package com.android.tools.lint.client.api;
     18 
     19 import static com.android.tools.lint.detector.api.LintConstants.ANDROID_MANIFEST_XML;
     20 import static com.android.tools.lint.detector.api.LintConstants.ATTR_IGNORE;
     21 import static com.android.tools.lint.detector.api.LintConstants.DOT_CLASS;
     22 import static com.android.tools.lint.detector.api.LintConstants.DOT_JAR;
     23 import static com.android.tools.lint.detector.api.LintConstants.DOT_JAVA;
     24 import static com.android.tools.lint.detector.api.LintConstants.OLD_PROGUARD_FILE;
     25 import static com.android.tools.lint.detector.api.LintConstants.PROGUARD_FILE;
     26 import static com.android.tools.lint.detector.api.LintConstants.RES_FOLDER;
     27 import static com.android.tools.lint.detector.api.LintConstants.SUPPRESS_ALL;
     28 import static com.android.tools.lint.detector.api.LintConstants.SUPPRESS_LINT;
     29 import static com.android.tools.lint.detector.api.LintConstants.TOOLS_URI;
     30 import static org.objectweb.asm.Opcodes.ASM4;
     31 
     32 import com.android.annotations.NonNull;
     33 import com.android.annotations.Nullable;
     34 import com.android.annotations.VisibleForTesting;
     35 import com.android.resources.ResourceFolderType;
     36 import com.android.tools.lint.client.api.LintListener.EventType;
     37 import com.android.tools.lint.detector.api.Category;
     38 import com.android.tools.lint.detector.api.ClassContext;
     39 import com.android.tools.lint.detector.api.Context;
     40 import com.android.tools.lint.detector.api.Detector;
     41 import com.android.tools.lint.detector.api.Issue;
     42 import com.android.tools.lint.detector.api.JavaContext;
     43 import com.android.tools.lint.detector.api.LintUtils;
     44 import com.android.tools.lint.detector.api.Location;
     45 import com.android.tools.lint.detector.api.Project;
     46 import com.android.tools.lint.detector.api.ResourceXmlDetector;
     47 import com.android.tools.lint.detector.api.Scope;
     48 import com.android.tools.lint.detector.api.Severity;
     49 import com.android.tools.lint.detector.api.XmlContext;
     50 import com.google.common.annotations.Beta;
     51 import com.google.common.base.CharMatcher;
     52 import com.google.common.base.Splitter;
     53 import com.google.common.collect.ArrayListMultimap;
     54 import com.google.common.collect.Multimap;
     55 import com.google.common.io.ByteStreams;
     56 import com.google.common.io.Files;
     57 
     58 import org.objectweb.asm.ClassReader;
     59 import org.objectweb.asm.ClassVisitor;
     60 import org.objectweb.asm.tree.AnnotationNode;
     61 import org.objectweb.asm.tree.ClassNode;
     62 import org.objectweb.asm.tree.FieldNode;
     63 import org.objectweb.asm.tree.MethodNode;
     64 import org.w3c.dom.Attr;
     65 import org.w3c.dom.Element;
     66 
     67 import java.io.File;
     68 import java.io.FileInputStream;
     69 import java.io.IOException;
     70 import java.util.ArrayDeque;
     71 import java.util.ArrayList;
     72 import java.util.Arrays;
     73 import java.util.Collection;
     74 import java.util.Collections;
     75 import java.util.Deque;
     76 import java.util.EnumSet;
     77 import java.util.HashMap;
     78 import java.util.HashSet;
     79 import java.util.IdentityHashMap;
     80 import java.util.Iterator;
     81 import java.util.List;
     82 import java.util.Map;
     83 import java.util.Set;
     84 import java.util.zip.ZipEntry;
     85 import java.util.zip.ZipInputStream;
     86 
     87 import lombok.ast.Annotation;
     88 import lombok.ast.AnnotationElement;
     89 import lombok.ast.AnnotationValue;
     90 import lombok.ast.ArrayInitializer;
     91 import lombok.ast.ClassDeclaration;
     92 import lombok.ast.Expression;
     93 import lombok.ast.MethodDeclaration;
     94 import lombok.ast.Modifiers;
     95 import lombok.ast.Node;
     96 import lombok.ast.StrictListAccessor;
     97 import lombok.ast.StringLiteral;
     98 import lombok.ast.TypeReference;
     99 import lombok.ast.VariableDefinition;
    100 
    101 /**
    102  * Analyzes Android projects and files
    103  * <p>
    104  * <b>NOTE: This is not a public or final API; if you rely on this be prepared
    105  * to adjust your code for the next tools release.</b>
    106  */
    107 @Beta
    108 public class LintDriver {
    109     /**
    110      * Max number of passes to run through the lint runner if requested by
    111      * {@link #requestRepeat}
    112      */
    113     private static final int MAX_PHASES = 3;
    114     private static final String SUPPRESS_LINT_VMSIG = '/' + SUPPRESS_LINT + ';';
    115 
    116     private final LintClient mClient;
    117     private volatile boolean mCanceled;
    118     private IssueRegistry mRegistry;
    119     private EnumSet<Scope> mScope;
    120     private List<? extends Detector> mApplicableDetectors;
    121     private Map<Scope, List<Detector>> mScopeDetectors;
    122     private List<LintListener> mListeners;
    123     private int mPhase;
    124     private List<Detector> mRepeatingDetectors;
    125     private EnumSet<Scope> mRepeatScope;
    126     private Project[] mCurrentProjects;
    127     private boolean mAbbreviating = true;
    128 
    129     /**
    130      * Creates a new {@link LintDriver}
    131      *
    132      * @param registry The registry containing issues to be checked
    133      * @param client the tool wrapping the analyzer, such as an IDE or a CLI
    134      */
    135     public LintDriver(@NonNull IssueRegistry registry, @NonNull LintClient client) {
    136         mRegistry = registry;
    137         mClient = new LintClientWrapper(client);
    138     }
    139 
    140     /** Cancels the current lint run as soon as possible */
    141     public void cancel() {
    142         mCanceled = true;
    143     }
    144 
    145     /**
    146      * Returns the scope for the lint job
    147      *
    148      * @return the scope, never null
    149      */
    150     @NonNull
    151     public EnumSet<Scope> getScope() {
    152         return mScope;
    153     }
    154 
    155     /**
    156      * Returns the lint client requesting the lint check
    157      *
    158      * @return the client, never null
    159      */
    160     @NonNull
    161     public LintClient getClient() {
    162         return mClient;
    163     }
    164 
    165     /**
    166      * Returns the current phase number. The first pass is numbered 1. Only one pass
    167      * will be performed, unless a {@link Detector} calls {@link #requestRepeat}.
    168      *
    169      * @return the current phase, usually 1
    170      */
    171     public int getPhase() {
    172         return mPhase;
    173     }
    174 
    175     /**
    176      * Returns the project containing a given file, or null if not found. This searches
    177      * only among the currently checked project and its library projects, not among all
    178      * possible projects being scanned sequentially.
    179      *
    180      * @param file the file to be checked
    181      * @return the corresponding project, or null if not found
    182      */
    183     @Nullable
    184     public Project findProjectFor(@NonNull File file) {
    185         if (mCurrentProjects != null) {
    186             if (mCurrentProjects.length == 1) {
    187                 return mCurrentProjects[0];
    188             }
    189             String path = file.getPath();
    190             for (Project project : mCurrentProjects) {
    191                 if (path.startsWith(project.getDir().getPath())) {
    192                     return project;
    193                 }
    194             }
    195         }
    196 
    197         return null;
    198     }
    199 
    200     /**
    201      * Sets whether lint should abbreviate output when appropriate.
    202      *
    203      * @param abbreviating true to abbreviate output, false to include everything
    204      */
    205     public void setAbbreviating(boolean abbreviating) {
    206         mAbbreviating = abbreviating;
    207     }
    208 
    209     /**
    210      * Returns whether lint should abbreviate output when appropriate.
    211      *
    212      * @return true if lint should abbreviate output, false when including everything
    213      */
    214     public boolean isAbbreviating() {
    215         return mAbbreviating;
    216     }
    217 
    218     /**
    219      * Analyze the given file (which can point to an Android project). Issues found
    220      * are reported to the associated {@link LintClient}.
    221      *
    222      * @param files the files and directories to be analyzed
    223      * @param scope the scope of the analysis; detectors with a wider scope will
    224      *            not be run. If null, the scope will be inferred from the files.
    225      */
    226     public void analyze(@NonNull List<File> files, @Nullable EnumSet<Scope> scope) {
    227         mCanceled = false;
    228         mScope = scope;
    229 
    230         Collection<Project> projects = computeProjects(files);
    231         if (projects.size() == 0) {
    232             mClient.log(null, "No projects found for %1$s", files.toString());
    233             return;
    234         }
    235         if (mCanceled) {
    236             return;
    237         }
    238 
    239         if (mScope == null) {
    240             // Infer the scope
    241             mScope = EnumSet.noneOf(Scope.class);
    242             for (Project project : projects) {
    243                 if (project.getSubset() != null) {
    244                     for (File file : project.getSubset()) {
    245                         String name = file.getName();
    246                         if (name.equals(ANDROID_MANIFEST_XML)) {
    247                             mScope.add(Scope.MANIFEST);
    248                         } else if (name.endsWith(".xml")) {
    249                             mScope.add(Scope.RESOURCE_FILE);
    250                         } else if (name.equals(PROGUARD_FILE) || name.equals(OLD_PROGUARD_FILE)) {
    251                             mScope.add(Scope.PROGUARD_FILE);
    252                         } else if (name.equals(RES_FOLDER)
    253                                 || file.getParent().equals(RES_FOLDER)) {
    254                             mScope.add(Scope.ALL_RESOURCE_FILES);
    255                             mScope.add(Scope.RESOURCE_FILE);
    256                         } else if (name.endsWith(DOT_JAVA)) {
    257                             mScope.add(Scope.JAVA_FILE);
    258                         } else if (name.endsWith(DOT_CLASS)) {
    259                             mScope.add(Scope.CLASS_FILE);
    260                         } else if (name.equals(OLD_PROGUARD_FILE) || name.equals(PROGUARD_FILE)) {
    261                             mScope.add(Scope.PROGUARD_FILE);
    262                         }
    263                     }
    264                 } else {
    265                     // Specified a full project: just use the full project scope
    266                     mScope = Scope.ALL;
    267                     break;
    268                 }
    269             }
    270         }
    271 
    272         fireEvent(EventType.STARTING, null);
    273 
    274         for (Project project : projects) {
    275             mPhase = 1;
    276 
    277             // The set of available detectors varies between projects
    278             computeDetectors(project);
    279 
    280             if (mApplicableDetectors.size() == 0) {
    281                 // No detectors enabled in this project: skip it
    282                 continue;
    283             }
    284 
    285             checkProject(project);
    286             if (mCanceled) {
    287                 break;
    288             }
    289 
    290             runExtraPhases(project);
    291         }
    292 
    293         fireEvent(mCanceled ? EventType.CANCELED : EventType.COMPLETED, null);
    294     }
    295 
    296     private void runExtraPhases(Project project) {
    297         // Did any detectors request another phase?
    298         if (mRepeatingDetectors != null) {
    299             // Yes. Iterate up to MAX_PHASES times.
    300 
    301             // During the extra phases, we might be narrowing the scope, and setting it in the
    302             // scope field such that detectors asking about the available scope will get the
    303             // correct result. However, we need to restore it to the original scope when this
    304             // is done in case there are other projects that will be checked after this, since
    305             // the repeated phases is done *per project*, not after all projects have been
    306             // processed.
    307             EnumSet<Scope> oldScope = mScope;
    308 
    309             do {
    310                 mPhase++;
    311                 fireEvent(EventType.NEW_PHASE,
    312                         new Context(this, project, null, project.getDir()));
    313 
    314                 // Narrow the scope down to the set of scopes requested by
    315                 // the rules.
    316                 if (mRepeatScope == null) {
    317                     mRepeatScope = Scope.ALL;
    318                 }
    319                 mScope = Scope.intersect(mScope, mRepeatScope);
    320                 if (mScope.isEmpty()) {
    321                     break;
    322                 }
    323 
    324                 // Compute the detectors to use for this pass.
    325                 // Unlike the normal computeDetectors(project) call,
    326                 // this is going to use the existing instances, and include
    327                 // those that apply for the configuration.
    328                 computeRepeatingDetectors(mRepeatingDetectors, project);
    329 
    330                 if (mApplicableDetectors.size() == 0) {
    331                     // No detectors enabled in this project: skip it
    332                     continue;
    333                 }
    334 
    335                 checkProject(project);
    336                 if (mCanceled) {
    337                     break;
    338                 }
    339             } while (mPhase < MAX_PHASES && mRepeatingDetectors != null);
    340 
    341             mScope = oldScope;
    342         }
    343     }
    344 
    345     private void computeRepeatingDetectors(List<Detector> detectors, Project project) {
    346         // Ensure that the current visitor is recomputed
    347         mCurrentFolderType = null;
    348         mCurrentVisitor = null;
    349 
    350         // Create map from detector class to issue such that we can
    351         // compute applicable issues for each detector in the list of detectors
    352         // to be repeated
    353         List<Issue> issues = mRegistry.getIssues();
    354         Multimap<Class<? extends Detector>, Issue> issueMap =
    355                 ArrayListMultimap.create(issues.size(), 3);
    356         for (Issue issue : issues) {
    357             issueMap.put(issue.getDetectorClass(), issue);
    358         }
    359 
    360         Map<Class<? extends Detector>, EnumSet<Scope>> detectorToScope =
    361                 new HashMap<Class<? extends Detector>, EnumSet<Scope>>();
    362         Map<Scope, List<Detector>> scopeToDetectors =
    363                 new HashMap<Scope, List<Detector>>();
    364 
    365         List<Detector> detectorList = new ArrayList<Detector>();
    366         // Compute the list of detectors (narrowed down from mRepeatingDetectors),
    367         // and simultaneously build up the detectorToScope map which tracks
    368         // the scopes each detector is affected by (this is used to populate
    369         // the mScopeDetectors map which is used during iteration).
    370         Configuration configuration = project.getConfiguration();
    371         for (Detector detector : detectors) {
    372             Class<? extends Detector> detectorClass = detector.getClass();
    373             Collection<Issue> detectorIssues = issueMap.get(detectorClass);
    374             if (issues != null) {
    375                 boolean add = false;
    376                 for (Issue issue : detectorIssues) {
    377                     // The reason we have to check whether the detector is enabled
    378                     // is that this is a per-project property, so when running lint in multiple
    379                     // projects, a detector enabled only in a different project could have
    380                     // requested another phase, and we end up in this project checking whether
    381                     // the detector is enabled here.
    382                     if (!configuration.isEnabled(issue)) {
    383                         continue;
    384                     }
    385 
    386                     add = true; // Include detector if any of its issues are enabled
    387 
    388                     EnumSet<Scope> s = detectorToScope.get(detectorClass);
    389                     EnumSet<Scope> issueScope = issue.getScope();
    390                     if (s == null) {
    391                         detectorToScope.put(detectorClass, issueScope);
    392                     } else if (!s.containsAll(issueScope)) {
    393                         EnumSet<Scope> union = EnumSet.copyOf(s);
    394                         union.addAll(issueScope);
    395                         detectorToScope.put(detectorClass, union);
    396                     }
    397                 }
    398 
    399                 if (add) {
    400                     detectorList.add(detector);
    401                     EnumSet<Scope> union = detectorToScope.get(detector.getClass());
    402                     for (Scope s : union) {
    403                         List<Detector> list = scopeToDetectors.get(s);
    404                         if (list == null) {
    405                             list = new ArrayList<Detector>();
    406                             scopeToDetectors.put(s, list);
    407                         }
    408                         list.add(detector);
    409                     }
    410                 }
    411             }
    412         }
    413 
    414         mApplicableDetectors = detectorList;
    415         mScopeDetectors = scopeToDetectors;
    416         mRepeatingDetectors = null;
    417         mRepeatScope = null;
    418 
    419         validateScopeList();
    420     }
    421 
    422     private void computeDetectors(@NonNull Project project) {
    423         // Ensure that the current visitor is recomputed
    424         mCurrentFolderType = null;
    425         mCurrentVisitor = null;
    426 
    427         Configuration configuration = project.getConfiguration();
    428         mScopeDetectors = new HashMap<Scope, List<Detector>>();
    429         mApplicableDetectors = mRegistry.createDetectors(mClient, configuration,
    430                 mScope, mScopeDetectors);
    431 
    432         validateScopeList();
    433     }
    434 
    435     /** Development diagnostics only, run with assertions on */
    436     @SuppressWarnings("all") // Turn off warnings for the intentional assertion side effect below
    437     private void validateScopeList() {
    438         boolean assertionsEnabled = false;
    439         assert assertionsEnabled = true; // Intentional side-effect
    440         if (assertionsEnabled) {
    441             List<Detector> resourceFileDetectors = mScopeDetectors.get(Scope.RESOURCE_FILE);
    442             if (resourceFileDetectors != null) {
    443                 for (Detector detector : resourceFileDetectors) {
    444                     assert detector instanceof ResourceXmlDetector : detector;
    445                 }
    446             }
    447 
    448             List<Detector> manifestDetectors = mScopeDetectors.get(Scope.MANIFEST);
    449             if (manifestDetectors != null) {
    450                 for (Detector detector : manifestDetectors) {
    451                     assert detector instanceof Detector.XmlScanner : detector;
    452                 }
    453             }
    454             List<Detector> javaCodeDetectors = mScopeDetectors.get(Scope.ALL_JAVA_FILES);
    455             if (javaCodeDetectors != null) {
    456                 for (Detector detector : javaCodeDetectors) {
    457                     assert detector instanceof Detector.JavaScanner : detector;
    458                 }
    459             }
    460             List<Detector> javaFileDetectors = mScopeDetectors.get(Scope.JAVA_FILE);
    461             if (javaFileDetectors != null) {
    462                 for (Detector detector : javaFileDetectors) {
    463                     assert detector instanceof Detector.JavaScanner : detector;
    464                 }
    465             }
    466 
    467             List<Detector> classDetectors = mScopeDetectors.get(Scope.CLASS_FILE);
    468             if (classDetectors != null) {
    469                 for (Detector detector : classDetectors) {
    470                     assert detector instanceof Detector.ClassScanner : detector;
    471                 }
    472             }
    473         }
    474     }
    475 
    476     private void registerProjectFile(
    477             @NonNull Map<File, Project> fileToProject,
    478             @NonNull File file,
    479             @NonNull File projectDir,
    480             @NonNull File rootDir) {
    481         fileToProject.put(file, mClient.getProject(projectDir, rootDir));
    482     }
    483 
    484     private Collection<Project> computeProjects(@NonNull List<File> files) {
    485         // Compute list of projects
    486         Map<File, Project> fileToProject = new HashMap<File, Project>();
    487 
    488         File sharedRoot = null;
    489 
    490         // Ensure that we have absolute paths such that if you lint
    491         //  "foo bar" in "baz" we can show baz/ as the root
    492         if (files.size() > 1) {
    493             List<File> absolute = new ArrayList<File>(files.size());
    494             for (File file : files) {
    495                 absolute.add(file.getAbsoluteFile());
    496             }
    497             files = absolute;
    498 
    499             sharedRoot = LintUtils.getCommonParent(files);
    500             if (sharedRoot != null && sharedRoot.getParentFile() == null) { // "/" ?
    501                 sharedRoot = null;
    502             }
    503         }
    504 
    505 
    506         for (File file : files) {
    507             if (file.isDirectory()) {
    508                 File rootDir = sharedRoot;
    509                 if (rootDir == null) {
    510                     rootDir = file;
    511                     if (files.size() > 1) {
    512                         rootDir = file.getParentFile();
    513                         if (rootDir == null) {
    514                             rootDir = file;
    515                         }
    516                     }
    517                 }
    518 
    519                 // Figure out what to do with a directory. Note that the meaning of the
    520                 // directory can be ambiguous:
    521                 // If you pass a directory which is unknown, we don't know if we should
    522                 // search upwards (in case you're pointing at a deep java package folder
    523                 // within the project), or if you're pointing at some top level directory
    524                 // containing lots of projects you want to scan. We attempt to do the
    525                 // right thing, which is to see if you're pointing right at a project or
    526                 // right within it (say at the src/ or res/) folder, and if not, you're
    527                 // hopefully pointing at a project tree that you want to scan recursively.
    528                 if (isProjectDir(file)) {
    529                     registerProjectFile(fileToProject, file, file, rootDir);
    530                     continue;
    531                 } else {
    532                     File parent = file.getParentFile();
    533                     if (parent != null) {
    534                         if (isProjectDir(parent)) {
    535                             registerProjectFile(fileToProject, file, parent, parent);
    536                             continue;
    537                         } else {
    538                             parent = parent.getParentFile();
    539                             if (isProjectDir(parent)) {
    540                                 registerProjectFile(fileToProject, file, parent, parent);
    541                                 continue;
    542                             }
    543                         }
    544                     }
    545 
    546                     // Search downwards for nested projects
    547                     addProjects(file, fileToProject, rootDir);
    548                 }
    549             } else {
    550                 // Pointed at a file: Search upwards for the containing project
    551                 File parent = file.getParentFile();
    552                 while (parent != null) {
    553                     if (isProjectDir(parent)) {
    554                         registerProjectFile(fileToProject, file, parent, parent);
    555                         break;
    556                     }
    557                     parent = parent.getParentFile();
    558                 }
    559             }
    560 
    561             if (mCanceled) {
    562                 return Collections.emptySet();
    563             }
    564         }
    565 
    566         for (Map.Entry<File, Project> entry : fileToProject.entrySet()) {
    567             File file = entry.getKey();
    568             Project project = entry.getValue();
    569             if (!file.equals(project.getDir())) {
    570                 if (file.isDirectory()) {
    571                     try {
    572                         File dir = file.getCanonicalFile();
    573                         if (dir.equals(project.getDir())) {
    574                             continue;
    575                         }
    576                     } catch (IOException ioe) {
    577                         // pass
    578                     }
    579                 }
    580 
    581                 project.addFile(file);
    582             }
    583         }
    584 
    585         // Partition the projects up such that we only return projects that aren't
    586         // included by other projects (e.g. because they are library projects)
    587 
    588         Collection<Project> allProjects = fileToProject.values();
    589         Set<Project> roots = new HashSet<Project>(allProjects);
    590         for (Project project : allProjects) {
    591             roots.removeAll(project.getAllLibraries());
    592         }
    593 
    594         if (LintUtils.assertionsEnabled()) {
    595             // Make sure that all the project directories are unique. This ensures
    596             // that we didn't accidentally end up with different project instances
    597             // for a library project discovered as a directory as well as one
    598             // initialized from the library project dependency list
    599             IdentityHashMap<Project, Project> projects =
    600                     new IdentityHashMap<Project, Project>();
    601             for (Project project : roots) {
    602                 projects.put(project, project);
    603                 for (Project library : project.getAllLibraries()) {
    604                     projects.put(library, library);
    605                 }
    606             }
    607             Set<File> dirs = new HashSet<File>();
    608             for (Project project : projects.keySet()) {
    609                 assert !dirs.contains(project.getDir());
    610                 dirs.add(project.getDir());
    611             }
    612         }
    613 
    614         return roots;
    615     }
    616 
    617     private void addProjects(
    618             @NonNull File dir,
    619             @NonNull Map<File, Project> fileToProject,
    620             @NonNull File rootDir) {
    621         if (mCanceled) {
    622             return;
    623         }
    624 
    625         if (isProjectDir(dir)) {
    626             registerProjectFile(fileToProject, dir, dir, rootDir);
    627         } else {
    628             File[] files = dir.listFiles();
    629             if (files != null) {
    630                 for (File file : files) {
    631                     if (file.isDirectory()) {
    632                         addProjects(file, fileToProject, rootDir);
    633                     }
    634                 }
    635             }
    636         }
    637     }
    638 
    639     private boolean isProjectDir(@NonNull File dir) {
    640         return new File(dir, ANDROID_MANIFEST_XML).exists();
    641     }
    642 
    643     private void checkProject(@NonNull Project project) {
    644         File projectDir = project.getDir();
    645 
    646         Context projectContext = new Context(this, project, null, projectDir);
    647         fireEvent(EventType.SCANNING_PROJECT, projectContext);
    648 
    649         List<Project> allLibraries = project.getAllLibraries();
    650         Set<Project> allProjects = new HashSet<Project>(allLibraries.size() + 1);
    651         allProjects.add(project);
    652         allProjects.addAll(allLibraries);
    653         mCurrentProjects = allProjects.toArray(new Project[allProjects.size()]);
    654 
    655         for (Detector check : mApplicableDetectors) {
    656             check.beforeCheckProject(projectContext);
    657             if (mCanceled) {
    658                 return;
    659             }
    660         }
    661 
    662         runFileDetectors(project, project);
    663 
    664         if (!Scope.checkSingleFile(mScope)) {
    665             List<Project> libraries = project.getDirectLibraries();
    666             for (Project library : libraries) {
    667                 Context libraryContext = new Context(this, library, project, projectDir);
    668                 fireEvent(EventType.SCANNING_LIBRARY_PROJECT, libraryContext);
    669 
    670                 for (Detector check : mApplicableDetectors) {
    671                     check.beforeCheckLibraryProject(libraryContext);
    672                     if (mCanceled) {
    673                         return;
    674                     }
    675                 }
    676 
    677                 runFileDetectors(library, project);
    678                 if (mCanceled) {
    679                     return;
    680                 }
    681 
    682                 for (Detector check : mApplicableDetectors) {
    683                     check.afterCheckLibraryProject(libraryContext);
    684                     if (mCanceled) {
    685                         return;
    686                     }
    687                 }
    688             }
    689         }
    690 
    691         for (Detector check : mApplicableDetectors) {
    692             check.afterCheckProject(projectContext);
    693             if (mCanceled) {
    694                 return;
    695             }
    696         }
    697 
    698         if (mCanceled) {
    699             mClient.report(
    700                 projectContext,
    701                 // Must provide an issue since API guarantees that the issue parameter
    702                 // is valid
    703                 Issue.create("Lint", "", "", Category.PERFORMANCE, 0, Severity.INFORMATIONAL, //$NON-NLS-1$
    704                         null, EnumSet.noneOf(Scope.class)),
    705                 Severity.INFORMATIONAL,
    706                 null /*range*/,
    707                 "Lint canceled by user", null);
    708         }
    709 
    710         mCurrentProjects = null;
    711     }
    712 
    713     private void runFileDetectors(@NonNull Project project, @Nullable Project main) {
    714         // Look up manifest information (but not for library projects)
    715         File manifestFile = project.getManifestFile();
    716         if (manifestFile != null) {
    717             XmlContext context = new XmlContext(this, project, main, manifestFile, null);
    718             IDomParser parser = mClient.getDomParser();
    719             context.document = parser.parseXml(context);
    720             if (context.document != null) {
    721                 project.readManifest(context.document);
    722 
    723                 if (!project.isLibrary() && mScope.contains(Scope.MANIFEST)) {
    724                     List<Detector> detectors = mScopeDetectors.get(Scope.MANIFEST);
    725                     if (detectors != null) {
    726                         XmlVisitor v = new XmlVisitor(parser, detectors);
    727                         fireEvent(EventType.SCANNING_FILE, context);
    728                         v.visitFile(context, manifestFile);
    729                     }
    730                 }
    731             }
    732         }
    733 
    734         // Process both Scope.RESOURCE_FILE and Scope.ALL_RESOURCE_FILES detectors together
    735         // in a single pass through the resource directories.
    736         if (mScope.contains(Scope.ALL_RESOURCE_FILES) || mScope.contains(Scope.RESOURCE_FILE)) {
    737             List<Detector> checks = union(mScopeDetectors.get(Scope.RESOURCE_FILE),
    738                     mScopeDetectors.get(Scope.ALL_RESOURCE_FILES));
    739             if (checks != null && checks.size() > 0) {
    740                 List<ResourceXmlDetector> xmlDetectors =
    741                         new ArrayList<ResourceXmlDetector>(checks.size());
    742                 for (Detector detector : checks) {
    743                     if (detector instanceof ResourceXmlDetector) {
    744                         xmlDetectors.add((ResourceXmlDetector) detector);
    745                     }
    746                 }
    747                 if (xmlDetectors.size() > 0) {
    748                     if (project.getSubset() != null) {
    749                         checkIndividualResources(project, main, xmlDetectors,
    750                                 project.getSubset());
    751                     } else {
    752                         File res = new File(project.getDir(), RES_FOLDER);
    753                         if (res.exists() && xmlDetectors.size() > 0) {
    754                             checkResFolder(project, main, res, xmlDetectors);
    755                         }
    756                     }
    757                 }
    758             }
    759         }
    760 
    761         if (mCanceled) {
    762             return;
    763         }
    764 
    765         if (mScope.contains(Scope.JAVA_FILE) || mScope.contains(Scope.ALL_JAVA_FILES)) {
    766             List<Detector> checks = union(mScopeDetectors.get(Scope.JAVA_FILE),
    767                     mScopeDetectors.get(Scope.ALL_JAVA_FILES));
    768             if (checks != null && checks.size() > 0) {
    769                 List<File> sourceFolders = project.getJavaSourceFolders();
    770                 checkJava(project, main, sourceFolders, checks);
    771             }
    772         }
    773 
    774         if (mCanceled) {
    775             return;
    776         }
    777 
    778         checkClasses(project, main);
    779 
    780         if (mCanceled) {
    781             return;
    782         }
    783 
    784         if (project == main && mScope.contains(Scope.PROGUARD_FILE)) {
    785             checkProGuard(project, main);
    786         }
    787     }
    788 
    789     private void checkProGuard(Project project, Project main) {
    790         List<Detector> detectors = mScopeDetectors.get(Scope.PROGUARD_FILE);
    791         if (detectors != null) {
    792             Project p = main != null ? main : project;
    793             List<File> files = new ArrayList<File>();
    794             String paths = p.getProguardPath();
    795             if (paths != null) {
    796                 Splitter splitter = Splitter.on(CharMatcher.anyOf(":;")); //$NON-NLS-1$
    797                 for (String path : splitter.split(paths)) {
    798                     if (path.contains("${")) { //$NON-NLS-1$
    799                         // Don't analyze the global/user proguard files
    800                         continue;
    801                     }
    802                     File file = new File(path);
    803                     if (!file.isAbsolute()) {
    804                         file = new File(project.getDir(), path);
    805                     }
    806                     if (file.exists()) {
    807                         files.add(file);
    808                     }
    809                 }
    810             }
    811             if (files.isEmpty()) {
    812                 File file = new File(project.getDir(), OLD_PROGUARD_FILE);
    813                 if (file.exists()) {
    814                     files.add(file);
    815                 }
    816                 file = new File(project.getDir(), PROGUARD_FILE);
    817                 if (file.exists()) {
    818                     files.add(file);
    819                 }
    820             }
    821             for (File file : files) {
    822                 Context context = new Context(this, project, main, file);
    823                 fireEvent(EventType.SCANNING_FILE, context);
    824                 for (Detector detector : detectors) {
    825                     if (detector.appliesTo(context, file)) {
    826                         detector.beforeCheckFile(context);
    827                         detector.run(context);
    828                         detector.afterCheckFile(context);
    829                     }
    830                 }
    831             }
    832         }
    833     }
    834 
    835     /**
    836      * Map from VM class name to corresponding super class VM name, if available.
    837      * This map is typically null except <b>during</b> class processing.
    838      */
    839     private Map<String, String> mSuperClassMap;
    840 
    841     /**
    842      * Returns the super class for the given class name,
    843      * which should be in VM format (e.g. java/lang/Integer, not java.lang.Integer).
    844      * If the super class is not known, returns null. This can happen if
    845      * the given class is not a known class according to the project or its
    846      * libraries, for example because it refers to one of the core libraries which
    847      * are not analyzed by lint.
    848      *
    849      * @param name the fully qualified class name
    850      * @return the corresponding super class name (in VM format), or null if not known
    851      */
    852     @Nullable
    853     public String getSuperClass(@NonNull String name) {
    854         if (mSuperClassMap == null) {
    855             throw new IllegalStateException("Only callable during ClassScanner#checkClass");
    856         }
    857         assert name.indexOf('.') == -1 : "Use VM signatures, e.g. java/lang/Integer";
    858         return mSuperClassMap.get(name);
    859     }
    860 
    861     @Nullable
    862     private static List<Detector> union(
    863             @Nullable List<Detector> list1,
    864             @Nullable List<Detector> list2) {
    865         if (list1 == null) {
    866             return list2;
    867         } else if (list2 == null) {
    868             return list1;
    869         } else {
    870             // Use set to pick out unique detectors, since it's possible for there to be overlap,
    871             // e.g. the DuplicateIdDetector registers both a cross-resource issue and a
    872             // single-file issue, so it shows up on both scope lists:
    873             Set<Detector> set = new HashSet<Detector>(list1.size() + list2.size());
    874             if (list1 != null) {
    875                 set.addAll(list1);
    876             }
    877             if (list2 != null) {
    878                 set.addAll(list2);
    879             }
    880 
    881             return new ArrayList<Detector>(set);
    882         }
    883     }
    884 
    885     /** Check the classes in this project (and if applicable, in any library projects */
    886     private void checkClasses(Project project, Project main) {
    887         if (!(mScope.contains(Scope.CLASS_FILE) || mScope.contains(Scope.JAVA_LIBRARIES))) {
    888             return;
    889         }
    890 
    891         // We need to read in all the classes up front such that we can initialize
    892         // the parent chains (such that for example for a virtual dispatch, we can
    893         // also check the super classes).
    894 
    895         List<File> libraries = project.getJavaLibraries();
    896         List<ClassEntry> libraryEntries;
    897         if (libraries.size() > 0) {
    898             libraryEntries = new ArrayList<ClassEntry>(64);
    899             findClasses(libraryEntries, libraries);
    900             Collections.sort(libraryEntries);
    901         } else {
    902             libraryEntries = Collections.emptyList();
    903         }
    904 
    905         List<File> classFolders = project.getJavaClassFolders();
    906         List<ClassEntry> classEntries;
    907         if (classFolders.size() == 0) {
    908             String message = String.format("No .class files were found in project \"%1$s\", "
    909                     + "so none of the classfile based checks could be run. "
    910                     + "Does the project need to be built first?", project.getName());
    911             Location location = Location.create(project.getDir());
    912             mClient.report(new Context(this, project, main, project.getDir()),
    913                     IssueRegistry.LINT_ERROR,
    914                     project.getConfiguration().getSeverity(IssueRegistry.LINT_ERROR),
    915                     location, message, null);
    916             classEntries = Collections.emptyList();
    917         } else {
    918             classEntries = new ArrayList<ClassEntry>(64);
    919             findClasses(classEntries, classFolders);
    920             Collections.sort(classEntries);
    921         }
    922 
    923         if (getPhase() == 1) {
    924             mSuperClassMap = getSuperMap(libraryEntries, classEntries);
    925         }
    926 
    927         // Actually run the detectors. Libraries should be called before the
    928         // main classes.
    929         runClassDetectors(Scope.JAVA_LIBRARIES, libraryEntries, project, main);
    930 
    931         if (mCanceled) {
    932             return;
    933         }
    934 
    935         runClassDetectors(Scope.CLASS_FILE, classEntries, project, main);
    936     }
    937 
    938     /**
    939      * Stack of {@link ClassNode} nodes for outer classes of the currently
    940      * processed class, including that class itself. Populated by
    941      * {@link #runClassDetectors(Scope, List, Project, Project)} and used by
    942      * {@link #getOuterClassNode(ClassNode)}
    943      */
    944     private Deque<ClassNode> mOuterClasses;
    945 
    946     private void runClassDetectors(Scope scope, List<ClassEntry> entries,
    947             Project project, Project main) {
    948         if (mScope.contains(scope)) {
    949             List<Detector> classDetectors = mScopeDetectors.get(scope);
    950             if (classDetectors != null && classDetectors.size() > 0 && entries.size() > 0) {
    951                 mOuterClasses = new ArrayDeque<ClassNode>();
    952                 for (ClassEntry entry : entries) {
    953                     ClassReader reader;
    954                     ClassNode classNode;
    955                     try {
    956                         reader = new ClassReader(entry.bytes);
    957                         classNode = new ClassNode();
    958                         reader.accept(classNode, 0 /* flags */);
    959                     } catch (Throwable t) {
    960                         mClient.log(null, "Error processing %1$s: broken class file?",
    961                                 entry.path());
    962                         continue;
    963                     }
    964 
    965                     ClassNode peek;
    966                     while ((peek = mOuterClasses.peek()) != null) {
    967                         if (classNode.name.startsWith(peek.name)) {
    968                             break;
    969                         } else {
    970                             mOuterClasses.pop();
    971                         }
    972                     }
    973                     mOuterClasses.push(classNode);
    974 
    975                     if (isSuppressed(null, classNode)) {
    976                         // Class was annotated with suppress all -- no need to look any further
    977                         continue;
    978                     }
    979 
    980                     ClassContext context = new ClassContext(this, project, main,
    981                             entry.file, entry.jarFile, entry.binDir, entry.bytes,
    982                             classNode, scope == Scope.JAVA_LIBRARIES /*fromLibrary*/);
    983                     runClassDetectors(context, classDetectors);
    984 
    985                     if (mCanceled) {
    986                         return;
    987                     }
    988                 }
    989 
    990                 mOuterClasses = null;
    991             }
    992         }
    993     }
    994 
    995     /** Returns the outer class node of the given class node
    996      * @param classNode the inner class node
    997      * @return the outer class node */
    998     public ClassNode getOuterClassNode(@NonNull ClassNode classNode) {
    999         String outerName = classNode.outerClass;
   1000 
   1001         Iterator<ClassNode> iterator = mOuterClasses.iterator();
   1002         while (iterator.hasNext()) {
   1003             ClassNode node = iterator.next();
   1004             if (outerName != null) {
   1005                 if (node.name.equals(outerName)) {
   1006                     return node;
   1007                 }
   1008             } else if (node == classNode) {
   1009                 return iterator.hasNext() ? iterator.next() : null;
   1010             }
   1011         }
   1012 
   1013         return null;
   1014     }
   1015 
   1016     private Map<String, String> getSuperMap(List<ClassEntry> libraryEntries,
   1017             List<ClassEntry> classEntries) {
   1018         int size = libraryEntries.size() + classEntries.size();
   1019         Map<String, String> map = new HashMap<String, String>(size);
   1020 
   1021         SuperclassVisitor visitor = new SuperclassVisitor(map);
   1022         addSuperClasses(visitor, libraryEntries);
   1023         addSuperClasses(visitor, classEntries);
   1024 
   1025         return map;
   1026     }
   1027 
   1028     private void addSuperClasses(SuperclassVisitor visitor, List<ClassEntry> entries) {
   1029         for (ClassEntry entry : entries) {
   1030             try {
   1031                 ClassReader reader = new ClassReader(entry.bytes);
   1032                 int flags = ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
   1033                 reader.accept(visitor, flags);
   1034             } catch (Throwable t) {
   1035                 mClient.log(null, "Error processing %1$s: broken class file?", entry.path());
   1036             }
   1037         }
   1038     }
   1039 
   1040     /** Visitor skimming classes and initializing a map of super classes */
   1041     private static class SuperclassVisitor extends ClassVisitor {
   1042         private final Map<String, String> mMap;
   1043 
   1044         public SuperclassVisitor(Map<String, String> map) {
   1045             super(ASM4);
   1046             mMap = map;
   1047         }
   1048 
   1049         @Override
   1050         public void visit(int version, int access, String name, String signature, String superName,
   1051                 String[] interfaces) {
   1052             if (superName != null) {
   1053                 mMap.put(name, superName);
   1054             }
   1055         }
   1056     }
   1057 
   1058     private void findClasses(
   1059             @NonNull List<ClassEntry> entries,
   1060             @NonNull List<File> classPath) {
   1061         for (File classPathEntry : classPath) {
   1062             if (classPathEntry.getName().endsWith(DOT_JAR)) {
   1063                 File jarFile = classPathEntry;
   1064                 try {
   1065                     FileInputStream fis = new FileInputStream(jarFile);
   1066                     ZipInputStream zis = new ZipInputStream(fis);
   1067                     ZipEntry entry = zis.getNextEntry();
   1068                     while (entry != null) {
   1069                         String name = entry.getName();
   1070                         if (name.endsWith(DOT_CLASS)) {
   1071                             try {
   1072                                 byte[] bytes = ByteStreams.toByteArray(zis);
   1073                                 if (bytes != null) {
   1074                                     File file = new File(entry.getName());
   1075                                     entries.add(new ClassEntry(file, jarFile, jarFile, bytes));
   1076                                 }
   1077                             } catch (Exception e) {
   1078                                 mClient.log(e, null);
   1079                                 continue;
   1080                             }
   1081                         }
   1082 
   1083                         if (mCanceled) {
   1084                             return;
   1085                         }
   1086 
   1087                         entry = zis.getNextEntry();
   1088                     }
   1089                 } catch (IOException e) {
   1090                     mClient.log(e, "Could not read jar file contents from %1$s", jarFile);
   1091                 }
   1092 
   1093                 continue;
   1094             } else if (classPathEntry.isDirectory()) {
   1095                 File binDir = classPathEntry;
   1096                 List<File> classFiles = new ArrayList<File>();
   1097                 addClassFiles(binDir, classFiles);
   1098 
   1099                 for (File file : classFiles) {
   1100                     try {
   1101                         byte[] bytes = Files.toByteArray(file);
   1102                         if (bytes != null) {
   1103                             entries.add(new ClassEntry(file, null /* jarFile*/, binDir, bytes));
   1104                         }
   1105                     } catch (IOException e) {
   1106                         mClient.log(e, null);
   1107                         continue;
   1108                     }
   1109 
   1110                     if (mCanceled) {
   1111                         return;
   1112                     }
   1113                 }
   1114             } else {
   1115                 mClient.log(null, "Ignoring class path entry %1$s", classPathEntry);
   1116             }
   1117         }
   1118     }
   1119 
   1120     private void runClassDetectors(ClassContext context, @NonNull List<Detector> checks) {
   1121         try {
   1122             File file = context.file;
   1123             ClassNode classNode = context.getClassNode();
   1124             for (Detector detector : checks) {
   1125                 if (detector.appliesTo(context, file)) {
   1126                     fireEvent(EventType.SCANNING_FILE, context);
   1127                     detector.beforeCheckFile(context);
   1128 
   1129                     Detector.ClassScanner scanner = (Detector.ClassScanner) detector;
   1130                     scanner.checkClass(context, classNode);
   1131                     detector.afterCheckFile(context);
   1132                 }
   1133 
   1134                 if (mCanceled) {
   1135                     return;
   1136                 }
   1137             }
   1138         } catch (Exception e) {
   1139             mClient.log(e, null);
   1140         }
   1141     }
   1142 
   1143     private void addClassFiles(@NonNull File dir, @NonNull List<File> classFiles) {
   1144         // Process the resource folder
   1145         File[] files = dir.listFiles();
   1146         if (files != null && files.length > 0) {
   1147             for (File file : files) {
   1148                 if (file.isFile() && file.getName().endsWith(DOT_CLASS)) {
   1149                     classFiles.add(file);
   1150                 } else if (file.isDirectory()) {
   1151                     // Recurse
   1152                     addClassFiles(file, classFiles);
   1153                 }
   1154             }
   1155         }
   1156     }
   1157 
   1158     private void checkJava(
   1159             @NonNull Project project,
   1160             @Nullable Project main,
   1161             @NonNull List<File> sourceFolders,
   1162             @NonNull List<Detector> checks) {
   1163         IJavaParser javaParser = mClient.getJavaParser();
   1164         if (javaParser == null) {
   1165             mClient.log(null, "No java parser provided to lint: not running Java checks");
   1166             return;
   1167         }
   1168 
   1169         assert checks.size() > 0;
   1170 
   1171         // Gather all Java source files in a single pass; more efficient.
   1172         List<File> sources = new ArrayList<File>(100);
   1173         for (File folder : sourceFolders) {
   1174             gatherJavaFiles(folder, sources);
   1175         }
   1176         if (sources.size() > 0) {
   1177             JavaVisitor visitor = new JavaVisitor(javaParser, checks);
   1178             for (File file : sources) {
   1179                 JavaContext context = new JavaContext(this, project, main, file);
   1180                 fireEvent(EventType.SCANNING_FILE, context);
   1181                 visitor.visitFile(context, file);
   1182                 if (mCanceled) {
   1183                     return;
   1184                 }
   1185             }
   1186         }
   1187     }
   1188 
   1189     private void gatherJavaFiles(@NonNull File dir, @NonNull List<File> result) {
   1190         File[] files = dir.listFiles();
   1191         if (files != null) {
   1192             for (File file : files) {
   1193                 if (file.isFile() && file.getName().endsWith(".java")) { //$NON-NLS-1$
   1194                     result.add(file);
   1195                 } else if (file.isDirectory()) {
   1196                     gatherJavaFiles(file, result);
   1197                 }
   1198             }
   1199         }
   1200     }
   1201 
   1202     private ResourceFolderType mCurrentFolderType;
   1203     private List<ResourceXmlDetector> mCurrentXmlDetectors;
   1204     private XmlVisitor mCurrentVisitor;
   1205 
   1206     @Nullable
   1207     private XmlVisitor getVisitor(
   1208             @NonNull ResourceFolderType type,
   1209             @NonNull List<ResourceXmlDetector> checks) {
   1210         if (type != mCurrentFolderType) {
   1211             mCurrentFolderType = type;
   1212 
   1213             // Determine which XML resource detectors apply to the given folder type
   1214             List<ResourceXmlDetector> applicableChecks =
   1215                     new ArrayList<ResourceXmlDetector>(checks.size());
   1216             for (ResourceXmlDetector check : checks) {
   1217                 if (check.appliesTo(type)) {
   1218                     applicableChecks.add(check);
   1219                 }
   1220             }
   1221 
   1222             // If the list of detectors hasn't changed, then just use the current visitor!
   1223             if (mCurrentXmlDetectors != null && mCurrentXmlDetectors.equals(applicableChecks)) {
   1224                 return mCurrentVisitor;
   1225             }
   1226 
   1227             if (applicableChecks.size() == 0) {
   1228                 mCurrentVisitor = null;
   1229                 return null;
   1230             }
   1231 
   1232             mCurrentVisitor = new XmlVisitor(mClient.getDomParser(), applicableChecks);
   1233         }
   1234 
   1235         return mCurrentVisitor;
   1236     }
   1237 
   1238     private void checkResFolder(
   1239             @NonNull Project project,
   1240             @Nullable Project main,
   1241             @NonNull File res,
   1242             @NonNull List<ResourceXmlDetector> checks) {
   1243         assert res.isDirectory();
   1244         File[] resourceDirs = res.listFiles();
   1245         if (resourceDirs == null) {
   1246             return;
   1247         }
   1248 
   1249         // Sort alphabetically such that we can process related folder types at the
   1250         // same time
   1251 
   1252         Arrays.sort(resourceDirs);
   1253         ResourceFolderType type = null;
   1254         for (File dir : resourceDirs) {
   1255             type = ResourceFolderType.getFolderType(dir.getName());
   1256             if (type != null) {
   1257                 checkResourceFolder(project, main, dir, type, checks);
   1258             }
   1259 
   1260             if (mCanceled) {
   1261                 return;
   1262             }
   1263         }
   1264     }
   1265 
   1266     private void checkResourceFolder(
   1267             @NonNull Project project,
   1268             @Nullable Project main,
   1269             @NonNull File dir,
   1270             @NonNull ResourceFolderType type,
   1271             @NonNull List<ResourceXmlDetector> checks) {
   1272         // Process the resource folder
   1273         File[] xmlFiles = dir.listFiles();
   1274         if (xmlFiles != null && xmlFiles.length > 0) {
   1275             XmlVisitor visitor = getVisitor(type, checks);
   1276             if (visitor != null) { // if not, there are no applicable rules in this folder
   1277                 for (File file : xmlFiles) {
   1278                     if (LintUtils.isXmlFile(file)) {
   1279                         XmlContext context = new XmlContext(this, project, main, file, type);
   1280                         fireEvent(EventType.SCANNING_FILE, context);
   1281                         visitor.visitFile(context, file);
   1282                         if (mCanceled) {
   1283                             return;
   1284                         }
   1285                     }
   1286                 }
   1287             }
   1288         }
   1289     }
   1290 
   1291     /** Checks individual resources */
   1292     private void checkIndividualResources(
   1293             @NonNull Project project,
   1294             @Nullable Project main,
   1295             @NonNull List<ResourceXmlDetector> xmlDetectors,
   1296             @NonNull List<File> files) {
   1297         for (File file : files) {
   1298             if (file.isDirectory()) {
   1299                 // Is it a resource folder?
   1300                 ResourceFolderType type = ResourceFolderType.getFolderType(file.getName());
   1301                 if (type != null && new File(file.getParentFile(), RES_FOLDER).exists()) {
   1302                     // Yes.
   1303                     checkResourceFolder(project, main, file, type, xmlDetectors);
   1304                 } else if (file.getName().equals(RES_FOLDER)) { // Is it the res folder?
   1305                     // Yes
   1306                     checkResFolder(project, main, file, xmlDetectors);
   1307                 } else {
   1308                     mClient.log(null, "Unexpected folder %1$s; should be project, " +
   1309                             "\"res\" folder or resource folder", file.getPath());
   1310                     continue;
   1311                 }
   1312             } else if (file.isFile() && LintUtils.isXmlFile(file)) {
   1313                 // Yes, find out its resource type
   1314                 String folderName = file.getParentFile().getName();
   1315                 ResourceFolderType type = ResourceFolderType.getFolderType(folderName);
   1316                 if (type != null) {
   1317                     XmlVisitor visitor = getVisitor(type, xmlDetectors);
   1318                     if (visitor != null) {
   1319                         XmlContext context = new XmlContext(this, project, main, file, type);
   1320                         fireEvent(EventType.SCANNING_FILE, context);
   1321                         visitor.visitFile(context, file);
   1322                     }
   1323                 }
   1324             }
   1325         }
   1326     }
   1327 
   1328     /**
   1329      * Adds a listener to be notified of lint progress
   1330      *
   1331      * @param listener the listener to be added
   1332      */
   1333     public void addLintListener(@NonNull LintListener listener) {
   1334         if (mListeners == null) {
   1335             mListeners = new ArrayList<LintListener>(1);
   1336         }
   1337         mListeners.add(listener);
   1338     }
   1339 
   1340     /**
   1341      * Removes a listener such that it is no longer notified of progress
   1342      *
   1343      * @param listener the listener to be removed
   1344      */
   1345     public void removeLintListener(@NonNull LintListener listener) {
   1346         mListeners.remove(listener);
   1347         if (mListeners.size() == 0) {
   1348             mListeners = null;
   1349         }
   1350     }
   1351 
   1352     /** Notifies listeners, if any, that the given event has occurred */
   1353     private void fireEvent(@NonNull LintListener.EventType type, @NonNull Context context) {
   1354         if (mListeners != null) {
   1355             for (int i = 0, n = mListeners.size(); i < n; i++) {
   1356                 LintListener listener = mListeners.get(i);
   1357                 listener.update(this, type, context);
   1358             }
   1359         }
   1360     }
   1361 
   1362     /**
   1363      * Wrapper around the lint client. This sits in the middle between a
   1364      * detector calling for example
   1365      * {@link LintClient#report(Context, Issue, Location, String, Object)} and
   1366      * the actual embedding tool, and performs filtering etc such that detectors
   1367      * and lint clients don't have to make sure they check for ignored issues or
   1368      * filtered out warnings.
   1369      */
   1370     private class LintClientWrapper extends LintClient {
   1371         @NonNull
   1372         private final LintClient mDelegate;
   1373 
   1374         public LintClientWrapper(@NonNull LintClient delegate) {
   1375             mDelegate = delegate;
   1376         }
   1377 
   1378         @Override
   1379         public void report(
   1380                 @NonNull Context context,
   1381                 @NonNull Issue issue,
   1382                 @NonNull Severity severity,
   1383                 @Nullable Location location,
   1384                 @NonNull String message,
   1385                 @Nullable Object data) {
   1386             Configuration configuration = context.getConfiguration();
   1387             if (!configuration.isEnabled(issue)) {
   1388                 if (issue != IssueRegistry.PARSER_ERROR && issue != IssueRegistry.LINT_ERROR) {
   1389                     mDelegate.log(null, "Incorrect detector reported disabled issue %1$s",
   1390                             issue.toString());
   1391                 }
   1392                 return;
   1393             }
   1394 
   1395             if (configuration.isIgnored(context, issue, location, message, data)) {
   1396                 return;
   1397             }
   1398 
   1399             if (severity == Severity.IGNORE) {
   1400                 return;
   1401             }
   1402 
   1403             mDelegate.report(context, issue, severity, location, message, data);
   1404         }
   1405 
   1406         // Everything else just delegates to the embedding lint client
   1407 
   1408         @Override
   1409         @NonNull
   1410         public Configuration getConfiguration(@NonNull Project project) {
   1411             return mDelegate.getConfiguration(project);
   1412         }
   1413 
   1414 
   1415         @Override
   1416         public void log(@NonNull Severity severity, @Nullable Throwable exception,
   1417                 @Nullable String format, @Nullable Object... args) {
   1418             mDelegate.log(exception, format, args);
   1419         }
   1420 
   1421         @Override
   1422         @NonNull
   1423         public String readFile(@NonNull File file) {
   1424             return mDelegate.readFile(file);
   1425         }
   1426 
   1427         @Override
   1428         @NonNull
   1429         public List<File> getJavaSourceFolders(@NonNull Project project) {
   1430             return mDelegate.getJavaSourceFolders(project);
   1431         }
   1432 
   1433         @Override
   1434         @NonNull
   1435         public List<File> getJavaClassFolders(@NonNull Project project) {
   1436             return mDelegate.getJavaClassFolders(project);
   1437         }
   1438 
   1439         @Override
   1440         public List<File> getJavaLibraries(Project project) {
   1441             return mDelegate.getJavaLibraries(project);
   1442         }
   1443 
   1444         @Override
   1445         @Nullable
   1446         public IDomParser getDomParser() {
   1447             return mDelegate.getDomParser();
   1448         }
   1449 
   1450         @Override
   1451         @NonNull
   1452         public Class<? extends Detector> replaceDetector(Class<? extends Detector> detectorClass) {
   1453             return mDelegate.replaceDetector(detectorClass);
   1454         }
   1455 
   1456         @Override
   1457         @NonNull
   1458         public SdkInfo getSdkInfo(Project project) {
   1459             return mDelegate.getSdkInfo(project);
   1460         }
   1461 
   1462         @Override
   1463         @NonNull
   1464         public Project getProject(@NonNull File dir, @NonNull File referenceDir) {
   1465             return mDelegate.getProject(dir, referenceDir);
   1466         }
   1467 
   1468         @Override
   1469         @Nullable
   1470         public IJavaParser getJavaParser() {
   1471             return mDelegate.getJavaParser();
   1472         }
   1473 
   1474         @Override
   1475         public File findResource(String relativePath) {
   1476             return mDelegate.findResource(relativePath);
   1477         }
   1478     }
   1479 
   1480     /**
   1481      * Requests another pass through the data for the given detector. This is
   1482      * typically done when a detector needs to do more expensive computation,
   1483      * but it only wants to do this once it <b>knows</b> that an error is
   1484      * present, or once it knows more specifically what to check for.
   1485      *
   1486      * @param detector the detector that should be included in the next pass.
   1487      *            Note that the lint runner may refuse to run more than a couple
   1488      *            of runs.
   1489      * @param scope the scope to be revisited. This must be a subset of the
   1490      *       current scope ({@link #getScope()}, and it is just a performance hint;
   1491      *       in particular, the detector should be prepared to be called on other
   1492      *       scopes as well (since they may have been requested by other detectors).
   1493      *       You can pall null to indicate "all".
   1494      */
   1495     public void requestRepeat(@NonNull Detector detector, @Nullable EnumSet<Scope> scope) {
   1496         if (mRepeatingDetectors == null) {
   1497             mRepeatingDetectors = new ArrayList<Detector>();
   1498         }
   1499         mRepeatingDetectors.add(detector);
   1500 
   1501         if (scope != null) {
   1502             if (mRepeatScope == null) {
   1503                 mRepeatScope = scope;
   1504             } else {
   1505                 mRepeatScope = EnumSet.copyOf(mRepeatScope);
   1506                 mRepeatScope.addAll(scope);
   1507             }
   1508         } else {
   1509             mRepeatScope = Scope.ALL;
   1510         }
   1511     }
   1512 
   1513     // Unfortunately, ASMs nodes do not extend a common DOM node type with parent
   1514     // pointers, so we have to have multiple methods which pass in each type
   1515     // of node (class, method, field) to be checked.
   1516 
   1517     // TODO: The Quickfix should look for lint warnings placed *inside* warnings
   1518     // and warn that they won't apply to checks that are bytecode oriented!
   1519 
   1520     /**
   1521      * Returns whether the given issue is suppressed in the given method.
   1522      *
   1523      * @param issue the issue to be checked, or null to just check for "all"
   1524      * @param method the method containing the issue
   1525      * @return true if there is a suppress annotation covering the specific
   1526      *         issue on this method
   1527      */
   1528     public boolean isSuppressed(@Nullable Issue issue, @NonNull MethodNode method) {
   1529         if (method.invisibleAnnotations != null) {
   1530             @SuppressWarnings("unchecked")
   1531             List<AnnotationNode> annotations = method.invisibleAnnotations;
   1532             return isSuppressed(issue, annotations);
   1533         }
   1534 
   1535         return false;
   1536     }
   1537 
   1538     /**
   1539      * Returns whether the given issue is suppressed for the given field.
   1540      *
   1541      * @param issue the issue to be checked, or null to just check for "all"
   1542      * @param field the field potentially annotated with a suppress annotation
   1543      * @return true if there is a suppress annotation covering the specific
   1544      *         issue on this field
   1545      */
   1546     public boolean isSuppressed(@Nullable Issue issue, @NonNull FieldNode field) {
   1547         if (field.invisibleAnnotations != null) {
   1548             @SuppressWarnings("unchecked")
   1549             List<AnnotationNode> annotations = field.invisibleAnnotations;
   1550             return isSuppressed(issue, annotations);
   1551         }
   1552 
   1553         return false;
   1554     }
   1555 
   1556     /**
   1557      * Returns whether the given issue is suppressed in the given class.
   1558      *
   1559      * @param issue the issue to be checked, or null to just check for "all"
   1560      * @param classNode the class containing the issue
   1561      * @return true if there is a suppress annotation covering the specific
   1562      *         issue in this class
   1563      */
   1564     public boolean isSuppressed(@Nullable Issue issue, @NonNull ClassNode classNode) {
   1565         if (classNode.invisibleAnnotations != null) {
   1566             @SuppressWarnings("unchecked")
   1567             List<AnnotationNode> annotations = classNode.invisibleAnnotations;
   1568             return isSuppressed(issue, annotations);
   1569         }
   1570 
   1571         return false;
   1572     }
   1573 
   1574     private boolean isSuppressed(@Nullable Issue issue, List<AnnotationNode> annotations) {
   1575         for (AnnotationNode annotation : annotations) {
   1576             String desc = annotation.desc;
   1577 
   1578             // We could obey @SuppressWarnings("all") too, but no need to look for it
   1579             // because that annotation only has source retention.
   1580 
   1581             if (desc.endsWith(SUPPRESS_LINT_VMSIG)) {
   1582                 if (annotation.values != null) {
   1583                     for (int i = 0, n = annotation.values.size(); i < n; i += 2) {
   1584                         String key = (String) annotation.values.get(i);
   1585                         if (key.equals("value")) {   //$NON-NLS-1$
   1586                             Object value = annotation.values.get(i + 1);
   1587                             if (value instanceof String) {
   1588                                 String id = (String) value;
   1589                                 if (id.equalsIgnoreCase(SUPPRESS_ALL) ||
   1590                                         issue != null && id.equalsIgnoreCase(issue.getId())) {
   1591                                     return true;
   1592                                 }
   1593                             } else if (value instanceof List) {
   1594                                 @SuppressWarnings("rawtypes")
   1595                                 List list = (List) value;
   1596                                 for (Object v : list) {
   1597                                     if (v instanceof String) {
   1598                                         String id = (String) v;
   1599                                         if (id.equalsIgnoreCase(SUPPRESS_ALL) || (issue != null
   1600                                                 && id.equalsIgnoreCase(issue.getId()))) {
   1601                                             return true;
   1602                                         }
   1603                                     }
   1604                                 }
   1605                             }
   1606                         }
   1607                     }
   1608                 }
   1609             }
   1610         }
   1611 
   1612         return false;
   1613     }
   1614 
   1615     /**
   1616      * Returns whether the given issue is suppressed in the given parse tree node.
   1617      *
   1618      * @param issue the issue to be checked, or null to just check for "all"
   1619      * @param scope the AST node containing the issue
   1620      * @return true if there is a suppress annotation covering the specific
   1621      *         issue in this class
   1622      */
   1623     public boolean isSuppressed(@NonNull Issue issue, @Nullable Node scope) {
   1624         while (scope != null) {
   1625             Class<? extends Node> type = scope.getClass();
   1626             // The Lombok AST uses a flat hierarchy of node type implementation classes
   1627             // so no need to do instanceof stuff here.
   1628             if (type == VariableDefinition.class) {
   1629                 // Variable
   1630                 VariableDefinition declaration = (VariableDefinition) scope;
   1631                 if (isSuppressed(issue, declaration.astModifiers())) {
   1632                     return true;
   1633                 }
   1634             } else if (type == MethodDeclaration.class) {
   1635                 // Method
   1636                 // Look for annotations on the method
   1637                 MethodDeclaration declaration = (MethodDeclaration) scope;
   1638                 if (isSuppressed(issue, declaration.astModifiers())) {
   1639                     return true;
   1640                 }
   1641             } else if (type == ClassDeclaration.class) {
   1642                 // Class
   1643                 ClassDeclaration declaration = (ClassDeclaration) scope;
   1644                 if (isSuppressed(issue, declaration.astModifiers())) {
   1645                     return true;
   1646                 }
   1647             }
   1648 
   1649             scope = scope.getParent();
   1650         }
   1651 
   1652         return false;
   1653     }
   1654 
   1655     /**
   1656      * Returns true if the given AST modifier has a suppress annotation for the
   1657      * given issue (which can be null to check for the "all" annotation)
   1658      *
   1659      * @param issue the issue to be checked
   1660      * @param modifiers the modifier to check
   1661      * @return true if the issue or all issues should be suppressed for this
   1662      *         modifier
   1663      */
   1664     private static boolean isSuppressed(@Nullable Issue issue, @NonNull Modifiers modifiers) {
   1665         if (modifiers == null) {
   1666             return false;
   1667         }
   1668         StrictListAccessor<Annotation, Modifiers> annotations = modifiers.astAnnotations();
   1669         if (annotations == null) {
   1670             return false;
   1671         }
   1672 
   1673         Iterator<Annotation> iterator = annotations.iterator();
   1674         while (iterator.hasNext()) {
   1675             Annotation annotation = iterator.next();
   1676             TypeReference t = annotation.astAnnotationTypeReference();
   1677             String typeName = t.getTypeName();
   1678             if (typeName.endsWith(SUPPRESS_LINT)
   1679                     || typeName.endsWith("SuppressWarnings")) {     //$NON-NLS-1$
   1680                 StrictListAccessor<AnnotationElement, Annotation> values =
   1681                         annotation.astElements();
   1682                 if (values != null) {
   1683                     Iterator<AnnotationElement> valueIterator = values.iterator();
   1684                     while (valueIterator.hasNext()) {
   1685                         AnnotationElement element = valueIterator.next();
   1686                         AnnotationValue valueNode = element.astValue();
   1687                         if (valueNode == null) {
   1688                             continue;
   1689                         }
   1690                         if (valueNode instanceof StringLiteral) {
   1691                             StringLiteral literal = (StringLiteral) valueNode;
   1692                             String value = literal.astValue();
   1693                             if (value.equalsIgnoreCase(SUPPRESS_ALL) ||
   1694                                     issue != null && issue.getId().equalsIgnoreCase(value)) {
   1695                                 return true;
   1696                             }
   1697                         } else if (valueNode instanceof ArrayInitializer) {
   1698                             ArrayInitializer array = (ArrayInitializer) valueNode;
   1699                             StrictListAccessor<Expression, ArrayInitializer> expressions =
   1700                                     array.astExpressions();
   1701                             if (expressions == null) {
   1702                                 continue;
   1703                             }
   1704                             Iterator<Expression> arrayIterator = expressions.iterator();
   1705                             while (arrayIterator.hasNext()) {
   1706                                 Expression arrayElement = arrayIterator.next();
   1707                                 if (arrayElement instanceof StringLiteral) {
   1708                                     String value = ((StringLiteral) arrayElement).astValue();
   1709                                     if (value.equalsIgnoreCase(SUPPRESS_ALL) || (issue != null
   1710                                             && issue.getId().equalsIgnoreCase(value))) {
   1711                                         return true;
   1712                                     }
   1713                                 }
   1714                             }
   1715                         }
   1716                     }
   1717                 }
   1718             }
   1719         }
   1720 
   1721         return false;
   1722     }
   1723 
   1724     /**
   1725      * Returns whether the given issue is suppressed in the given XML DOM node.
   1726      *
   1727      * @param issue the issue to be checked, or null to just check for "all"
   1728      * @param node the DOM node containing the issue
   1729      * @return true if there is a suppress annotation covering the specific
   1730      *         issue in this class
   1731      */
   1732     public boolean isSuppressed(@NonNull Issue issue, @Nullable org.w3c.dom.Node node) {
   1733         if (node instanceof Attr) {
   1734             node = ((Attr) node).getOwnerElement();
   1735         }
   1736         while (node != null) {
   1737             if (node.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE) {
   1738                 Element element = (Element) node;
   1739                 if (element.hasAttributeNS(TOOLS_URI, ATTR_IGNORE)) {
   1740                     String ignore = element.getAttributeNS(TOOLS_URI, ATTR_IGNORE);
   1741                     if (ignore.indexOf(',') == -1) {
   1742                         if (ignore.equalsIgnoreCase(SUPPRESS_ALL) || (issue != null
   1743                                 && issue.getId().equalsIgnoreCase(ignore))) {
   1744                             return true;
   1745                         }
   1746                     } else {
   1747                         for (String id : ignore.split(",")) { //$NON-NLS-1$
   1748                             if (id.equalsIgnoreCase(SUPPRESS_ALL) || (issue != null
   1749                                     && issue.getId().equalsIgnoreCase(id))) {
   1750                                 return true;
   1751                             }
   1752                         }
   1753                     }
   1754                 }
   1755             }
   1756 
   1757             node = node.getParentNode();
   1758         }
   1759 
   1760         return false;
   1761     }
   1762 
   1763     /** A pending class to be analyzed by {@link #checkClasses} */
   1764     @VisibleForTesting
   1765     static class ClassEntry implements Comparable<ClassEntry> {
   1766         public final File file;
   1767         public final File jarFile;
   1768         public final File binDir;
   1769         public final byte[] bytes;
   1770 
   1771         public ClassEntry(File file, File jarFile, File binDir, byte[] bytes) {
   1772             super();
   1773             this.file = file;
   1774             this.jarFile = jarFile;
   1775             this.binDir = binDir;
   1776             this.bytes = bytes;
   1777         }
   1778 
   1779         public String path() {
   1780             if (jarFile != null) {
   1781                 return jarFile.getPath() + ':' + file.getPath();
   1782             } else {
   1783                 return file.getPath();
   1784             }
   1785         }
   1786 
   1787         @Override
   1788         public int compareTo(ClassEntry other) {
   1789             String p1 = file.getPath();
   1790             String p2 = other.file.getPath();
   1791             int m1 = p1.length();
   1792             int m2 = p2.length();
   1793             int m = Math.min(m1, m2);
   1794 
   1795             for (int i = 0; i < m; i++) {
   1796                 char c1 = p1.charAt(i);
   1797                 char c2 = p2.charAt(i);
   1798                 if (c1 != c2) {
   1799                     // Sort Foo$Bar.class *after* Foo.class, even though $ < .
   1800                     if (c1 == '.' && c2 == '$') {
   1801                         return -1;
   1802                     }
   1803                     if (c1 == '$' && c2 == '.') {
   1804                         return 1;
   1805                     }
   1806                     return c1 - c2;
   1807                 }
   1808             }
   1809 
   1810             return (m == m1) ? -1 : 1;
   1811         }
   1812 
   1813         @Override
   1814         public String toString() {
   1815             return file.getPath();
   1816         }
   1817     }
   1818 }
   1819