• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
LintDriver(@onNull IssueRegistry registry, @NonNull LintClient client)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 */
cancel()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
getScope()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
getClient()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      */
getPhase()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
findProjectFor(@onNull File file)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      */
setAbbreviating(boolean abbreviating)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      */
isAbbreviating()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      */
analyze(@onNull List<File> files, @Nullable EnumSet<Scope> scope)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 
runExtraPhases(Project project)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 
computeRepeatingDetectors(List<Detector> detectors, Project project)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 
computeDetectors(@onNull Project project)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
validateScopeList()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 
registerProjectFile( @onNull Map<File, Project> fileToProject, @NonNull File file, @NonNull File projectDir, @NonNull File rootDir)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 
computeProjects(@onNull List<File> files)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 
addProjects( @onNull File dir, @NonNull Map<File, Project> fileToProject, @NonNull File rootDir)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 
isProjectDir(@onNull File dir)639     private boolean isProjectDir(@NonNull File dir) {
640         return new File(dir, ANDROID_MANIFEST_XML).exists();
641     }
642 
checkProject(@onNull Project project)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 
runFileDetectors(@onNull Project project, @Nullable Project main)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 
checkProGuard(Project project, Project main)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
getSuperClass(@onNull String name)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
union( @ullable List<Detector> list1, @Nullable List<Detector> list2)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 */
checkClasses(Project project, Project main)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 
runClassDetectors(Scope scope, List<ClassEntry> entries, Project project, Project main)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 */
getOuterClassNode(@onNull ClassNode classNode)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 
getSuperMap(List<ClassEntry> libraryEntries, List<ClassEntry> classEntries)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 
addSuperClasses(SuperclassVisitor visitor, List<ClassEntry> entries)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 
SuperclassVisitor(Map<String, String> map)1044         public SuperclassVisitor(Map<String, String> map) {
1045             super(ASM4);
1046             mMap = map;
1047         }
1048 
1049         @Override
visit(int version, int access, String name, String signature, String superName, String[] interfaces)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 
findClasses( @onNull List<ClassEntry> entries, @NonNull List<File> classPath)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 
runClassDetectors(ClassContext context, @NonNull List<Detector> checks)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 
addClassFiles(@onNull File dir, @NonNull List<File> classFiles)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 
checkJava( @onNull Project project, @Nullable Project main, @NonNull List<File> sourceFolders, @NonNull List<Detector> checks)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 
gatherJavaFiles(@onNull File dir, @NonNull List<File> result)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
getVisitor( @onNull ResourceFolderType type, @NonNull List<ResourceXmlDetector> checks)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 
checkResFolder( @onNull Project project, @Nullable Project main, @NonNull File res, @NonNull List<ResourceXmlDetector> checks)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 
checkResourceFolder( @onNull Project project, @Nullable Project main, @NonNull File dir, @NonNull ResourceFolderType type, @NonNull List<ResourceXmlDetector> checks)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 */
checkIndividualResources( @onNull Project project, @Nullable Project main, @NonNull List<ResourceXmlDetector> xmlDetectors, @NonNull List<File> files)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      */
addLintListener(@onNull LintListener listener)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      */
removeLintListener(@onNull LintListener listener)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 */
fireEvent(@onNull LintListener.EventType type, @NonNull Context context)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 
LintClientWrapper(@onNull LintClient delegate)1374         public LintClientWrapper(@NonNull LintClient delegate) {
1375             mDelegate = delegate;
1376         }
1377 
1378         @Override
report( @onNull Context context, @NonNull Issue issue, @NonNull Severity severity, @Nullable Location location, @NonNull String message, @Nullable Object data)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
getConfiguration(@onNull Project project)1410         public Configuration getConfiguration(@NonNull Project project) {
1411             return mDelegate.getConfiguration(project);
1412         }
1413 
1414 
1415         @Override
log(@onNull Severity severity, @Nullable Throwable exception, @Nullable String format, @Nullable Object... args)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
readFile(@onNull File file)1423         public String readFile(@NonNull File file) {
1424             return mDelegate.readFile(file);
1425         }
1426 
1427         @Override
1428         @NonNull
getJavaSourceFolders(@onNull Project project)1429         public List<File> getJavaSourceFolders(@NonNull Project project) {
1430             return mDelegate.getJavaSourceFolders(project);
1431         }
1432 
1433         @Override
1434         @NonNull
getJavaClassFolders(@onNull Project project)1435         public List<File> getJavaClassFolders(@NonNull Project project) {
1436             return mDelegate.getJavaClassFolders(project);
1437         }
1438 
1439         @Override
getJavaLibraries(Project project)1440         public List<File> getJavaLibraries(Project project) {
1441             return mDelegate.getJavaLibraries(project);
1442         }
1443 
1444         @Override
1445         @Nullable
getDomParser()1446         public IDomParser getDomParser() {
1447             return mDelegate.getDomParser();
1448         }
1449 
1450         @Override
1451         @NonNull
replaceDetector(Class<? extends Detector> detectorClass)1452         public Class<? extends Detector> replaceDetector(Class<? extends Detector> detectorClass) {
1453             return mDelegate.replaceDetector(detectorClass);
1454         }
1455 
1456         @Override
1457         @NonNull
getSdkInfo(Project project)1458         public SdkInfo getSdkInfo(Project project) {
1459             return mDelegate.getSdkInfo(project);
1460         }
1461 
1462         @Override
1463         @NonNull
getProject(@onNull File dir, @NonNull File referenceDir)1464         public Project getProject(@NonNull File dir, @NonNull File referenceDir) {
1465             return mDelegate.getProject(dir, referenceDir);
1466         }
1467 
1468         @Override
1469         @Nullable
getJavaParser()1470         public IJavaParser getJavaParser() {
1471             return mDelegate.getJavaParser();
1472         }
1473 
1474         @Override
findResource(String relativePath)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      */
requestRepeat(@onNull Detector detector, @Nullable EnumSet<Scope> scope)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      */
isSuppressed(@ullable Issue issue, @NonNull MethodNode method)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      */
isSuppressed(@ullable Issue issue, @NonNull FieldNode field)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      */
isSuppressed(@ullable Issue issue, @NonNull ClassNode classNode)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 
isSuppressed(@ullable Issue issue, List<AnnotationNode> annotations)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      */
isSuppressed(@onNull Issue issue, @Nullable Node scope)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      */
isSuppressed(@ullable Issue issue, @NonNull Modifiers modifiers)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      */
isSuppressed(@onNull Issue issue, @Nullable org.w3c.dom.Node node)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 
ClassEntry(File file, File jarFile, File binDir, byte[] bytes)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 
path()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
compareTo(ClassEntry other)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
toString()1814         public String toString() {
1815             return file.getPath();
1816         }
1817     }
1818 }
1819