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