| /* |
| * Copyright (C) 2011 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.tools.lint.client.api |
| |
| import com.android.SdkConstants.ATTR_IGNORE |
| import com.android.SdkConstants.CLASS_CONSTRUCTOR |
| import com.android.SdkConstants.CONSTRUCTOR_NAME |
| import com.android.SdkConstants.DOT_CLASS |
| import com.android.SdkConstants.DOT_JAR |
| import com.android.SdkConstants.DOT_JAVA |
| import com.android.SdkConstants.DOT_KT |
| import com.android.SdkConstants.DOT_KTS |
| import com.android.SdkConstants.FQCN_SUPPRESS_LINT |
| import com.android.SdkConstants.KOTLIN_SUPPRESS |
| import com.android.SdkConstants.RES_FOLDER |
| import com.android.SdkConstants.SUPPRESS_ALL |
| import com.android.SdkConstants.SUPPRESS_LINT |
| import com.android.SdkConstants.TOOLS_URI |
| import com.android.SdkConstants.VALUE_TRUE |
| import com.android.annotations.VisibleForTesting |
| import com.android.ide.common.repository.GradleCoordinate |
| import com.android.ide.common.repository.GradleVersion |
| import com.android.ide.common.repository.ResourceVisibilityLookup |
| import com.android.ide.common.resources.ResourceItem |
| import com.android.ide.common.resources.ResourceRepository |
| import com.android.ide.common.resources.configuration.FolderConfiguration.QUALIFIER_SPLITTER |
| import com.android.repository.api.ProgressIndicator |
| import com.android.resources.ResourceFolderType |
| import com.android.sdklib.BuildToolInfo |
| import com.android.sdklib.IAndroidTarget |
| import com.android.sdklib.repository.AndroidSdkHandler |
| import com.android.tools.lint.client.api.LintListener.EventType |
| import com.android.tools.lint.detector.api.BinaryResourceScanner |
| import com.android.tools.lint.detector.api.Category |
| import com.android.tools.lint.detector.api.ClassContext |
| import com.android.tools.lint.detector.api.ClassScanner |
| import com.android.tools.lint.detector.api.Context |
| import com.android.tools.lint.detector.api.Detector |
| import com.android.tools.lint.detector.api.GradleContext |
| import com.android.tools.lint.detector.api.GradleScanner |
| import com.android.tools.lint.detector.api.Issue |
| import com.android.tools.lint.detector.api.JavaContext |
| import com.android.tools.lint.detector.api.LintFix |
| import com.android.tools.lint.detector.api.Location |
| import com.android.tools.lint.detector.api.OtherFileScanner |
| import com.android.tools.lint.detector.api.Platform |
| import com.android.tools.lint.detector.api.Project |
| import com.android.tools.lint.detector.api.ResourceContext |
| import com.android.tools.lint.detector.api.ResourceFolderScanner |
| import com.android.tools.lint.detector.api.Scope |
| import com.android.tools.lint.detector.api.Severity |
| import com.android.tools.lint.detector.api.SourceCodeScanner |
| import com.android.tools.lint.detector.api.TextFormat |
| import com.android.tools.lint.detector.api.XmlContext |
| import com.android.tools.lint.detector.api.XmlScanner |
| import com.android.tools.lint.detector.api.assertionsEnabled |
| import com.android.tools.lint.detector.api.formatList |
| import com.android.tools.lint.detector.api.getCommonParent |
| import com.android.tools.lint.detector.api.getNextInstruction |
| import com.android.tools.lint.detector.api.isAnonymousClass |
| import com.android.tools.lint.detector.api.isXmlFile |
| import com.android.utils.Pair |
| import com.android.utils.SdkUtils.isBitmapFile |
| import com.google.common.annotations.Beta |
| import com.google.common.base.Objects |
| import com.google.common.base.Splitter |
| import com.google.common.collect.ArrayListMultimap |
| import com.google.common.collect.Iterables |
| import com.google.common.collect.Sets |
| import com.intellij.openapi.progress.ProcessCanceledException |
| import com.intellij.openapi.project.IndexNotReadyException |
| import com.intellij.openapi.util.Computable |
| import com.intellij.openapi.util.io.FileUtil |
| import com.intellij.psi.PsiAnnotationMemberValue |
| import com.intellij.psi.PsiArrayInitializerExpression |
| import com.intellij.psi.PsiArrayInitializerMemberValue |
| import com.intellij.psi.PsiCompiledElement |
| import com.intellij.psi.PsiElement |
| import com.intellij.psi.PsiFile |
| import com.intellij.psi.PsiLiteral |
| import com.intellij.psi.PsiModifierList |
| import com.intellij.psi.PsiModifierListOwner |
| import org.jetbrains.annotations.Contract |
| import org.jetbrains.uast.UAnnotated |
| import org.jetbrains.uast.UCallExpression |
| import org.jetbrains.uast.UElement |
| import org.jetbrains.uast.UExpression |
| import org.jetbrains.uast.UFile |
| import org.jetbrains.uast.ULiteralExpression |
| import org.jetbrains.uast.getParentOfType |
| import org.objectweb.asm.ClassReader |
| import org.objectweb.asm.Opcodes |
| import org.objectweb.asm.tree.AbstractInsnNode |
| import org.objectweb.asm.tree.AnnotationNode |
| import org.objectweb.asm.tree.ClassNode |
| import org.objectweb.asm.tree.FieldInsnNode |
| import org.objectweb.asm.tree.FieldNode |
| import org.objectweb.asm.tree.MethodInsnNode |
| import org.objectweb.asm.tree.MethodNode |
| import org.w3c.dom.Attr |
| import org.w3c.dom.Document |
| import org.w3c.dom.Element |
| import java.io.File |
| import java.io.IOException |
| import java.net.URL |
| import java.net.URLConnection |
| import java.util.ArrayDeque |
| import java.util.ArrayList |
| import java.util.Arrays |
| import java.util.Deque |
| import java.util.EnumMap |
| import java.util.EnumSet |
| import java.util.HashMap |
| import java.util.HashSet |
| import java.util.IdentityHashMap |
| import java.util.LinkedHashMap |
| import java.util.function.Predicate |
| import java.util.regex.Pattern |
| import kotlin.system.measureTimeMillis |
| |
| /** |
| * Analyzes Android projects and files |
| * |
| * |
| * **NOTE: This is not a public or final API; if you rely on this be prepared |
| * to adjust your code for the next tools release.** |
| */ |
| @Beta |
| class LintDriver |
| /** |
| * Creates a new [LintDriver] |
| * |
| * @param registry The registry containing issues to be checked |
| * |
| * @param request The request which points to the original files to be checked, |
| * the original scope, the original [LintClient], as well as the release mode. |
| * |
| * @param client the tool wrapping the analyzer, such as an IDE or a CLI |
| */ |
| (var registry: IssueRegistry, client: LintClient, val request: LintRequest) { |
| /** True if execution has been canceled */ |
| @Volatile |
| internal var isCanceled: Boolean = false |
| private set |
| |
| /** The original client (not the wrapped one intended to pass to detectors */ |
| private val realClient: LintClient = client |
| |
| /** |
| * Stashed circular project (we need to report this but can't report it |
| * at the early stage during initialization where this is detected). Cleared |
| * once reported. |
| */ |
| private var circularProjectError: CircularDependencyException? = null |
| |
| /** The associated [LintClient] */ |
| val client: LintClient = LintClientWrapper(client) |
| |
| private val projectRoots: Collection<Project> |
| |
| init { |
| projectRoots = |
| try { |
| request.getProjects() ?: computeProjects(request.files) |
| } catch (e: CircularDependencyException) { |
| circularProjectError = e |
| emptyList() |
| } |
| } |
| |
| /** |
| * The scope for the lint job |
| */ |
| var scope: EnumSet<Scope> = request.getScope() ?: Scope.infer(projectRoots) |
| |
| /** |
| * The relevant platforms lint is to run on. By default, |
| * this is [Platform.ANDROID_SET]. Note that within an |
| * Android project there may be non-Android libraries, but this |
| * flag indicates whether there's any Android sources. |
| */ |
| var platforms: EnumSet<Platform> = request.getPlatform() ?: Platform.ANDROID_SET |
| |
| private lateinit var applicableDetectors: List<Detector> |
| private lateinit var scopeDetectors: Map<Scope, MutableList<Detector>> |
| private var listeners: MutableList<LintListener>? = null |
| |
| /** |
| * Returns the current phase number. The first pass is numbered 1. Only one pass |
| * will be performed, unless a [Detector] calls [.requestRepeat]. |
| * |
| * @return the current phase, usually 1 |
| */ |
| var phase: Int = 0 |
| private set |
| |
| private var repeatingDetectors: MutableList<Detector>? = null |
| private var repeatScope: EnumSet<Scope>? = null |
| private var currentProjects: Array<Project>? = null |
| private var currentProject: Project? = null |
| |
| /** |
| * Whether lint should abbreviate output when appropriate. |
| */ |
| var isAbbreviating = true |
| |
| /** Whether to allow suppressing issues with restrictions ([Issue.suppressNames]) */ |
| var allowSuppress = false |
| |
| private var parserErrors: Boolean = false |
| /** Whether we should run all normal checks on test sources */ |
| var checkTestSources: Boolean = false |
| /** |
| * Whether we should run any checks (including tests marked with [Scope.TEST_SOURCES] |
| * on test sources |
| */ |
| var ignoreTestSources: Boolean = false |
| /** Whether we should include generated sources in the analysis */ |
| var checkGeneratedSources: Boolean = false |
| /** Whether we're only analyzing fatal-severity issues */ |
| var fatalOnlyMode: Boolean = false |
| /** Baseline to apply to the analysis */ |
| var baseline: LintBaseline? = null |
| /** Whether dependent projects should be checked */ |
| var checkDependencies = true |
| |
| /** Cancels the current lint run as soon as possible */ |
| fun cancel() { |
| isCanceled = true |
| } |
| |
| /** Time the analysis started */ |
| var analysisStartTime = System.currentTimeMillis() |
| |
| /** Count of files the driver has encountered (intended for analytics) */ |
| var fileCount = 0 |
| /** Count of modules the driver has encountered (intended for analytics) */ |
| var moduleCount = 0 |
| /** Number of Java sources to encountered in source or test folders */ |
| var javaFileCount = 0 |
| /** Number of Kotlin sources to encountered in source or test folders */ |
| var kotlinFileCount = 0 |
| /** Number of resource files (XML or bitmaps) encountered in res folders */ |
| var resourceFileCount = 0 |
| /** Number of source files encountered in test folders */ |
| var testSourceCount = 0 |
| |
| /** Time to initialize the lint project */ |
| var initializeTimeMs = 0L |
| /** Time to register custom detectors */ |
| var registerCustomDetectorsTimeMs = 0L |
| /** Time to compute the applicable detectors */ |
| var computeDetectorsTimeMs = 0L |
| /** Time to run the first round of checks */ |
| var checkProjectTimeMs = 0L |
| /** Time to run any extra phases */ |
| var extraPhasesTimeMs = 0L |
| /** Time to report baseline issues */ |
| var reportBaselineIssuesTimeMs = 0L |
| /** Time to dispose projects */ |
| var disposeProjectsTimeMs = 0L |
| /** Time to generate reports */ |
| var reportGenerationTimeMs = 0L |
| |
| /** |
| * Returns the project containing a given file, or null if not found. This searches |
| * only among the currently checked project and its library projects, not among all |
| * possible projects being scanned sequentially. |
| * |
| * @param file the file to be checked |
| * |
| * @return the corresponding project, or null if not found |
| */ |
| fun findProjectFor(file: File): Project? { |
| val projects = currentProjects ?: return null |
| if (projects.size == 1) { |
| return projects[0] |
| } |
| val path = file.path |
| for (project in projects) { |
| if (path.startsWith(project.dir.path)) { |
| return project |
| } |
| } |
| |
| return null |
| } |
| |
| /** |
| * Returns whether lint has encountered any files with fatal parser errors |
| * (e.g. broken source code, or even broken parsers) |
| * |
| * This is useful for checks that need to make sure they've seen all data in |
| * order to be conclusive (such as an unused resource check). |
| * |
| * @return true if any files were not properly processed because they |
| * contained parser errors |
| */ |
| fun hasParserErrors(): Boolean = parserErrors |
| |
| /** |
| * Sets whether lint has encountered files with fatal parser errors. |
| * |
| * @see .hasParserErrors |
| * @param hasErrors whether parser errors have been encountered |
| */ |
| fun setHasParserErrors(hasErrors: Boolean) { |
| parserErrors = hasErrors |
| } |
| |
| /** |
| * Returns the projects being analyzed |
| * |
| * @return the projects being analyzed |
| */ |
| val projects: List<Project> |
| get() { |
| val p = currentProjects ?: return emptyList() |
| return Arrays.asList(*p) |
| } |
| |
| /** |
| * Analyze the given files (which can point to Android projects or directories |
| * containing Android projects). Issues found are reported to the associated |
| * [LintClient]. |
| * |
| * |
| * Note that the [LintDriver] is not multi thread safe or re-entrant; |
| * if you want to run potentially overlapping lint jobs, create a separate driver |
| * for each job. |
| */ |
| fun analyze() { |
| isCanceled = false |
| assert(!scope.contains(Scope.ALL_RESOURCE_FILES) || scope.contains(Scope.RESOURCE_FILE)) |
| |
| circularProjectError?.let { |
| val project = it.project |
| if (project != null) { |
| currentProject = project |
| LintClient.report( |
| client = client, issue = IssueRegistry.LINT_ERROR, |
| message = it.message ?: "Circular project dependencies", |
| project = project, driver = this, location = it.location |
| ) |
| currentProject = null |
| } |
| circularProjectError = null |
| return |
| } |
| |
| val projects = projectRoots |
| if (projects.isEmpty()) { |
| client.log(null, "No projects found for %1\$s", request.files.toString()) |
| return |
| } |
| initializeTimeMs += measureTimeMillis { |
| realClient.performInitializeProjects(projects) |
| } |
| |
| if (isCanceled) { |
| realClient.performDisposeProjects(projects) |
| return |
| } |
| |
| for (project in projects) { |
| fireEvent(EventType.REGISTERED_PROJECT, project = project) |
| } |
| |
| registerCustomDetectorsTimeMs += measureTimeMillis { |
| registerCustomDetectors(projects) |
| } |
| |
| // See if the lint.xml file specifies a baseline and we're not in incremental mode |
| if (baseline == null && scope.size > 2) { |
| val lastProject = Iterables.getLast(projects) |
| val mainConfiguration = client.getConfiguration(lastProject, this) |
| val baselineFile = mainConfiguration.baselineFile |
| if (baselineFile != null) { |
| baseline = LintBaseline(client, baselineFile) |
| } |
| } |
| |
| fireEvent(EventType.STARTING, null) |
| |
| try { |
| for (project in projects) { |
| phase = 1 |
| |
| val main = request.getMainProject(project) |
| |
| // The set of available detectors varies between projects |
| computeDetectorsTimeMs += measureTimeMillis { |
| computeDetectors(project) |
| } |
| |
| if (applicableDetectors.isEmpty()) { |
| // No detectors enabled in this project: skip it |
| continue |
| } |
| |
| checkProjectTimeMs += measureTimeMillis { |
| checkProject(project, main) |
| } |
| if (isCanceled) { |
| break |
| } |
| |
| extraPhasesTimeMs += measureTimeMillis { |
| runExtraPhases(project, main) |
| } |
| } |
| } catch (throwable: Throwable) { |
| // Process canceled etc |
| if (!handleDetectorError(null, this, throwable)) { |
| cancel() |
| } |
| } |
| |
| val baseline = this.baseline |
| if (baseline != null && !isCanceled) { |
| val lastProject = Iterables.getLast(projects) |
| val main = request.getMainProject(lastProject) |
| reportBaselineIssuesTimeMs += measureTimeMillis { |
| baseline.reportBaselineIssues(this, main) |
| } |
| } |
| |
| fireEvent(if (isCanceled) EventType.CANCELED else EventType.COMPLETED, null) |
| disposeProjectsTimeMs += measureTimeMillis { |
| realClient.performDisposeProjects(projects) |
| } |
| } |
| |
| private fun registerCustomDetectors(projects: Collection<Project>) { |
| // Look at the various projects, and if any of them provide a custom |
| // lint jar, "add" them (this will replace the issue registry with |
| // a CompositeIssueRegistry containing the original issue registry |
| // plus JarFileIssueRegistry instances for each lint jar |
| val jarFiles = Sets.newHashSet<File>() |
| for (project in projects) { |
| jarFiles.addAll(client.findRuleJars(project)) |
| for (library in project.allLibraries) { |
| jarFiles.addAll(client.findRuleJars(library)) |
| } |
| } |
| |
| jarFiles.addAll(client.findGlobalRuleJars()) |
| |
| if (!jarFiles.isEmpty()) { |
| val extraRegistries = JarFileIssueRegistry.get( |
| client, jarFiles, |
| currentProject ?: projects.firstOrNull() |
| ) |
| if (extraRegistries.isNotEmpty()) { |
| val registries = ArrayList<IssueRegistry>(jarFiles.size + 1) |
| // Include the builtin checks too |
| registries.add(registry) |
| for (extraRegistry in extraRegistries) { |
| registries.add(extraRegistry) |
| } |
| registry = CompositeIssueRegistry(registries) |
| } |
| } |
| } |
| |
| private fun runExtraPhases(project: Project, main: Project) { |
| // Did any detectors request another phase? |
| repeatingDetectors ?: return |
| |
| // Yes. Iterate up to MAX_PHASES times. |
| |
| // During the extra phases, we might be narrowing the scope, and setting it in the |
| // scope field such that detectors asking about the available scope will get the |
| // correct result. However, we need to restore it to the original scope when this |
| // is done in case there are other projects that will be checked after this, since |
| // the repeated phases is done *per project*, not after all projects have been |
| // processed. |
| val oldScope = scope |
| |
| do { |
| phase++ |
| fireEvent( |
| EventType.NEW_PHASE, |
| Context(this, project, null, project.dir) |
| ) |
| |
| // Narrow the scope down to the set of scopes requested by |
| // the rules. |
| if (repeatScope == null) { |
| repeatScope = Scope.ALL |
| } |
| scope = Scope.intersect(scope, repeatScope!!) |
| if (scope.isEmpty()) { |
| break |
| } |
| |
| // Compute the detectors to use for this pass. |
| // Unlike the normal computeDetectors(project) call, |
| // this is going to use the existing instances, and include |
| // those that apply for the configuration. |
| repeatingDetectors?.let { computeRepeatingDetectors(it, project) } |
| |
| if (applicableDetectors.isEmpty()) { |
| // No detectors enabled in this project: skip it |
| continue |
| } |
| |
| checkProject(project, main) |
| if (isCanceled) { |
| break |
| } |
| } while (phase < MAX_PHASES && repeatingDetectors != null) |
| |
| scope = oldScope |
| } |
| |
| private fun computeRepeatingDetectors(detectors: List<Detector>, project: Project) { |
| // Ensure that the current visitor is recomputed |
| currentFolderType = null |
| currentVisitor = null |
| currentXmlDetectors = null |
| currentBinaryDetectors = null |
| |
| // Create map from detector class to issue such that we can |
| // compute applicable issues for each detector in the list of detectors |
| // to be repeated |
| val issues = registry.issues |
| val issueMap = ArrayListMultimap.create<Class<out Detector>, Issue>(issues.size, 3) |
| for (issue in issues) { |
| issueMap.put(issue.implementation.detectorClass, issue) |
| } |
| |
| val detectorToScope = HashMap<Class<out Detector>, EnumSet<Scope>>() |
| val scopeToDetectors: MutableMap<Scope, MutableList<Detector>> = |
| EnumMap<Scope, MutableList<Detector>>(Scope::class.java) |
| |
| val detectorList = ArrayList<Detector>() |
| // Compute the list of detectors (narrowed down from repeatingDetectors), |
| // and simultaneously build up the detectorToScope map which tracks |
| // the scopes each detector is affected by (this is used to populate |
| // the scopeDetectors map which is used during iteration). |
| val configuration = project.getConfiguration(this) |
| for (detector in detectors) { |
| val detectorClass = detector.javaClass |
| val detectorIssues = issueMap.get(detectorClass) |
| if (detectorIssues != null) { |
| var add = false |
| for (issue in detectorIssues) { |
| // The reason we have to check whether the detector is enabled |
| // is that this is a per-project property, so when running lint in multiple |
| // projects, a detector enabled only in a different project could have |
| // requested another phase, and we end up in this project checking whether |
| // the detector is enabled here. |
| if (!configuration.isEnabled(issue)) { |
| continue |
| } |
| |
| add = true // Include detector if any of its issues are enabled |
| |
| val s = detectorToScope[detectorClass] |
| val issueScope = issue.implementation.scope |
| if (s == null) { |
| detectorToScope[detectorClass] = issueScope |
| } else if (!s.containsAll(issueScope)) { |
| val union = EnumSet.copyOf(s) |
| union.addAll(issueScope) |
| detectorToScope[detectorClass] = union |
| } |
| } |
| |
| if (add) { |
| detectorList.add(detector) |
| val union = detectorToScope[detector.javaClass] |
| if (union != null) { |
| for (s in union) { |
| var list: MutableList<Detector>? = scopeToDetectors[s] |
| if (list == null) { |
| list = ArrayList() |
| scopeToDetectors[s] = list |
| } |
| list.add(detector) |
| } |
| } |
| } |
| } |
| } |
| |
| applicableDetectors = detectorList |
| scopeDetectors = scopeToDetectors |
| repeatingDetectors = null |
| repeatScope = null |
| |
| validateScopeList() |
| } |
| |
| private fun computeDetectors(project: Project) { |
| // Ensure that the current visitor is recomputed |
| currentFolderType = null |
| currentVisitor = null |
| |
| val configuration = project.getConfiguration(this) |
| val map = EnumMap<Scope, MutableList<Detector>>(Scope::class.java) |
| scopeDetectors = map |
| applicableDetectors = registry.createDetectors(client, configuration, scope, platforms, map) |
| |
| validateScopeList() |
| } |
| |
| /** Development diagnostics only, run with assertions on */ |
| private // Turn off warnings for the intentional assertion side effect below |
| fun validateScopeList() { |
| if (assertionsEnabled()) { |
| val resourceFileDetectors = scopeDetectors[Scope.RESOURCE_FILE] |
| if (resourceFileDetectors != null) { |
| for (detector in resourceFileDetectors) { |
| assert(detector is XmlScanner) { detector } |
| } |
| } |
| |
| val manifestDetectors = scopeDetectors[Scope.MANIFEST] |
| if (manifestDetectors != null) { |
| for (detector in manifestDetectors) { |
| assert(detector is XmlScanner) { detector } |
| } |
| } |
| |
| val javaCodeDetectors = scopeDetectors[Scope.ALL_JAVA_FILES] |
| if (javaCodeDetectors != null) { |
| for (detector in javaCodeDetectors) { |
| assert(detector is SourceCodeScanner) { detector } |
| } |
| } |
| |
| val javaFileDetectors = scopeDetectors[Scope.JAVA_FILE] |
| if (javaFileDetectors != null) { |
| for (detector in javaFileDetectors) { |
| assert(detector is SourceCodeScanner) { detector } |
| } |
| } |
| |
| val classDetectors = scopeDetectors[Scope.CLASS_FILE] |
| if (classDetectors != null) { |
| for (detector in classDetectors) { |
| assert(detector is ClassScanner) { detector } |
| } |
| } |
| |
| val classCodeDetectors = scopeDetectors[Scope.ALL_CLASS_FILES] |
| if (classCodeDetectors != null) { |
| for (detector in classCodeDetectors) { |
| assert(detector is ClassScanner) { detector } |
| } |
| } |
| |
| val gradleDetectors = scopeDetectors[Scope.GRADLE_FILE] |
| if (gradleDetectors != null) { |
| for (detector in gradleDetectors) { |
| assert(detector is GradleScanner) { detector } |
| } |
| } |
| |
| val otherDetectors = scopeDetectors[Scope.OTHER] |
| if (otherDetectors != null) { |
| for (detector in otherDetectors) { |
| assert(detector is OtherFileScanner) { detector } |
| } |
| } |
| |
| val dirDetectors = scopeDetectors[Scope.RESOURCE_FOLDER] |
| if (dirDetectors != null) { |
| for (detector in dirDetectors) { |
| assert(detector is ResourceFolderScanner) { detector } |
| } |
| } |
| |
| val binaryDetectors = scopeDetectors[Scope.BINARY_RESOURCE_FILE] |
| if (binaryDetectors != null) { |
| for (detector in binaryDetectors) { |
| assert(detector is BinaryResourceScanner) { detector } |
| } |
| } |
| } |
| } |
| |
| private fun registerProjectFile( |
| fileToProject: MutableMap<File, Project>, |
| file: File, |
| projectDir: File, |
| rootDir: File |
| ) { |
| fileToProject[file] = client.getProject(projectDir, rootDir) |
| } |
| |
| private fun computeProjects(relativeFiles: List<File>): Collection<Project> { |
| // Compute list of projects |
| val fileToProject = LinkedHashMap<File, Project>() |
| |
| var sharedRoot: File? = null |
| |
| // Ensure that we have absolute paths such that if you lint |
| // "foo bar" in "baz" we can show baz/ as the root |
| val absolute = ArrayList<File>(relativeFiles.size) |
| for (file in relativeFiles) { |
| absolute.add(file.absoluteFile) |
| } |
| // Always use absoluteFiles so that we can check the file's getParentFile() |
| // which is null if the file is not absolute. |
| @Suppress("UnnecessaryVariable") |
| val files = absolute |
| |
| if (files.size > 1) { |
| sharedRoot = getCommonParent(files) |
| if (sharedRoot != null && sharedRoot.parentFile == null) { // "/" ? |
| sharedRoot = null |
| } |
| } |
| |
| for (file in files) { |
| if (file.isDirectory) { |
| var rootDir = sharedRoot |
| if (rootDir == null) { |
| rootDir = file |
| if (files.size > 1) { |
| rootDir = file.parentFile |
| if (rootDir == null) { |
| rootDir = file |
| } |
| } |
| } |
| |
| // Figure out what to do with a directory. Note that the meaning of the |
| // directory can be ambiguous: |
| // If you pass a directory which is unknown, we don't know if we should |
| // search upwards (in case you're pointing at a deep java package folder |
| // within the project), or if you're pointing at some top level directory |
| // containing lots of projects you want to scan. We attempt to do the |
| // right thing, which is to see if you're pointing right at a project or |
| // right within it (say at the src/ or res/) folder, and if not, you're |
| // hopefully pointing at a project tree that you want to scan recursively. |
| if (client.isProjectDirectory(file)) { |
| registerProjectFile(fileToProject, file, file, rootDir) |
| continue |
| } else { |
| var parent: File? = file.parentFile |
| if (parent != null) { |
| if (client.isProjectDirectory(parent)) { |
| registerProjectFile(fileToProject, file, parent, parent) |
| continue |
| } else { |
| parent = parent.parentFile |
| if (parent != null && client.isProjectDirectory(parent)) { |
| registerProjectFile(fileToProject, file, parent, parent) |
| continue |
| } |
| } |
| } |
| |
| // Search downwards for nested projects |
| addProjects(file, fileToProject, rootDir) |
| } |
| } else { |
| // Pointed at a file: Search upwards for the containing project |
| var parent: File? = file.parentFile |
| while (parent != null) { |
| if (client.isProjectDirectory(parent)) { |
| registerProjectFile(fileToProject, file, parent, parent) |
| break |
| } |
| parent = parent.parentFile |
| } |
| } |
| |
| if (isCanceled) { |
| return emptySet() |
| } |
| } |
| |
| for ((file, project) in fileToProject) { |
| if (file != project.dir) { |
| if (file.isDirectory) { |
| try { |
| val dir = file.canonicalFile |
| if (dir == project.dir) { |
| continue |
| } |
| } catch (ioe: IOException) { |
| // pass |
| } |
| } |
| |
| project.addFile(file) |
| } |
| } |
| |
| // Partition the projects up such that we only return projects that aren't |
| // included by other projects (e.g. because they are library projects) |
| |
| val allProjects = fileToProject.values |
| val roots = HashSet(allProjects) |
| for (project in allProjects) { |
| roots.removeAll(project.allLibraries) |
| } |
| |
| // Report issues for all projects that are explicitly referenced. We need to |
| // do this here, since the project initialization will mark all library |
| // projects as no-report projects by default. |
| for (project in allProjects) { |
| // Report issues for all projects explicitly listed or found via a directory |
| // traversal -- including library projects. |
| project.reportIssues = true |
| } |
| |
| if (assertionsEnabled()) { |
| // Make sure that all the project directories are unique. This ensures |
| // that we didn't accidentally end up with different project instances |
| // for a library project discovered as a directory as well as one |
| // initialized from the library project dependency list |
| val projects = IdentityHashMap<Project, Project>() |
| for (project in roots) { |
| projects[project] = project |
| for (library in project.allLibraries) { |
| projects[library] = library |
| } |
| } |
| val dirs = HashSet<File>() |
| for (project in projects.keys) { |
| assert(!dirs.contains(project.dir)) |
| dirs.add(project.dir) |
| } |
| } |
| |
| return roots |
| } |
| |
| private fun addProjects( |
| dir: File, |
| fileToProject: MutableMap<File, Project>, |
| rootDir: File |
| ) { |
| if (isCanceled) { |
| return |
| } |
| |
| if (client.isProjectDirectory(dir)) { |
| registerProjectFile(fileToProject, dir, dir, rootDir) |
| } else { |
| val files = dir.listFiles() |
| if (files != null) { |
| for (file in files) { |
| if (file.isDirectory) { |
| addProjects(file, fileToProject, rootDir) |
| } |
| } |
| } |
| } |
| } |
| |
| private fun checkProject(project: Project, main: Project) { |
| val projectDir = project.dir |
| |
| val projectContext = Context(this, project, null, projectDir) |
| fireEvent(EventType.SCANNING_PROJECT, projectContext) |
| |
| val allLibraries = project.allLibraries |
| val allProjects = HashSet<Project>(allLibraries.size + 1) |
| allProjects.add(project) |
| allProjects.addAll(allLibraries) |
| currentProjects = allProjects.toTypedArray() |
| |
| currentProject = project |
| |
| for (check in applicableDetectors) { |
| check.beforeCheckRootProject(projectContext) |
| check.beforeCheckEachProject(projectContext) |
| if (isCanceled) { |
| return |
| } |
| } |
| |
| assert(currentProject === project) |
| runFileDetectors(project, main) |
| if (isCanceled) { |
| return |
| } |
| |
| runDelayedRunnables() |
| |
| if (checkDependencies && !Scope.checkSingleFile(scope)) { |
| val libraries = project.allLibraries |
| for (library in libraries) { |
| val libraryContext = Context(this, library, project, projectDir) |
| fireEvent(EventType.SCANNING_LIBRARY_PROJECT, libraryContext) |
| currentProject = library |
| |
| for (check in applicableDetectors) { |
| check.beforeCheckEachProject(libraryContext) |
| if (isCanceled) { |
| return |
| } |
| } |
| assert(currentProject === library) |
| |
| runFileDetectors(library, main) |
| if (isCanceled) { |
| return |
| } |
| assert(currentProject === library) |
| |
| runDelayedRunnables() |
| |
| for (check in applicableDetectors) { |
| check.afterCheckEachProject(libraryContext) |
| if (isCanceled) { |
| return |
| } |
| } |
| } |
| } |
| |
| currentProject = project |
| |
| for (check in applicableDetectors) { |
| client.runReadAction(Runnable { |
| check.afterCheckEachProject(projectContext) |
| check.afterCheckRootProject(projectContext) |
| }) |
| if (isCanceled) { |
| return |
| } |
| } |
| |
| if (isCanceled) { |
| client.report( |
| projectContext, |
| // Must provide an issue since API guarantees that the issue parameter |
| IssueRegistry.CANCELLED, |
| Severity.INFORMATIONAL, |
| Location.create(project.dir), |
| "Lint canceled by user", TextFormat.RAW, null |
| ) |
| } |
| |
| currentProjects = null |
| } |
| |
| private fun runFileDetectors(project: Project, main: Project?) { |
| if (phase == 1) { |
| moduleCount++ |
| } |
| |
| // Look up manifest information (but not for library projects) |
| if (project.isAndroidProject) { |
| for (manifestFile in project.manifestFiles) { |
| val parser = client.xmlParser |
| val context = createXmlContext(project, main, manifestFile, null, parser) |
| if (context != null) { |
| try { |
| project.readManifest(context.document) |
| if ((!project.isLibrary || main != null && |
| main.isMergingManifests) && scope.contains(Scope.MANIFEST) |
| ) { |
| val detectors = scopeDetectors[Scope.MANIFEST] |
| if (detectors != null) { |
| val xmlDetectors = ArrayList<XmlScanner>(detectors.size) |
| for (detector in detectors) { |
| if (detector is XmlScanner) { |
| xmlDetectors.add(detector) |
| } |
| } |
| |
| val v = ResourceVisitor(parser, xmlDetectors, null) |
| fireEvent(EventType.SCANNING_FILE, context) |
| v.visitFile(context) |
| fileCount++ |
| resourceFileCount++ |
| } |
| } |
| } finally { |
| disposeXmlContext(context) |
| } |
| } |
| } |
| |
| // Process both Scope.RESOURCE_FILE and Scope.ALL_RESOURCE_FILES detectors together |
| // in a single pass through the resource directories. |
| if (scope.contains(Scope.ALL_RESOURCE_FILES) || |
| scope.contains(Scope.RESOURCE_FILE) || |
| scope.contains(Scope.RESOURCE_FOLDER) || |
| scope.contains(Scope.BINARY_RESOURCE_FILE) |
| ) { |
| val dirChecks = scopeDetectors[Scope.RESOURCE_FOLDER] |
| val binaryChecks = scopeDetectors[Scope.BINARY_RESOURCE_FILE] |
| val checks = union( |
| scopeDetectors[Scope.RESOURCE_FILE], |
| scopeDetectors[Scope.ALL_RESOURCE_FILES] |
| ) ?: emptyList() |
| var haveXmlChecks = !checks.isEmpty() |
| val xmlDetectors: MutableList<XmlScanner> |
| if (haveXmlChecks) { |
| xmlDetectors = ArrayList(checks.size) |
| for (detector in checks) { |
| if (detector is XmlScanner) { |
| xmlDetectors.add(detector) |
| } |
| } |
| haveXmlChecks = !xmlDetectors.isEmpty() |
| } else { |
| xmlDetectors = mutableListOf() |
| } |
| if (haveXmlChecks || |
| dirChecks != null && !dirChecks.isEmpty() || |
| binaryChecks != null && !binaryChecks.isEmpty() |
| ) { |
| val files = project.subset |
| if (files != null) { |
| checkIndividualResources( |
| project, main, xmlDetectors, dirChecks, |
| binaryChecks, files |
| ) |
| } else { |
| val resourceFolders = project.resourceFolders |
| if (!resourceFolders.isEmpty()) { |
| for (res in resourceFolders) { |
| checkResFolder( |
| project, main, res, xmlDetectors, dirChecks, |
| binaryChecks |
| ) |
| } |
| } |
| if (checkGeneratedSources) { |
| val generatedResourceFolders = project.generatedResourceFolders |
| if (!generatedResourceFolders.isEmpty()) { |
| for (res in generatedResourceFolders) { |
| checkResFolder( |
| project, main, res, xmlDetectors, dirChecks, |
| binaryChecks |
| ) |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| if (isCanceled) { |
| return |
| } |
| } |
| |
| if (scope.contains(Scope.JAVA_FILE) || scope.contains(Scope.ALL_JAVA_FILES)) { |
| val checks = union( |
| scopeDetectors[Scope.JAVA_FILE], |
| scopeDetectors[Scope.ALL_JAVA_FILES] |
| ) |
| if (checks != null && !checks.isEmpty()) { |
| val files = project.subset |
| if (files != null) { |
| checkIndividualJavaFiles(project, main, checks, files) |
| } else { |
| val sourceFolders = project.javaSourceFolders |
| val testFolders = if (!ignoreTestSources) |
| project.testSourceFolders |
| else |
| emptyList<File>() |
| |
| val generatedFolders = if (checkGeneratedSources) |
| project.generatedSourceFolders |
| else emptyList<File>() |
| checkJava(project, main, sourceFolders, testFolders, generatedFolders, checks) |
| } |
| } |
| } |
| |
| if (isCanceled) { |
| return |
| } |
| |
| if (scope.contains(Scope.CLASS_FILE) || |
| scope.contains(Scope.ALL_CLASS_FILES) || |
| scope.contains(Scope.JAVA_LIBRARIES) |
| ) { |
| checkClasses(project, main) |
| } |
| |
| if (isCanceled) { |
| return |
| } |
| |
| if (scope.contains(Scope.GRADLE_FILE)) { |
| checkBuildScripts(project, main) |
| } |
| |
| if (isCanceled) { |
| return |
| } |
| |
| if (scope.contains(Scope.OTHER)) { |
| val checks = scopeDetectors[Scope.OTHER] |
| if (checks != null) { |
| val visitor = OtherFileVisitor(checks) |
| visitor.scan(this, project, main) |
| } |
| } |
| |
| if (isCanceled) { |
| return |
| } |
| |
| if (project === main && scope.contains(Scope.PROGUARD_FILE) && |
| project.isAndroidProject |
| ) { |
| checkProGuard(project, main) |
| } |
| |
| if (project === main && scope.contains(Scope.PROPERTY_FILE)) { |
| checkProperties(project, main) |
| } |
| } |
| |
| private fun checkBuildScripts(project: Project, main: Project?) { |
| val detectors = scopeDetectors[Scope.GRADLE_FILE] |
| if (detectors != null) { |
| val files = project.subset ?: project.gradleBuildScripts |
| if (files.isEmpty()) { |
| return |
| } |
| val gradleScanners = ArrayList<GradleScanner>(detectors.size) |
| val customVisitedGradleScanners = ArrayList<GradleScanner>(detectors.size) |
| for (detector in detectors) { |
| if (detector is GradleScanner) { |
| if (detector.customVisitor) { |
| customVisitedGradleScanners.add(detector) |
| } else { |
| gradleScanners.add(detector) |
| } |
| } |
| } |
| if (gradleScanners.isEmpty() && customVisitedGradleScanners.isEmpty()) { |
| return |
| } |
| for (file in files) { |
| // Gradle Kotlin Script? Use Java parsing mechanism instead |
| if (file.path.endsWith(DOT_KTS)) { |
| val context = JavaContext(this, project, main, file) |
| val uastParser = client.getUastParser(currentProject) |
| context.uastParser = uastParser |
| |
| uastParser.prepare(listOf(context), emptyList()) |
| client.runReadAction(Runnable { |
| val uFile = uastParser.parse(context) |
| if (uFile != null) { |
| context.setJavaFile(uFile.psi) // needed for getLocation |
| context.uastFile = uFile |
| fireEvent(EventType.SCANNING_FILE, context) |
| |
| val uastVisitor = |
| UastGradleVisitor(context) |
| val gradleContext = |
| GradleContext(uastVisitor, this, project, main, file) |
| fireEvent(EventType.SCANNING_FILE, context) |
| for (detector in detectors) { |
| detector.beforeCheckFile(context) |
| } |
| |
| uastVisitor.visitBuildScript(gradleContext, gradleScanners) |
| for (scanner in customVisitedGradleScanners) { |
| scanner.visitBuildScript(context) |
| } |
| for (detector in detectors) { |
| detector.afterCheckFile(context) |
| } |
| |
| context.setJavaFile(null) |
| context.uastFile = null |
| } |
| }) |
| uastParser.dispose() |
| fileCount++ |
| } else { |
| val gradleVisitor = project.client.getGradleVisitor() |
| val context = GradleContext(gradleVisitor, this, project, main, file) |
| fireEvent(EventType.SCANNING_FILE, context) |
| for (detector in detectors) { |
| detector.beforeCheckFile(context) |
| } |
| gradleVisitor.visitBuildScript(context, gradleScanners) |
| for (scanner in customVisitedGradleScanners) { |
| scanner.visitBuildScript(context) |
| } |
| for (detector in detectors) { |
| detector.afterCheckFile(context) |
| } |
| fileCount++ |
| } |
| } |
| } |
| } |
| |
| private fun checkProGuard(project: Project, main: Project) { |
| val detectors = scopeDetectors[Scope.PROGUARD_FILE] |
| if (detectors != null) { |
| val files = project.proguardFiles |
| for (file in files) { |
| val context = Context(this, project, main, file) |
| fireEvent(EventType.SCANNING_FILE, context) |
| for (detector in detectors) { |
| detector.beforeCheckFile(context) |
| detector.run(context) |
| detector.afterCheckFile(context) |
| fileCount++ |
| } |
| } |
| } |
| } |
| |
| private fun checkProperties(project: Project, main: Project) { |
| val detectors = scopeDetectors[Scope.PROPERTY_FILE] |
| if (detectors != null) { |
| for (file in project.propertyFiles) { |
| val context = Context(this, project, main, file) |
| fireEvent(EventType.SCANNING_FILE, context) |
| for (detector in detectors) { |
| detector.beforeCheckFile(context) |
| detector.run(context) |
| detector.afterCheckFile(context) |
| fileCount++ |
| } |
| } |
| } |
| } |
| |
| /** |
| * Returns the super class for the given class name, |
| * which should be in VM format (e.g. java/lang/Integer, not java.lang.Integer). |
| * If the super class is not known, returns null. This can happen if |
| * the given class is not a known class according to the project or its |
| * libraries, for example because it refers to one of the core libraries which |
| * are not analyzed by lint. |
| * |
| * @param name the fully qualified class name |
| * |
| * @return the corresponding super class name (in VM format), or null if not known |
| */ |
| fun getSuperClass(name: String): String? = client.getSuperClass(currentProject!!, name) |
| |
| /** |
| * Returns true if the given class is a subclass of the given super class. |
| * |
| * @param classNode the class to check whether it is a subclass of the given |
| * super class name |
| * |
| * @param superClassName the fully qualified super class name (in VM format, |
| * e.g. java/lang/Integer, not java.lang.Integer. |
| * |
| * @return true if the given class is a subclass of the given super class |
| */ |
| fun isSubclassOf(classNode: ClassNode, superClassName: String): Boolean { |
| if (superClassName == classNode.superName) { |
| return true |
| } |
| |
| if (currentProject != null) { |
| val isSub = client.isSubclassOf(currentProject!!, classNode.name, superClassName) |
| if (isSub != null) { |
| return isSub |
| } |
| } |
| |
| var className: String? = classNode.name |
| while (className != null) { |
| if (className == superClassName) { |
| return true |
| } |
| className = getSuperClass(className) |
| } |
| |
| return false |
| } |
| |
| /** Check the classes in this project (and if applicable, in any library projects */ |
| private fun checkClasses(project: Project, main: Project?) { |
| val files = project.subset |
| if (files != null) { |
| checkIndividualClassFiles(project, main, files) |
| return |
| } |
| |
| // We need to read in all the classes up front such that we can initialize |
| // the parent chains (such that for example for a virtual dispatch, we can |
| // also check the super classes). |
| |
| val libraries = project.getJavaLibraries(false) |
| val libraryEntries = ClassEntry.fromClassPath(client, libraries, true) |
| |
| val classFolders = project.javaClassFolders |
| val classEntries: List<ClassEntry> |
| classEntries = if (classFolders.isEmpty()) { |
| val message = String.format( |
| "No `.class` files were found in project \"%1\$s\", " + |
| "so none of the classfile based checks could be run. " + |
| "Does the project need to be built first?", project.name |
| ) |
| LintClient.report( |
| client = client, issue = IssueRegistry.LINT_ERROR, message = message, |
| project = project, mainProject = main, driver = this |
| ) |
| emptyList() |
| } else { |
| ClassEntry.fromClassPath(client, classFolders, true) |
| } |
| |
| // Actually run the detectors. Libraries should be called before the |
| // main classes. |
| runClassDetectors(Scope.JAVA_LIBRARIES, libraryEntries, project, main) |
| |
| if (isCanceled) { |
| return |
| } |
| |
| runClassDetectors(Scope.CLASS_FILE, classEntries, project, main) |
| runClassDetectors(Scope.ALL_CLASS_FILES, classEntries, project, main) |
| } |
| |
| private fun checkIndividualClassFiles( |
| project: Project, |
| main: Project?, |
| files: List<File> |
| ) { |
| val classFiles = ArrayList<File>(files.size) |
| val classFolders = project.javaClassFolders |
| if (!classFolders.isEmpty()) { |
| for (file in files) { |
| val path = file.path |
| if (file.isFile && path.endsWith(DOT_CLASS)) { |
| classFiles.add(file) |
| } |
| } |
| } |
| |
| val entries = ClassEntry.fromClassFiles( |
| client, classFiles, classFolders, |
| true |
| ) |
| if (!entries.isEmpty()) { |
| entries.sort() |
| runClassDetectors(Scope.CLASS_FILE, entries, project, main) |
| } |
| } |
| |
| /** |
| * Stack of [ClassNode] nodes for outer classes of the currently |
| * processed class, including that class itself. Populated by |
| * [.runClassDetectors] and used by |
| * [.getOuterClassNode] |
| */ |
| private var outerClasses: Deque<ClassNode>? = null |
| |
| private fun runClassDetectors( |
| scope: Scope, |
| entries: List<ClassEntry>, |
| project: Project, |
| main: Project? |
| ) { |
| if (this.scope.contains(scope)) { |
| val classDetectors = scopeDetectors[scope] |
| if (classDetectors != null && !classDetectors.isEmpty() && !entries.isEmpty()) { |
| val visitor = AsmVisitor(client, classDetectors) |
| |
| var sourceContents: CharSequence? = null |
| var sourceName = "" |
| outerClasses = ArrayDeque<ClassNode>() |
| var prev: ClassEntry? = null |
| for (entry in entries) { |
| if (prev != null && prev.compareTo(entry) == 0) { |
| // Duplicate entries for some reason: ignore |
| continue |
| } |
| prev = entry |
| |
| val reader: ClassReader |
| val classNode: ClassNode |
| try { |
| reader = ClassReader(entry.bytes) |
| classNode = ClassNode() |
| reader.accept(classNode, 0 /* flags */) |
| } catch (t: Throwable) { |
| client.log( |
| null, |
| "Error processing ${entry.path()}: broken class file? (${t.message})" |
| ) |
| continue |
| } |
| |
| var peek: ClassNode? |
| while (true) { |
| peek = outerClasses?.peek() |
| if (peek == null) { |
| break |
| } |
| if (classNode.name.startsWith(peek.name)) { |
| break |
| } else { |
| outerClasses?.pop() |
| } |
| } |
| outerClasses?.push(classNode) |
| |
| if (isSuppressed(null, classNode)) { |
| // Class was annotated with suppress all -- no need to look any further |
| continue |
| } |
| |
| if (sourceContents != null) { |
| // Attempt to reuse the source buffer if initialized |
| // This means making sure that the source files |
| // foo/bar/MyClass and foo/bar/MyClass$Bar |
| // and foo/bar/MyClass$3 and foo/bar/MyClass$3$1 have the same prefix. |
| val newName = classNode.name |
| var newRootLength = newName.indexOf('$') |
| if (newRootLength == -1) { |
| newRootLength = newName.length |
| } |
| var oldRootLength = sourceName.indexOf('$') |
| if (oldRootLength == -1) { |
| oldRootLength = sourceName.length |
| } |
| if (newRootLength != oldRootLength || !sourceName.regionMatches( |
| 0, |
| newName, |
| 0, |
| newRootLength |
| ) |
| ) { |
| sourceContents = null |
| } |
| } |
| |
| val context = ClassContext( |
| this, project, main, |
| entry.file, entry.jarFile, entry.binDir, entry.bytes, |
| classNode, scope == Scope.JAVA_LIBRARIES /*fromLibrary*/, |
| sourceContents |
| ) |
| |
| try { |
| visitor.runClassDetectors(context) |
| } catch (throwable: Throwable) { |
| // Process canceled etc |
| if (!handleDetectorError(context, this, throwable)) { |
| cancel() |
| } |
| } |
| |
| // We're not counting class files even though technically lint has |
| // to process them separately; this will essentially double the |
| // observed file count (which is usually taken to mean source files) |
| // and with lots of inner classes, more than double. |
| // fileCount++ |
| |
| if (isCanceled) { |
| return |
| } |
| |
| sourceContents = context.getSourceContents(false/*read*/) |
| sourceName = classNode.name |
| } |
| |
| outerClasses = null |
| } |
| } |
| } |
| |
| /** Returns the outer class node of the given class node |
| * @param classNode the inner class node |
| * |
| * @return the outer class node |
| */ |
| fun getOuterClassNode(classNode: ClassNode): ClassNode? { |
| val outerName = classNode.outerClass |
| |
| val iterator = outerClasses?.iterator() ?: return null |
| while (iterator.hasNext()) { |
| val node = iterator.next() |
| if (outerName != null) { |
| if (node.name == outerName) { |
| return node |
| } |
| } else if (node === classNode) { |
| return if (iterator.hasNext()) iterator.next() else null |
| } |
| } |
| |
| return null |
| } |
| |
| /** |
| * Returns the [ClassNode] corresponding to the given type, if possible, or null |
| * |
| * @param type the fully qualified type, using JVM signatures (/ and $, not . as path |
| * separators) |
| * |
| * @param flags the ASM flags to pass to the [ClassReader], normally 0 but can |
| * for example be [ClassReader.SKIP_CODE] and/oor |
| * [ClassReader.SKIP_DEBUG] |
| * |
| * @return the class node for the type, or null |
| */ |
| fun findClass(context: ClassContext, type: String, flags: Int): ClassNode? { |
| val relative = type.replace('/', File.separatorChar) + DOT_CLASS |
| val classFile = findClassFile(context.project, relative) |
| if (classFile != null) { |
| if (classFile.path.endsWith(DOT_JAR)) { |
| // TODO: Handle .jar files |
| return null |
| } |
| |
| try { |
| val bytes = client.readBytes(classFile) |
| val reader = ClassReader(bytes) |
| val classNode = ClassNode() |
| reader.accept(classNode, flags) |
| |
| return classNode |
| } catch (t: Throwable) { |
| client.log( |
| null, |
| "Error processing ${classFile.path}: broken class file? (${t.message})" |
| ) |
| } |
| } |
| |
| return null |
| } |
| |
| private fun findClassFile(project: Project, relativePath: String): File? { |
| for (root in client.getJavaClassFolders(project)) { |
| val path = File(root, relativePath) |
| if (path.exists()) { |
| return path |
| } |
| } |
| // Search in the libraries |
| for (root in client.getJavaLibraries(project, true)) { |
| // TODO: Handle .jar files! |
| val path = File(root, relativePath) |
| if (path.exists()) { |
| return path |
| } |
| } |
| |
| // Search dependent projects |
| for (library in project.directLibraries) { |
| val path = findClassFile(library, relativePath) |
| if (path != null) { |
| return path |
| } |
| } |
| |
| return null |
| } |
| |
| private fun checkJava( |
| project: Project, |
| main: Project?, |
| sourceFolders: List<File>, |
| testSourceFolders: List<File>, |
| generatedSources: List<File>, |
| checks: List<Detector> |
| ) { |
| assert(!checks.isEmpty()) |
| |
| // Gather all Java source files in a single pass; more efficient. |
| val sources = ArrayList<File>(100) |
| for (folder in sourceFolders) { |
| gatherJavaFiles(folder, sources) |
| } |
| for (folder in generatedSources) { |
| gatherJavaFiles(folder, sources) |
| } |
| |
| val contexts = ArrayList<JavaContext>(2 * sources.size) |
| for (file in sources) { |
| val context = JavaContext(this, project, main, file) |
| contexts.add(context) |
| } |
| |
| // Test sources |
| val testContexts: List<JavaContext> |
| if (ignoreTestSources) { |
| testContexts = emptyList() |
| } else { |
| sources.clear() |
| for (folder in testSourceFolders) { |
| gatherJavaFiles(folder, sources) |
| } |
| testContexts = ArrayList(sources.size) |
| if (!ignoreTestSources) { |
| for (file in sources) { |
| val context = JavaContext(this, project, main, file) |
| context.isTestSource = true |
| testContexts.add(context) |
| } |
| } |
| } |
| |
| // Visit all contexts |
| if (!contexts.isEmpty() || !testContexts.isEmpty()) { |
| visitJavaFiles(checks, project, main, contexts, testContexts) |
| } |
| } |
| |
| private fun visitJavaFiles( |
| checks: List<Detector>, |
| project: Project, |
| main: Project?, |
| contexts: List<JavaContext>, |
| testContexts: List<JavaContext> |
| ) { |
| val allContexts: List<JavaContext> |
| if (testContexts.isEmpty()) { |
| allContexts = contexts |
| } else { |
| allContexts = ArrayList(contexts.size + testContexts.size) |
| allContexts.addAll(contexts) |
| allContexts.addAll(testContexts) |
| } |
| |
| // Force all test sources into the normal source check (where all checks apply) ? |
| if (checkTestSources) { |
| visitJavaFiles(checks, project, main, allContexts, allContexts, emptyList()) |
| } else { |
| visitJavaFiles(checks, project, main, allContexts, contexts, testContexts) |
| } |
| } |
| |
| private fun visitJavaFiles( |
| checks: List<Detector>, |
| project: Project, |
| main: Project?, |
| allContexts: List<JavaContext>, |
| srcContexts: List<JavaContext>, |
| testContexts: List<JavaContext> |
| ) { |
| // Temporary: we still have some builtin checks that aren't migrated to |
| // PSI. Until that's complete, remove them from the list here |
| val uastScanners = ArrayList<Detector>(checks.size) |
| for (detector in checks) { |
| if (detector is SourceCodeScanner) { |
| uastScanners.add(detector) |
| } |
| } |
| |
| if (!uastScanners.isEmpty()) { |
| val parser = client.getUastParser(currentProject) |
| for (context in allContexts) { |
| context.uastParser = parser |
| } |
| val uElementVisitor = UElementVisitor(parser, uastScanners) |
| |
| parserErrors = !uElementVisitor.prepare(srcContexts, testContexts) |
| |
| for (context in srcContexts) { |
| fireEvent(EventType.SCANNING_FILE, context) |
| // TODO: Don't hold read lock around the entire process? |
| client.runReadAction(Runnable { uElementVisitor.visitFile(context) }) |
| fileCount++ |
| if (context.file.name.endsWith(DOT_JAVA)) { |
| javaFileCount++ |
| } else { |
| kotlinFileCount++ |
| } |
| if (isCanceled) { |
| return |
| } |
| } |
| |
| val projectContext = Context(this, project, main, project.dir) |
| uElementVisitor.visitGroups(projectContext, allContexts) |
| uElementVisitor.dispose() |
| |
| if (!testContexts.isEmpty()) { |
| val testScanners = filterTestScanners(uastScanners) |
| if (!testScanners.isEmpty()) { |
| val uTestVisitor = UElementVisitor(parser, testScanners) |
| |
| for (context in testContexts) { |
| fireEvent(EventType.SCANNING_FILE, context) |
| // TODO: Don't hold read lock around the entire process? |
| client.runReadAction(Runnable { uTestVisitor.visitFile(context) }) |
| fileCount++ |
| testSourceCount++ |
| if (context.file.name.endsWith(DOT_JAVA)) { |
| javaFileCount++ |
| } else { |
| kotlinFileCount++ |
| } |
| if (isCanceled) { |
| return |
| } |
| } |
| |
| uTestVisitor.dispose() |
| } |
| } |
| } |
| } |
| |
| private fun filterTestScanners(scanners: List<Detector>): List<Detector> { |
| val testScanners = ArrayList<Detector>(scanners.size) |
| // Compute intersection of Java and test scanners |
| var sourceScanners: Collection<Detector> = |
| scopeDetectors[Scope.TEST_SOURCES] ?: return emptyList() |
| if (sourceScanners.size > 15 && scanners.size > 15) { |
| sourceScanners = Sets.newHashSet(sourceScanners) // switch from list to set |
| } |
| for (check in scanners) { |
| if (sourceScanners.contains(check)) { |
| testScanners.add(check) |
| } |
| } |
| return testScanners |
| } |
| |
| private fun checkIndividualJavaFiles( |
| project: Project, |
| main: Project?, |
| checks: List<Detector>, |
| files: List<File> |
| ) { |
| |
| val contexts = ArrayList<JavaContext>(files.size) |
| val testContexts = ArrayList<JavaContext>(files.size) |
| val testFolders = project.testSourceFolders |
| val generatedFolders = project.generatedSourceFolders |
| for (file in files) { |
| val path = file.path |
| if (path.endsWith(DOT_JAVA) || path.endsWith(DOT_KT)) { |
| // Figure out if this is a generated test context |
| if (!checkGeneratedSources && |
| generatedFolders.asSequence().any { FileUtil.isAncestor(it, file, false) } |
| ) { |
| continue |
| } |
| |
| val context = JavaContext(this, project, main, file) |
| |
| // Figure out if this file is a test context |
| if (testFolders.asSequence().any { FileUtil.isAncestor(it, file, false) }) { |
| context.isTestSource = true |
| testContexts.add(context) |
| } else { |
| contexts.add(context) |
| } |
| } |
| } |
| |
| if (contexts.isEmpty() && testContexts.isEmpty()) { |
| return |
| } |
| |
| // We're not sure if these individual files are tests or non-tests; treat them |
| // as non-tests now. This gives you warnings if you're editing an individual |
| // test file for example. |
| |
| visitJavaFiles(checks, project, main, contexts, testContexts) |
| } |
| |
| private var currentFolderType: ResourceFolderType? = null |
| private var currentXmlDetectors: List<XmlScanner>? = null |
| private var currentBinaryDetectors: List<Detector>? = null |
| private var currentVisitor: ResourceVisitor? = null |
| |
| private fun getVisitor( |
| type: ResourceFolderType, |
| checks: List<XmlScanner>, |
| binaryChecks: List<Detector>? |
| ): ResourceVisitor? { |
| if (type != currentFolderType) { |
| currentFolderType = type |
| |
| // Determine which XML resource detectors apply to the given folder type |
| val applicableXmlChecks = ArrayList<XmlScanner>(checks.size) |
| for (check in checks) { |
| if (check.appliesTo(type)) { |
| applicableXmlChecks.add(check) |
| } |
| } |
| var applicableBinaryChecks: MutableList<Detector>? = null |
| if (binaryChecks != null) { |
| applicableBinaryChecks = ArrayList(binaryChecks.size) |
| for (check in binaryChecks) { |
| if (check.appliesTo(type)) { |
| applicableBinaryChecks.add(check) |
| } |
| } |
| } |
| |
| // If the list of detectors hasn't changed, then just use the current visitor! |
| if (currentXmlDetectors != null && currentXmlDetectors == applicableXmlChecks && |
| Objects.equal(currentBinaryDetectors, applicableBinaryChecks) |
| ) { |
| return currentVisitor |
| } |
| |
| currentXmlDetectors = applicableXmlChecks |
| currentBinaryDetectors = applicableBinaryChecks |
| |
| if (applicableXmlChecks.isEmpty() && |
| (applicableBinaryChecks == null || applicableBinaryChecks.isEmpty()) |
| ) { |
| currentVisitor = null |
| return null |
| } |
| |
| val parser = client.xmlParser |
| currentVisitor = ResourceVisitor( |
| parser, applicableXmlChecks, |
| applicableBinaryChecks |
| ) |
| } |
| |
| return currentVisitor |
| } |
| |
| private fun checkResFolder( |
| project: Project, |
| main: Project?, |
| res: File, |
| xmlChecks: List<XmlScanner>, |
| dirChecks: List<Detector>?, |
| binaryChecks: List<Detector>? |
| ) { |
| val resourceDirs = res.listFiles() ?: return |
| |
| // Sort alphabetically such that we can process related folder types at the |
| // same time, and to have a defined behavior such that detectors can rely on |
| // predictable ordering, e.g. layouts are seen before menus are seen before |
| // values, etc (l < m < v). |
| |
| Arrays.sort(resourceDirs) |
| for (dir in resourceDirs) { |
| val type = ResourceFolderType.getFolderType(dir.name) |
| if (type != null) { |
| checkResourceFolder(project, main, dir, type, xmlChecks, dirChecks, binaryChecks) |
| } |
| |
| if (isCanceled) { |
| return |
| } |
| } |
| } |
| |
| private fun checkResourceFolder( |
| project: Project, |
| main: Project?, |
| dir: File, |
| type: ResourceFolderType, |
| xmlChecks: List<XmlScanner>, |
| dirChecks: List<Detector>?, |
| binaryChecks: List<Detector>? |
| ) { |
| |
| // Process the resource folder |
| |
| if (dirChecks != null && !dirChecks.isEmpty()) { |
| val context = ResourceContext(this, project, main, dir, type, "") |
| val folderName = dir.name |
| fireEvent(EventType.SCANNING_FILE, context) |
| for (check in dirChecks) { |
| if (check.appliesTo(type)) { |
| check.beforeCheckFile(context) |
| check.checkFolder(context, folderName) |
| check.afterCheckFile(context) |
| fileCount++ |
| resourceFileCount++ |
| } |
| } |
| if (binaryChecks == null && xmlChecks.isEmpty()) { |
| return |
| } |
| } |
| |
| val files = dir.listFiles() |
| if (files == null || files.isEmpty()) { |
| return |
| } |
| |
| val visitor = getVisitor(type, xmlChecks, binaryChecks) |
| if (visitor != null) { // if not, there are no applicable rules in this folder |
| val parser = visitor.parser |
| |
| // Process files in alphabetical order, to ensure stable output |
| // (for example for the duplicate resource detector) |
| Arrays.sort(files) |
| for (file in files) { |
| if (isXmlFile(file)) { |
| val context = createXmlContext(project, main, file, type, parser) ?: continue |
| try { |
| fireEvent(EventType.SCANNING_FILE, context) |
| visitor.visitFile(context) |
| } finally { |
| disposeXmlContext(context) |
| } |
| fileCount++ |
| resourceFileCount++ |
| } else if (binaryChecks != null && |
| (isBitmapFile(file) || type == ResourceFolderType.RAW) |
| ) { |
| val context = object : ResourceContext(this, project, main, file, type, "") { |
| override val resourceFolder: File? |
| // Like super, but for the parent folder instead of the context file |
| get() = if (resourceFolderType != null) file.parentFile else null |
| } |
| fireEvent(EventType.SCANNING_FILE, context) |
| visitor.visitBinaryResource(context) |
| fileCount++ |
| resourceFileCount++ |
| } |
| if (isCanceled) { |
| return |
| } |
| } |
| } |
| } |
| |
| private fun disposeXmlContext(context: XmlContext) = |
| context.parser.dispose(context, context.document) |
| |
| private fun createXmlContext( |
| project: Project, |
| main: Project?, |
| file: File, |
| type: ResourceFolderType?, |
| parser: XmlParser |
| ): XmlContext? { |
| assert(isXmlFile(file)) |
| val contents = client.readFile(file) |
| if (contents.isEmpty()) { |
| return null |
| } |
| val xml = contents.toString() |
| val document = parser.parseXml(xml, file) ?: return null |
| |
| // Ignore empty documents |
| document.documentElement ?: return null |
| |
| return XmlContext(this, project, main, file, type, parser, xml, document) |
| } |
| |
| /** Checks individual resources */ |
| private fun checkIndividualResources( |
| project: Project, |
| main: Project?, |
| xmlDetectors: List<XmlScanner>, |
| dirChecks: List<Detector>?, |
| binaryChecks: List<Detector>?, |
| files: List<File> |
| ) { |
| for (file in files) { |
| if (file.isDirectory) { |
| // Is it a resource folder? |
| val type = ResourceFolderType.getFolderType(file.name) |
| if (type != null && File(file.parentFile, RES_FOLDER).exists()) { |
| // Yes. |
| checkResourceFolder( |
| project, main, file, type, xmlDetectors, dirChecks, |
| binaryChecks |
| ) |
| } else if (file.name == RES_FOLDER) { // Is it the res folder? |
| // Yes |
| checkResFolder(project, main, file, xmlDetectors, dirChecks, binaryChecks) |
| } |
| } else if (file.isFile && isXmlFile(file)) { |
| // Yes, find out its resource type |
| val folderName = file.parentFile.name |
| val type = ResourceFolderType.getFolderType(folderName) |
| if (type != null) { |
| val visitor = getVisitor(type, xmlDetectors, binaryChecks) |
| if (visitor != null) { |
| val parser = visitor.parser |
| val context = createXmlContext(project, main, file, type, parser) |
| if (context != null) { |
| try { |
| fireEvent(EventType.SCANNING_FILE, context) |
| visitor.visitFile(context) |
| } finally { |
| disposeXmlContext(context) |
| } |
| } |
| fileCount++ |
| resourceFileCount++ |
| } |
| } |
| } else if (binaryChecks != null && file.isFile && isBitmapFile(file)) { |
| // Yes, find out its resource type |
| val folderName = file.parentFile.name |
| val type = ResourceFolderType.getFolderType(folderName) |
| if (type != null) { |
| val visitor = getVisitor(type, xmlDetectors, binaryChecks) |
| if (visitor != null) { |
| val context = ResourceContext(this, project, main, file, type, "") |
| fireEvent(EventType.SCANNING_FILE, context) |
| visitor.visitBinaryResource(context) |
| fileCount++ |
| resourceFileCount++ |
| if (isCanceled) { |
| return |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * Adds a listener to be notified of lint progress |
| * |
| * @param listener the listener to be added |
| */ |
| fun addLintListener(listener: LintListener) { |
| if (listeners == null) { |
| listeners = ArrayList(1) |
| } |
| listeners!!.add(listener) |
| } |
| |
| /** |
| * Removes a listener such that it is no longer notified of progress |
| * |
| * @param listener the listener to be removed |
| */ |
| fun removeLintListener(listener: LintListener) { |
| listeners!!.remove(listener) |
| if (listeners!!.isEmpty()) { |
| listeners = null |
| } |
| } |
| |
| /** Notifies listeners, if any, that the given event has occurred */ |
| private fun fireEvent( |
| type: LintListener.EventType, |
| context: Context? = null, |
| project: Project? = context?.project |
| ) { |
| if (listeners != null) { |
| for (listener in listeners!!) { |
| listener.update(this, type, project, context) |
| } |
| } |
| } |
| |
| /** |
| * Wrapper around the lint client. This sits in the middle between a |
| * detector calling for example [LintClient.report] and |
| * the actual embedding tool, and performs filtering etc such that detectors |
| * and lint clients don't have to make sure they check for ignored issues or |
| * filtered out warnings. |
| */ |
| private inner class LintClientWrapper(private val delegate: LintClient) : |
| LintClient(clientName) { |
| override fun getMergedManifest(project: Project): Document? = |
| delegate.getMergedManifest(project) |
| |
| override fun resolveMergeManifestSources( |
| mergedManifest: Document, |
| reportFile: Any |
| ) = |
| delegate.resolveMergeManifestSources(mergedManifest, reportFile) |
| |
| override fun findManifestSourceNode( |
| mergedNode: org.w3c.dom.Node |
| ): Pair<File, out org.w3c.dom.Node>? = |
| delegate.findManifestSourceNode(mergedNode) |
| |
| override fun findManifestSourceLocation(mergedNode: org.w3c.dom.Node): Location? = |
| delegate.findManifestSourceLocation(mergedNode) |
| |
| override fun report( |
| context: Context, |
| issue: Issue, |
| severity: Severity, |
| location: Location, |
| message: String, |
| format: TextFormat, |
| fix: LintFix? |
| ) { |
| |
| if (currentProject != null && currentProject?.reportIssues == false) { |
| return |
| } |
| |
| val configuration = context.configuration |
| if (!configuration.isEnabled(issue)) { |
| if (issue.category !== Category.LINT) { |
| delegate.log( |
| null, "Incorrect detector reported disabled issue %1\$s", |
| issue.toString() |
| ) |
| } |
| return |
| } |
| |
| if (configuration.isIgnored(context, issue, location, message)) { |
| return |
| } |
| |
| if (severity == Severity.IGNORE) { |
| return |
| } |
| |
| val baseline = baseline |
| if (baseline != null) { |
| val filtered = baseline.findAndMark( |
| issue, location, message, severity, |
| context.project |
| ) |
| if (filtered) { |
| if (!allowSuppress && issue.suppressNames != null) { |
| flagInvalidSuppress( |
| context, issue, Location.create(baseline.file), |
| issue.suppressNames |
| ) |
| } else { |
| return |
| } |
| } |
| } |
| |
| reportGenerationTimeMs += measureTimeMillis { |
| delegate.report(context, issue, severity, location, message, format, fix) |
| } |
| } |
| |
| private fun unsupported(): Nothing = |
| throw UnsupportedOperationException( |
| "This method should not be called by lint " + |
| "detectors; it is intended only for usage by the lint infrastructure" |
| ) |
| |
| // Everything else just delegates to the embedding lint client |
| |
| override fun getConfiguration( |
| project: Project, |
| driver: LintDriver? |
| ): Configuration = |
| delegate.getConfiguration(project, driver) |
| |
| override fun getDisplayPath(file: File): String = delegate.getDisplayPath(file) |
| |
| override fun log( |
| severity: Severity, |
| exception: Throwable?, |
| format: String?, |
| vararg args: Any |
| ) = delegate.log(exception, format, *args) |
| |
| override fun getTestLibraries(project: Project): List<File> = |
| delegate.getTestLibraries(project) |
| |
| override fun getClientRevision(): String? = delegate.getClientRevision() |
| |
| override fun runReadAction(runnable: Runnable) = delegate.runReadAction(runnable) |
| |
| override fun <T> runReadAction(computable: Computable<T>): T = |
| delegate.runReadAction(computable) |
| |
| override fun readFile(file: File): CharSequence = delegate.readFile(file) |
| |
| @Throws(IOException::class) |
| override fun readBytes(file: File): ByteArray = delegate.readBytes(file) |
| |
| override fun getJavaSourceFolders(project: Project): List<File> = |
| delegate.getJavaSourceFolders(project) |
| |
| override fun getGeneratedSourceFolders(project: Project): List<File> = |
| delegate.getGeneratedSourceFolders(project) |
| |
| override fun getJavaClassFolders(project: Project): List<File> = |
| delegate.getJavaClassFolders(project) |
| |
| override fun getJavaLibraries(project: Project, includeProvided: Boolean): List<File> = |
| delegate.getJavaLibraries(project, includeProvided) |
| |
| override fun getTestSourceFolders(project: Project): List<File> = |
| delegate.getTestSourceFolders(project) |
| |
| override fun getBuildTools(project: Project): BuildToolInfo? = |
| delegate.getBuildTools(project) |
| |
| override fun createSuperClassMap(project: Project): Map<String, String> = |
| delegate.createSuperClassMap(project) |
| |
| override fun getResourceFolders(project: Project): List<File> = |
| delegate.getResourceFolders(project) |
| |
| override val xmlParser: XmlParser |
| get() = delegate.xmlParser |
| |
| override fun replaceDetector( |
| detectorClass: Class<out Detector> |
| ): Class<out Detector> = |
| delegate.replaceDetector(detectorClass) |
| |
| override fun getSdkInfo(project: Project): SdkInfo = delegate.getSdkInfo(project) |
| |
| override fun getProject(dir: File, referenceDir: File): Project = |
| delegate.getProject(dir, referenceDir) |
| |
| override fun getUastParser(project: Project?): UastParser = delegate.getUastParser(project) |
| |
| override fun findResource(relativePath: String): File? = delegate.findResource(relativePath) |
| |
| override fun getCacheDir(name: String?, create: Boolean): File? = |
| delegate.getCacheDir(name, create) |
| |
| override fun getClassPath(project: Project): LintClient.ClassPathInfo = |
| delegate.performGetClassPath(project) |
| |
| override fun log( |
| exception: Throwable?, |
| format: String?, |
| vararg args: Any |
| ) = delegate.log(exception, format, *args) |
| |
| override fun initializeProjects(knownProjects: Collection<Project>): Unit = unsupported() |
| |
| override fun disposeProjects(knownProjects: Collection<Project>): Unit = unsupported() |
| |
| override fun getSdkHome(): File? = delegate.getSdkHome() |
| |
| override fun getTargets(): Array<IAndroidTarget> = delegate.getTargets() |
| |
| override fun getSdk(): AndroidSdkHandler? = delegate.getSdk() |
| |
| override fun getCompileTarget(project: Project): IAndroidTarget? = |
| delegate.getCompileTarget(project) |
| |
| override fun getSuperClass(project: Project, name: String): String? = |
| delegate.getSuperClass(project, name) |
| |
| override fun isSubclassOf( |
| project: Project, |
| name: String, |
| superClassName: String |
| ): Boolean? = |
| delegate.isSubclassOf(project, name, superClassName) |
| |
| override fun getProjectName(project: Project): String = delegate.getProjectName(project) |
| |
| override fun isGradleProject(project: Project): Boolean = delegate.isGradleProject(project) |
| |
| override fun createProject(dir: File, referenceDir: File): Project = unsupported() |
| |
| override fun findGlobalRuleJars(): List<File> = delegate.findGlobalRuleJars() |
| |
| override fun findRuleJars(project: Project): List<File> = delegate.findRuleJars(project) |
| |
| override fun isProjectDirectory(dir: File): Boolean = delegate.isProjectDirectory(dir) |
| |
| override fun registerProject(dir: File, project: Project): Unit = unsupported() |
| |
| override fun addCustomLintRules(registry: IssueRegistry): IssueRegistry = |
| delegate.addCustomLintRules(registry) |
| |
| override fun getAssetFolders(project: Project): List<File> = |
| delegate.getAssetFolders(project) |
| |
| override fun createUrlClassLoader(urls: Array<URL>, parent: ClassLoader): ClassLoader = |
| delegate.createUrlClassLoader(urls, parent) |
| |
| override fun checkForSuppressComments(): Boolean = delegate.checkForSuppressComments() |
| |
| override fun supportsProjectResources(): Boolean = delegate.supportsProjectResources() |
| |
| override fun getResourceRepository( |
| project: Project, |
| includeModuleDependencies: Boolean, |
| includeLibraries: Boolean |
| ): ResourceRepository? = |
| delegate.getResourceRepository( |
| project, includeModuleDependencies, |
| includeLibraries |
| ) |
| |
| override fun getRepositoryLogger(): ProgressIndicator = delegate.getRepositoryLogger() |
| |
| override fun getResourceVisibilityProvider(): ResourceVisibilityLookup.Provider = |
| delegate.getResourceVisibilityProvider() |
| |
| override fun createResourceItemHandle(item: ResourceItem): Location.Handle = |
| delegate.createResourceItemHandle(item) |
| |
| @Throws(IOException::class) |
| override fun openConnection(url: URL): URLConnection? = delegate.openConnection(url) |
| |
| @Throws(IOException::class) |
| override fun openConnection(url: URL, timeout: Int): URLConnection? = |
| delegate.openConnection(url, timeout) |
| |
| override fun closeConnection(connection: URLConnection) = |
| delegate.closeConnection(connection) |
| |
| override fun getGradleVisitor(): GradleVisitor = delegate.getGradleVisitor() |
| |
| override fun getGeneratedResourceFolders(project: Project): List<File> { |
| return delegate.getGeneratedResourceFolders(project) |
| } |
| |
| override fun getHighestKnownVersion( |
| coordinate: GradleCoordinate, |
| filter: Predicate<GradleVersion>? |
| ): GradleVersion? { |
| return delegate.getHighestKnownVersion(coordinate, filter) |
| } |
| } |
| |
| private val runLaterOutsideReadActionList = mutableListOf<Runnable>() |
| |
| /** |
| * Runs [runnable] later after running file detectors, _without_ holding the PSI read lock. |
| * Useful for network requests, for example, where we want to avoid freezing the UI. |
| * Runnables will be run in the order that they are added here. |
| * |
| * Important: the [runnable] is responsible for initiating its own read actions using |
| * [LintClient.runReadAction] if it needs to access PSI. Keep in mind that |
| * some Lint methods may access PSI implicitly, such as [Context.report]. |
| */ |
| fun runLaterOutsideReadAction(runnable: Runnable) { |
| runLaterOutsideReadActionList.add(runnable) |
| } |
| |
| private fun runDelayedRunnables() { |
| // We allow the list of "run later" runnables to grow during iteration. |
| var i = 0 |
| while (i < runLaterOutsideReadActionList.size) { |
| runLaterOutsideReadActionList[i].run() |
| if (isCanceled) { |
| return |
| } |
| ++i |
| } |
| runLaterOutsideReadActionList.clear() |
| } |
| |
| /** |
| * Requests another pass through the data for the given detector. This is |
| * typically done when a detector needs to do more expensive computation, |
| * but it only wants to do this once it **knows** that an error is |
| * present, or once it knows more specifically what to check for. |
| * |
| * @param detector the detector that should be included in the next pass. |
| * Note that the lint runner may refuse to run more than a couple |
| * of runs. |
| * |
| * @param scope the scope to be revisited. This must be a subset of the |
| * current scope ([.getScope], and it is just a performance hint; |
| * in particular, the detector should be prepared to be called on other |
| * scopes as well (since they may have been requested by other detectors). |
| * You can pall null to indicate "all". |
| */ |
| fun requestRepeat(detector: Detector, scope: EnumSet<Scope>?) { |
| if (repeatingDetectors == null) { |
| repeatingDetectors = ArrayList() |
| } |
| repeatingDetectors!!.add(detector) |
| |
| if (scope != null) { |
| if (repeatScope == null) { |
| repeatScope = scope |
| } else { |
| repeatScope = EnumSet.copyOf(repeatScope) |
| repeatScope!!.addAll(scope) |
| } |
| } else { |
| repeatScope = Scope.ALL |
| } |
| } |
| |
| // Unfortunately, ASMs nodes do not extend a common DOM node type with parent |
| // pointers, so we have to have multiple methods which pass in each type |
| // of node (class, method, field) to be checked. |
| |
| /** |
| * Returns whether the given issue is suppressed in the given method. |
| * |
| * @param issue the issue to be checked, or null to just check for "all" |
| * |
| * @param classNode the class containing the issue |
| * |
| * @param method the method containing the issue |
| * |
| * @param instruction the instruction within the method, if any |
| * |
| * @return true if there is a suppress annotation covering the specific |
| * issue on this method |
| */ |
| fun isSuppressed( |
| issue: Issue?, |
| classNode: ClassNode, |
| method: MethodNode, |
| instruction: AbstractInsnNode? |
| ): Boolean { |
| if (method.invisibleAnnotations != null) { |
| @Suppress("UNCHECKED_CAST") |
| val annotations = method.invisibleAnnotations as List<AnnotationNode> |
| return isSuppressed(issue, annotations) |
| } |
| |
| // Initializations of fields end up placed in generated methods (<init> |
| // for members and <clinit> for static fields). |
| if (instruction != null && method.name[0] == '<') { |
| val next = getNextInstruction(instruction) |
| if (next != null && next.type == AbstractInsnNode.FIELD_INSN) { |
| val fieldRef = next as FieldInsnNode? |
| val field = findField(classNode, fieldRef!!.owner, fieldRef.name) |
| if (field != null && isSuppressed(issue, field)) { |
| return true |
| } |
| } else if (classNode.outerClass != null && classNode.outerMethod == null && |
| isAnonymousClass(classNode) |
| ) { |
| if (isSuppressed(issue, classNode)) { |
| return true |
| } |
| } |
| } |
| |
| return false |
| } |
| |
| private fun findField( |
| classNode: ClassNode, |
| owner: String, |
| name: String |
| ): FieldNode? { |
| var current: ClassNode? = classNode |
| while (current != null) { |
| if (owner == current.name) { |
| val fieldList = current.fields // ASM API |
| for (f in fieldList) { |
| val field = f as FieldNode |
| if (field.name == name) { |
| return field |
| } |
| } |
| return null |
| } |
| current = getOuterClassNode(current) |
| } |
| return null |
| } |
| |
| private fun findMethod( |
| classNode: ClassNode, |
| name: String, |
| includeInherited: Boolean |
| ): MethodNode? { |
| var current: ClassNode? = classNode |
| while (current != null) { |
| val methodList = current.methods // ASM API |
| for (f in methodList) { |
| val method = f as MethodNode |
| if (method.name == name) { |
| return method |
| } |
| } |
| |
| current = if (includeInherited) { |
| getOuterClassNode(current) |
| } else { |
| break |
| } |
| } |
| return null |
| } |
| |
| /** |
| * Returns whether the given issue is suppressed for the given field. |
| * |
| * @param issue the issue to be checked, or null to just check for "all" |
| * |
| * @param field the field potentially annotated with a suppress annotation |
| * |
| * @return true if there is a suppress annotation covering the specific |
| * issue on this field |
| */ |
| // API; reserve need to require driver state later |
| fun isSuppressed(issue: Issue?, field: FieldNode): Boolean { |
| if (field.invisibleAnnotations != null) { |
| @Suppress("UNCHECKED_CAST") |
| val annotations = field.invisibleAnnotations as List<AnnotationNode> |
| return isSuppressed(issue, annotations) |
| } |
| |
| return false |
| } |
| |
| /** |
| * Returns whether the given issue is suppressed in the given class. |
| * |
| * @param issue the issue to be checked, or null to just check for "all" |
| * |
| * @param classNode the class containing the issue |
| * |
| * @return true if there is a suppress annotation covering the specific |
| * issue in this class |
| */ |
| fun isSuppressed(issue: Issue?, classNode: ClassNode): Boolean { |
| if (classNode.invisibleAnnotations != null) { |
| @Suppress("UNCHECKED_CAST") |
| val annotations = classNode.invisibleAnnotations as List<AnnotationNode> |
| return isSuppressed(issue, annotations) |
| } |
| |
| if (classNode.outerClass != null && classNode.outerMethod == null && |
| isAnonymousClass(classNode) |
| ) { |
| val outer = getOuterClassNode(classNode) |
| if (outer != null) { |
| var m = findMethod(outer, CONSTRUCTOR_NAME, false) |
| if (m != null) { |
| val call = findConstructorInvocation(m, classNode.name) |
| if (call != null) { |
| if (isSuppressed(issue, outer, m, call)) { |
| return true |
| } |
| } |
| } |
| m = findMethod(outer, CLASS_CONSTRUCTOR, false) |
| if (m != null) { |
| val call = findConstructorInvocation(m, classNode.name) |
| if (call != null) { |
| if (isSuppressed(issue, outer, m, call)) { |
| return true |
| } |
| } |
| } |
| } |
| } |
| |
| return false |
| } |
| |
| private fun isSuppressed(issue: Issue?, annotations: List<AnnotationNode>): Boolean { |
| for (annotation in annotations) { |
| val desc = annotation.desc |
| |
| // We could obey @SuppressWarnings("all") too, but no need to look for it |
| // because that annotation only has source retention. |
| |
| if (desc.endsWith(SUPPRESS_LINT_VMSIG)) { |
| if (annotation.values != null) { |
| var i = 0 |
| val n = annotation.values.size |
| while (i < n) { |
| val key = annotation.values[i] as String |
| if (key == "value") { |
| val value = annotation.values[i + 1] |
| if (value is String) { |
| if (matches(issue, value)) { |
| return true |
| } |
| } else if (value is List<*>) { |
| for (v in value) { |
| if (v is String) { |
| if (matches(issue, v)) { |
| return true |
| } |
| } |
| } |
| } |
| } |
| i += 2 |
| } |
| } |
| } |
| } |
| |
| return false |
| } |
| |
| fun isSuppressed( |
| context: JavaContext?, |
| issue: Issue, |
| scope: UElement? |
| ): Boolean { |
| val customSuppressNames = if (!allowSuppress) { |
| issue.suppressNames?.toSet() |
| } else { |
| null |
| } |
| |
| var currentScope = scope |
| val checkComments = client.checkForSuppressComments() && |
| context != null && context.containsCommentSuppress() |
| while (currentScope != null) { |
| if (currentScope is UAnnotated) { |
| if (isSuppressed(issue, currentScope)) { |
| if (customSuppressNames != null && context != null) { |
| flagInvalidSuppress( |
| context, issue, context.getLocation(currentScope), |
| issue.suppressNames |
| ) |
| return false |
| } |
| return true |
| } |
| |
| if (customSuppressNames != null && |
| isAnnotatedWith(currentScope, customSuppressNames) |
| ) { |
| return true |
| } |
| } |
| |
| if (checkComments && context != null && |
| context.isSuppressedWithComment(currentScope, issue) |
| ) { |
| if (customSuppressNames != null) { |
| flagInvalidSuppress( |
| context, issue, context.getLocation(currentScope), |
| issue.suppressNames |
| ) |
| return false |
| } |
| return true |
| } |
| |
| if (currentScope is UFile) { |
| return false |
| } |
| currentScope = currentScope.uastParent |
| } |
| |
| return false |
| } |
| |
| fun isSuppressed( |
| context: JavaContext?, |
| issue: Issue, |
| scope: PsiElement? |
| ): Boolean { |
| scope ?: return false |
| |
| val customSuppressNames = if (!allowSuppress) { |
| issue.suppressNames?.toSet() |
| } else { |
| null |
| } |
| |
| var currentScope = scope |
| val checkComments = client.checkForSuppressComments() && |
| context != null && context.containsCommentSuppress() |
| while (currentScope != null) { |
| if (currentScope is PsiModifierListOwner) { |
| val modifierList = currentScope.modifierList |
| if (isSuppressed(issue, modifierList)) { |
| if (customSuppressNames != null && context != null) { |
| flagInvalidSuppress( |
| context, issue, context.getLocation(currentScope), |
| issue.suppressNames |
| ) |
| return false |
| } |
| return true |
| } |
| |
| if (customSuppressNames != null && |
| isAnnotatedWith(modifierList, customSuppressNames) |
| ) { |
| return true |
| } |
| } |
| |
| if (checkComments && context!!.isSuppressedWithComment(currentScope, issue)) { |
| if (customSuppressNames != null) { |
| flagInvalidSuppress( |
| context, issue, context.getLocation(currentScope), |
| issue.suppressNames |
| ) |
| return false |
| } |
| return true |
| } |
| |
| if (currentScope is PsiFile) { |
| return false |
| } |
| |
| currentScope = currentScope.parent |
| } |
| |
| return false |
| } |
| |
| fun isSuppressed( |
| context: JavaContext?, |
| issue: Issue, |
| scope: UAnnotated? |
| ): Boolean { |
| scope ?: return false |
| |
| val customSuppressNames = if (!allowSuppress) { |
| issue.suppressNames?.toSet() |
| } else { |
| null |
| } |
| |
| var currentScope: UAnnotated = scope |
| val checkComments = client.checkForSuppressComments() && |
| context != null && context.containsCommentSuppress() |
| while (true) { |
| if (isSuppressed(issue, currentScope)) { |
| if (customSuppressNames != null && context != null) { |
| flagInvalidSuppress( |
| context, issue, context.getLocation(currentScope), |
| issue.suppressNames |
| ) |
| return false |
| } |
| return true |
| } |
| |
| if (customSuppressNames != null && |
| isAnnotatedWith(currentScope, customSuppressNames) |
| ) { |
| return true |
| } |
| |
| if (checkComments && context!!.isSuppressedWithComment(currentScope, issue)) { |
| if (customSuppressNames != null) { |
| flagInvalidSuppress( |
| context, issue, context.getLocation(currentScope), |
| issue.suppressNames |
| ) |
| return false |
| } |
| return true |
| } |
| currentScope = currentScope.getParentOfType(UAnnotated::class.java) ?: return false |
| if (currentScope is PsiFile) { |
| return false |
| } |
| } |
| } |
| |
| private fun flagInvalidSuppress( |
| context: Context, |
| issue: Issue, |
| location: Location, |
| names: Collection<String>? |
| ) { |
| var message = "Issue `${issue.id}` is not allowed to be suppressed" |
| if (names != null) { |
| message += " (but can be with ${ |
| formatList( |
| names.map { "`@${it.substring(it.lastIndexOf('.') + 1)}`" }.toList(), |
| sort = false, |
| useConjunction = true |
| ) |
| })" |
| } |
| |
| context.report(IssueRegistry.LINT_ERROR, location, message) |
| } |
| |
| /** |
| * Returns whether the given issue is suppressed in the given XML DOM node. |
| * |
| * @param issue the issue to be checked, or null to just check for "all" |
| * |
| * @param node the DOM node containing the issue |
| * |
| * @return true if there is a suppress annotation covering the specific |
| * issue in this class |
| */ |
| fun isSuppressed( |
| context: XmlContext?, |
| issue: Issue, |
| node: org.w3c.dom.Node? |
| ): Boolean { |
| if (context != null && context.resourceFolderType == null && node != null) { |
| // manifest file |
| // Look for merged manifest source nodes |
| if (context.client.isMergeManifestNode(node)) { |
| val source = context.client.findManifestSourceNode(node) |
| if (source != null) { |
| val sourceNode = source.second |
| if (sourceNode != null && sourceNode != node) { |
| return isSuppressed(context, issue, source.second) |
| } |
| } |
| } |
| } |
| |
| var currentNode = node |
| if (currentNode is Attr) { |
| currentNode = currentNode.ownerElement |
| } |
| val checkComments = client.checkForSuppressComments() && |
| context != null && context.containsCommentSuppress() |
| while (currentNode != null) { |
| if (currentNode.nodeType == org.w3c.dom.Node.ELEMENT_NODE) { |
| val element = currentNode as Element |
| if (element.hasAttributeNS(TOOLS_URI, ATTR_IGNORE)) { |
| val ignore = element.getAttributeNS(TOOLS_URI, ATTR_IGNORE) |
| if (isSuppressed(issue, ignore)) { |
| return true |
| } |
| } else if (checkComments && context!!.isSuppressedWithComment(currentNode, issue)) { |
| return true |
| } |
| } |
| |
| currentNode = currentNode.parentNode |
| } |
| |
| return false |
| } |
| |
| private var cachedFolder: File? = null |
| private var cachedFolderVersion = -1 |
| |
| /** |
| * Returns the folder version of the given file. For example, for the file values-v14/foo.xml, |
| * it returns 14. |
| * |
| * @param resourceFile the file to be checked |
| * |
| * @return the folder version, or -1 if no specific version was specified |
| */ |
| fun getResourceFolderVersion(resourceFile: File): Int { |
| val parent = resourceFile.parentFile ?: return -1 |
| if (parent == cachedFolder) { |
| return cachedFolderVersion |
| } |
| |
| cachedFolder = parent |
| cachedFolderVersion = -1 |
| |
| for (qualifier in QUALIFIER_SPLITTER.split(parent.name)) { |
| val matcher = VERSION_PATTERN.matcher(qualifier) |
| if (matcher.matches()) { |
| val group = matcher.group(1)!! |
| cachedFolderVersion = Integer.parseInt(group) |
| break |
| } |
| } |
| |
| return cachedFolderVersion |
| } |
| |
| companion object { |
| /** |
| * Max number of passes to run through the lint runner if requested by |
| * [.requestRepeat] |
| */ |
| private const val MAX_PHASES = 3 |
| |
| private const val SUPPRESS_LINT_VMSIG = "/$SUPPRESS_LINT;" |
| |
| /** Prefix used by the comment suppress mechanism in Studio/IntelliJ */ |
| private const val STUDIO_ID_PREFIX = "AndroidLint" |
| |
| private const val SUPPRESS_WARNINGS_FQCN = "java.lang.SuppressWarnings" |
| |
| private val DEFAULT_SUPPRESS_ANNOTATIONS = setOf( |
| FQCN_SUPPRESS_LINT, |
| SUPPRESS_WARNINGS_FQCN, |
| KOTLIN_SUPPRESS, |
| // When missing imports |
| SUPPRESS_LINT |
| ) |
| |
| /** |
| * For testing only: returns the number of exceptions thrown during Java AST analysis |
| * |
| * @return the number of internal errors found |
| */ |
| @get:VisibleForTesting |
| @JvmStatic |
| var crashCount: Int = 0 |
| private set |
| |
| /** Max number of logs to include */ |
| private const val MAX_REPORTED_CRASHES = 20 |
| |
| /** |
| * Handles an exception and returns whether the lint analysis can continue (true means |
| * continue, false means abort) |
| */ |
| @JvmStatic |
| fun handleDetectorError( |
| context: Context?, |
| driver: LintDriver, |
| throwable: Throwable |
| ): Boolean { |
| when { |
| throwable is IndexNotReadyException -> { |
| // Attempting to access PSI during startup before indices are ready; |
| // ignore these (because once indexing is over highlighting will be |
| // retriggered.) |
| // |
| // See http://b.android.com/176644 for an example. |
| return true |
| } |
| throwable is ProcessCanceledException -> { |
| // Cancelling inspections in the IDE |
| driver.cancel() |
| return false |
| } |
| throwable is AssertionError && |
| throwable.message?.startsWith("Already disposed: ") == true -> { |
| // Editor is in the middle of analysis when project |
| // is created. This isn't common, but is often triggered by Studio UI |
| // testsuite which rapidly opens, edits and closes projects. |
| // Silently abort the analysis. |
| return false |
| } |
| } |
| |
| if (crashCount++ > MAX_REPORTED_CRASHES) { |
| // No need to keep spamming the user that a lot of the files |
| // are tripping up ECJ, they get the picture. |
| return true |
| } |
| |
| val sb = StringBuilder(100) |
| sb.append("Unexpected failure during lint analysis") |
| context?.file?.name?.let { sb.append(" of ").append(it) } |
| sb.append(" (this is a bug in lint or one of the libraries it depends on)\n\n") |
| if (throwable.message?.isNotBlank() == true) { |
| sb.append("Message: ${throwable.message}\n") |
| } |
| sb.append("Stack: ") |
| sb.append("`") |
| sb.append(throwable.javaClass.simpleName) |
| sb.append(':') |
| appendStackTraceSummary(throwable, sb) |
| sb.append("`") |
| sb.append( |
| "\n\nYou can set environment variable `LINT_PRINT_STACKTRACE=true` to " + |
| "dump a full stacktrace to stdout." |
| ) |
| |
| val throwableMessage = throwable.message |
| if (throwableMessage != null && throwableMessage.startsWith( |
| "loader constraint violation: when resolving field \"QUALIFIER_SPLITTER\" the class loader" |
| ) |
| ) { |
| // Rewrite error message |
| sb.setLength(0) |
| sb.append( |
| """ |
| Lint crashed because it is being invoked with the wrong version of Guava |
| (the Android version instead of the JRE version, which is required in the |
| Gradle plugin). |
| |
| This usually happens when projects incorrectly install a dependency resolution |
| strategy in **all** configurations instead of just the compile and run |
| configurations. |
| |
| See https://issuetracker.google.com/71991293 for more information and the |
| proper way to install a dependency resolution strategy. |
| |
| (Note that this breaks a lot of lint analysis so this report is incomplete.) |
| """.trimIndent() |
| ) |
| } |
| |
| val project = when { |
| driver.currentProject != null -> driver.currentProject |
| driver.currentProjects?.isNotEmpty() == true -> driver.currentProjects?.last() |
| else -> null |
| } |
| val message = sb.toString() |
| when { |
| context != null -> context.report( |
| IssueRegistry.LINT_ERROR, |
| Location.create(context.file), |
| message |
| ) |
| project != null -> { |
| val projectDir = project.dir |
| val projectContext = Context(driver, project, null, projectDir) |
| projectContext.report( |
| IssueRegistry.LINT_ERROR, |
| Location.create(project.dir), |
| message |
| ) |
| } |
| else -> driver.client.log(throwable, message) |
| } |
| |
| if (VALUE_TRUE == System.getenv("LINT_PRINT_STACKTRACE") || |
| VALUE_TRUE == System.getProperty("lint.print-stacktrace") |
| ) { |
| throwable.printStackTrace() |
| } |
| |
| return true |
| } |
| |
| fun appendStackTraceSummary(throwable: Throwable, sb: StringBuilder) { |
| val stackTrace = throwable.stackTrace |
| var count = 0 |
| for (frame in stackTrace) { |
| if (count > 0) { |
| sb.append('\u2190') // Left arrow |
| } |
| |
| val className = frame.className |
| sb.append(className.substring(className.lastIndexOf('.') + 1)) |
| sb.append('.').append(frame.methodName) |
| sb.append('(') |
| sb.append(frame.fileName).append(':').append(frame.lineNumber) |
| sb.append(')') |
| count++ |
| // Only print the top N frames such that we can identify the bug |
| if (count == 8) { |
| break |
| } |
| } |
| } |
| |
| /** |
| * For testing only: clears the crash counter |
| */ |
| @JvmStatic |
| @VisibleForTesting |
| fun clearCrashCount() { |
| crashCount = 0 |
| } |
| |
| @Contract("!null,_->!null") |
| private fun union( |
| list1: List<Detector>?, |
| list2: List<Detector>? |
| ): List<Detector>? = |
| when { |
| list1 == null -> list2 |
| list2 == null -> list1 |
| else -> { |
| // Use set to pick out unique detectors, since it's possible for there to be overlap, |
| // e.g. the DuplicateIdDetector registers both a cross-resource issue and a |
| // single-file issue, so it shows up on both scope lists: |
| val set = HashSet<Detector>(list1.size + list2.size) |
| set.addAll(list1) |
| set.addAll(list2) |
| |
| ArrayList(set) |
| } |
| } |
| |
| private fun gatherJavaFiles(dir: File, result: MutableList<File>) { |
| val files = dir.listFiles() |
| if (files != null) { |
| for (file in files) { |
| if (file.isFile) { |
| val path = file.path |
| if (path.endsWith(DOT_JAVA) || path.endsWith(DOT_KT)) { |
| result.add(file) |
| } |
| } else if (file.isDirectory) { |
| gatherJavaFiles(file, result) |
| } |
| } |
| } |
| } |
| |
| private fun findConstructorInvocation( |
| method: MethodNode, |
| className: String |
| ): MethodInsnNode? { |
| val nodes = method.instructions |
| var i = 0 |
| val n = nodes.size() |
| while (i < n) { |
| val instruction = nodes.get(i) |
| if (instruction.opcode == Opcodes.INVOKESPECIAL) { |
| val call = instruction as MethodInsnNode |
| if (className == call.owner) { |
| return call |
| } |
| } |
| i++ |
| } |
| |
| return null |
| } |
| |
| private fun matches(issue: Issue?, id: String): Boolean { |
| if (id.equals(SUPPRESS_ALL, ignoreCase = true)) { |
| return true |
| } |
| |
| if (issue != null) { |
| val issueId = issue.id |
| if (id.equals(issueId, ignoreCase = true)) { |
| return true |
| } |
| if (id.startsWith(STUDIO_ID_PREFIX) && |
| id.regionMatches( |
| STUDIO_ID_PREFIX.length, |
| issueId, |
| 0, |
| issueId.length, |
| ignoreCase = true |
| ) && |
| id.substring(STUDIO_ID_PREFIX.length).equals(issueId, ignoreCase = true) |
| ) { |
| return true |
| } |
| } |
| |
| return false |
| } |
| |
| /** |
| * Returns true if the given issue is suppressed by the given suppress string; this |
| * is typically the same as the issue id, but is allowed to not match case sensitively, |
| * and is allowed to be a comma separated list, and can be the string "all" |
| * |
| * @param issue the issue id to match |
| * |
| * @param string the suppress string -- typically the id, or "all", or a comma separated list |
| * of ids |
| * |
| * @return true if the issue is suppressed by the given string |
| */ |
| private fun isSuppressed(issue: Issue, string: String): Boolean { |
| if (string.isEmpty()) { |
| return false |
| } |
| |
| if (string.indexOf(',') == -1) { |
| if (matches(issue, string)) { |
| return true |
| } |
| } else { |
| for (id in Splitter.on(',').trimResults().split(string)) { |
| if (matches(issue, id)) { |
| return true |
| } |
| } |
| } |
| |
| return false |
| } |
| |
| /** |
| * Returns true if the given AST modifier has a suppress annotation for the |
| * given issue (which can be null to check for the "all" annotation) |
| * |
| * @param issue the issue to be checked |
| * |
| * @param modifierList the modifier to check |
| * |
| * @return true if the issue or all issues should be suppressed for this |
| * modifier |
| */ |
| @JvmStatic |
| fun isSuppressed( |
| issue: Issue, |
| modifierList: PsiModifierList? |
| ): Boolean { |
| if (modifierList == null) { |
| return false |
| } |
| |
| for (annotation in modifierList.annotations) { |
| val fqcn = annotation.qualifiedName |
| if (fqcn != null && (fqcn == FQCN_SUPPRESS_LINT || |
| fqcn == SUPPRESS_WARNINGS_FQCN || |
| fqcn == KOTLIN_SUPPRESS || |
| // when missing imports |
| fqcn == SUPPRESS_LINT) |
| ) { |
| val parameterList = annotation.parameterList |
| for (pair in parameterList.attributes) { |
| if (isSuppressed(issue, pair.value)) { |
| return true |
| } |
| } |
| } |
| } |
| |
| return false |
| } |
| |
| private fun isAnnotatedWith( |
| modifierList: PsiModifierList?, |
| names: Set<String> |
| ): Boolean { |
| if (modifierList == null) { |
| return false |
| } |
| |
| for (annotation in modifierList.annotations) { |
| val fqcn = annotation.qualifiedName |
| if (fqcn != null && names.contains(fqcn)) { |
| return true |
| } |
| } |
| |
| return false |
| } |
| |
| /** |
| * Returns true if the given AST modifier has a suppress annotation for the |
| * given issue (which can be null to check for the "all" annotation) |
| * |
| * @param issue the issue to be checked |
| * |
| * @param annotated the annotated element |
| * |
| * @return true if the issue or all issues should be suppressed for this |
| * modifier |
| */ |
| @JvmStatic |
| fun isSuppressed(issue: Issue, annotated: UAnnotated): Boolean { |
| val annotations = annotated.annotations |
| if (annotations.isEmpty()) { |
| return false |
| } |
| |
| for (annotation in annotations) { |
| val fqcn = annotation.qualifiedName |
| if (fqcn != null && (fqcn == FQCN_SUPPRESS_LINT || |
| fqcn == SUPPRESS_WARNINGS_FQCN || |
| fqcn == KOTLIN_SUPPRESS || |
| // when missing imports |
| fqcn == SUPPRESS_LINT) |
| ) { |
| val attributeList = annotation.attributeValues |
| for (attribute in attributeList) { |
| if (isSuppressedExpression(issue, attribute.expression)) { |
| return true |
| } |
| } |
| } else if (fqcn == null) { |
| // Work around type resolution problems |
| // Work around bugs in UAST type resolution for file annotations: |
| // parse the source string instead. |
| val psi = annotation.psi ?: continue |
| if (psi is PsiCompiledElement) { |
| continue |
| } |
| val text = psi.text |
| if (text.contains("SuppressLint(") || |
| text.contains("SuppressWarnings(") || |
| text.contains("Suppress(") |
| ) { |
| val start = text.indexOf('(') |
| val end = text.indexOf(')', start + 1) |
| if (end != -1) { |
| var value = text.substring(start + 1, end) |
| |
| // Strip off attribute name, e.g. |
| // @SuppressLint(id = "O") -> O |
| val index = value.indexOf('=') |
| if (index != -1) { |
| value = value.substring(index + 1).trim() |
| } |
| |
| // We're looking at source, so get rid of extra syntax |
| // characters, e.g. from { "foo", "bar" } to just foo, bar |
| // |
| value = value.replace(Regex("[\"{}]"), "") |
| |
| if (isSuppressed(issue, value)) { |
| return true |
| } |
| } |
| } |
| } |
| } |
| |
| return false |
| } |
| |
| private fun isAnnotatedWith( |
| annotated: UAnnotated, |
| names: Set<String> |
| ): Boolean { |
| val annotations = annotated.annotations |
| if (annotations.isEmpty()) { |
| return false |
| } |
| |
| for (annotation in annotations) { |
| val fqcn = annotation.qualifiedName |
| if (fqcn != null && names.contains(fqcn)) { |
| return true |
| } |
| } |
| |
| return false |
| } |
| |
| /** |
| * Returns true if the annotation member value, assumed to be specified on a a SuppressWarnings |
| * or SuppressLint annotation, specifies the given id (or "all"). |
| * |
| * @param issue the issue to be checked |
| * |
| * @param value the member value to check |
| * |
| * @return true if the issue or all issues should be suppressed for this modifier |
| */ |
| @JvmStatic |
| fun isSuppressed( |
| issue: Issue, |
| value: PsiAnnotationMemberValue? |
| ): Boolean { |
| if (value is PsiLiteral) { |
| val literalValue = value.value |
| if (literalValue is String) { |
| if (isSuppressed(issue, literalValue)) { |
| return true |
| } |
| } else if (literalValue == null) { |
| // Kotlin UAST workaround |
| val v = value.text.removeSurrounding("\"") |
| if (v.isNotEmpty() && isSuppressed(issue, v)) { |
| return true |
| } |
| } |
| } else if (value is PsiArrayInitializerMemberValue) { |
| for (mmv in value.initializers) { |
| if (isSuppressed(issue, mmv)) { |
| return true |
| } |
| } |
| } else if (value is PsiArrayInitializerExpression) { |
| val initializers = value.initializers |
| for (e in initializers) { |
| if (isSuppressed(issue, e)) { |
| return true |
| } |
| } |
| } |
| |
| return false |
| } |
| |
| /** |
| * Returns true if the annotation member value, assumed to be specified on a a S |
| * uppressWarnings or SuppressLint annotation, specifies the given id (or "all"). |
| * |
| * @param issue the issue to be checked |
| * |
| * @param value the member value to check |
| * |
| * @return true if the issue or all issues should be suppressed for this modifier |
| */ |
| @JvmStatic |
| private fun isSuppressedExpression(issue: Issue, value: UExpression?): Boolean { |
| if (value is ULiteralExpression) { |
| val literalValue = value.value |
| if (literalValue is String) { |
| if (isSuppressed(issue, literalValue)) { |
| return true |
| } |
| } |
| } else if (value is UCallExpression) { |
| for (mmv in value.valueArguments) { |
| if (isSuppressedExpression(issue, mmv)) { |
| return true |
| } |
| } |
| } |
| |
| return false |
| } |
| |
| /** Pattern for version qualifiers */ |
| private val VERSION_PATTERN = Pattern.compile("^v(\\d+)$") |
| } |
| } |