| /* |
| * Copyright (C) 2014 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.checks |
| |
| import com.android.SdkConstants |
| import com.android.SdkConstants.ANDROID_URI |
| import com.android.SdkConstants.ATTR_TARGET_SDK_VERSION |
| import com.android.SdkConstants.DOT_TOML |
| import com.android.SdkConstants.GRADLE_PLUGIN_MINIMUM_VERSION |
| import com.android.SdkConstants.GRADLE_PLUGIN_RECOMMENDED_VERSION |
| import com.android.SdkConstants.PLATFORM_WINDOWS |
| import com.android.SdkConstants.currentPlatform |
| import com.android.ide.common.gradle.Component |
| import com.android.ide.common.gradle.Dependency |
| import com.android.ide.common.gradle.RichVersion |
| import com.android.ide.common.gradle.Version |
| import com.android.ide.common.repository.AgpVersion |
| import com.android.ide.common.repository.GoogleMavenRepository |
| import com.android.ide.common.repository.GoogleMavenRepository.Companion.MAVEN_GOOGLE_CACHE_DIR_KEY |
| import com.android.ide.common.repository.MavenRepositories |
| import com.android.io.CancellableFileIo |
| import com.android.sdklib.AndroidTargetHash |
| import com.android.sdklib.SdkVersionInfo |
| import com.android.sdklib.SdkVersionInfo.HIGHEST_KNOWN_STABLE_API |
| import com.android.sdklib.SdkVersionInfo.LOWEST_ACTIVE_API |
| import com.android.tools.lint.checks.GooglePlaySdkIndex.Companion.GOOGLE_PLAY_SDK_INDEX_KEY |
| import com.android.tools.lint.checks.GooglePlaySdkIndex.Companion.GOOGLE_PLAY_SDK_INDEX_URL |
| import com.android.tools.lint.client.api.LintClient |
| import com.android.tools.lint.client.api.LintTomlDocument |
| import com.android.tools.lint.client.api.LintTomlMapValue |
| import com.android.tools.lint.client.api.LintTomlValue |
| import com.android.tools.lint.client.api.TomlContext |
| import com.android.tools.lint.client.api.TomlScanner |
| import com.android.tools.lint.detector.api.Category |
| import com.android.tools.lint.detector.api.Constraint |
| 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.GradleContext.Companion.getIntLiteralValue |
| import com.android.tools.lint.detector.api.GradleContext.Companion.getStringLiteralValue |
| import com.android.tools.lint.detector.api.GradleContext.Companion.isNonNegativeInteger |
| import com.android.tools.lint.detector.api.GradleContext.Companion.isStringLiteral |
| import com.android.tools.lint.detector.api.GradleScanner |
| import com.android.tools.lint.detector.api.Implementation |
| import com.android.tools.lint.detector.api.Incident |
| import com.android.tools.lint.detector.api.Issue |
| import com.android.tools.lint.detector.api.LintFix |
| import com.android.tools.lint.detector.api.LintMap |
| import com.android.tools.lint.detector.api.Location |
| import com.android.tools.lint.detector.api.Project |
| import com.android.tools.lint.detector.api.Scope |
| import com.android.tools.lint.detector.api.Severity |
| import com.android.tools.lint.detector.api.XmlContext |
| import com.android.tools.lint.detector.api.XmlScanner |
| import com.android.tools.lint.detector.api.getLanguageLevel |
| import com.android.tools.lint.detector.api.guessGradleLocation |
| import com.android.tools.lint.detector.api.isNumberString |
| import com.android.tools.lint.detector.api.minSdkLessThan |
| import com.android.tools.lint.detector.api.readUrlData |
| import com.android.tools.lint.detector.api.readUrlDataAsString |
| import com.android.tools.lint.model.LintModelArtifactType |
| import com.android.tools.lint.model.LintModelDependency |
| import com.android.tools.lint.model.LintModelExternalLibrary |
| import com.android.tools.lint.model.LintModelLibrary |
| import com.android.tools.lint.model.LintModelMavenName |
| import com.android.tools.lint.model.LintModelModuleType |
| import com.android.utils.XmlUtils |
| import com.android.utils.appendCapitalized |
| import com.android.utils.iterator |
| import com.android.utils.usLocaleCapitalize |
| import com.google.common.base.Joiner |
| import com.google.common.base.Splitter |
| import com.google.common.collect.ArrayListMultimap |
| import com.intellij.pom.java.LanguageLevel.JDK_1_7 |
| import com.intellij.pom.java.LanguageLevel.JDK_1_8 |
| import java.io.File |
| import java.io.IOException |
| import java.io.UnsupportedEncodingException |
| import java.net.URLEncoder |
| import java.nio.file.Path |
| import java.util.Calendar |
| import java.util.Collections |
| import java.util.EnumSet |
| import java.util.function.Predicate |
| import kotlin.text.Charsets.UTF_8 |
| import org.jetbrains.uast.UCallExpression |
| import org.w3c.dom.Attr |
| import org.w3c.dom.Element |
| |
| /** Checks Gradle files for potential errors. */ |
| open class GradleDetector : Detector(), GradleScanner, TomlScanner, XmlScanner { |
| |
| protected open val gradleUserHome: File |
| get() { |
| // See org.gradle.initialization.BuildLayoutParameters |
| var gradleUserHome: String? = System.getProperty("gradle.user.home") |
| if (gradleUserHome == null) { |
| gradleUserHome = System.getenv("GRADLE_USER_HOME") |
| if (gradleUserHome == null) { |
| gradleUserHome = System.getProperty("user.home") + File.separator + ".gradle" |
| } |
| } |
| |
| return File(gradleUserHome) |
| } |
| |
| private var artifactCacheHome: File? = null |
| |
| /** |
| * If incrementally editing a single build.gradle file, tracks whether we've already transitively |
| * checked GMS versions such that we don't flag the same error on every single dependency |
| * declaration. |
| */ |
| private var mCheckedGms: Boolean = false |
| |
| /** |
| * If incrementally editing a single build.gradle file, tracks whether we've already transitively |
| * checked wearable library versions such that we don't flag the same error on every single |
| * dependency declaration. |
| */ |
| private var mCheckedWearableLibs: Boolean = false |
| |
| /** |
| * If incrementally editing a single build.gradle file, tracks whether we've already applied |
| * kotlin-android plugin. |
| */ |
| private var mAppliedKotlinAndroidPlugin: Boolean = false |
| |
| /** |
| * If incrementally editing a single build.gradle file, tracks whether we've already applied |
| * kotlin-kapt plugin. |
| */ |
| private var mAppliedKotlinKaptPlugin: Boolean = false |
| |
| /** |
| * If incrementally editing a single build.gradle file, tracks whether we've already applied the |
| * KSP plugin. |
| */ |
| private var mAppliedKspPlugin: Boolean = false |
| |
| /** |
| * If incrementally editing a single build.gradle file, tracks whether we have applied a java |
| * plugin (e.g. application, java-library) |
| */ |
| private var mAppliedJavaPlugin: Boolean = false |
| |
| data class JavaPluginInfo(val cookie: Any) |
| |
| private var mJavaPluginInfo: JavaPluginInfo? = null |
| |
| private var mDeclaredSourceCompatibility: Boolean = false |
| private var mDeclaredTargetCompatibility: Boolean = false |
| |
| /** |
| * If incrementally editing a single build.gradle file, tracks whether we have declared the google |
| * maven repository in the buildscript block. |
| * |
| * Because there are many ways to declare repositories for plugin resolution (including e.g. in |
| * pluginManagement declarations in settings files), we track whether we have seen anything at all |
| * in buildscript repositories; if we haven't, we don't know whether the google maven repository |
| * is actually visible to the project. |
| */ |
| private var mDeclaredGoogleMavenRepository: Boolean = false |
| private var mDeclaredBuildscriptRepository: Boolean = false |
| |
| data class AgpVersionCheckInfo( |
| val newerVersion: Version, |
| val newerVersionIsSafe: Boolean, |
| val safeReplacement: Version?, |
| val dependency: Dependency, |
| val isResolved: Boolean, |
| val cookie: Any, |
| ) |
| |
| /** Stores information for a check of the Android gradle plugin dependency version. */ |
| private var agpVersionCheckInfo: AgpVersionCheckInfo? = null |
| |
| private val blockedDependencies = HashMap<Project, BlockedDependencies>() |
| |
| // ---- Implements XmlScanner ---- |
| |
| override fun getApplicableAttributes(): Collection<String>? { |
| return listOf(ATTR_TARGET_SDK_VERSION) |
| } |
| |
| override fun visitAttribute(context: XmlContext, attribute: Attr) { |
| if (attribute.namespaceURI != ANDROID_URI) { |
| return |
| } |
| |
| val element = attribute.ownerElement |
| val target = attribute.value |
| try { |
| val targetSdkVersion = target.toInt() |
| val location = context.getLocation(attribute) |
| when ( |
| val tsdk = |
| checkTargetSdk( |
| context, |
| ManifestDetector.calendar ?: Calendar.getInstance(), |
| targetSdkVersion, |
| ) |
| ) { |
| is TargetSdkCheckResult.Expired -> { |
| context.report( |
| EXPIRED_TARGET_SDK_VERSION, |
| element, |
| location, |
| tsdk.message, |
| targetSdkLintFix(targetSdkVersion, tsdk.requiredVersion), |
| ) |
| } |
| is TargetSdkCheckResult.Expiring -> { |
| context.report( |
| EXPIRING_TARGET_SDK_VERSION, |
| element, |
| location, |
| tsdk.message, |
| targetSdkLintFix(targetSdkVersion, tsdk.requiredVersion), |
| ) |
| } |
| is TargetSdkCheckResult.NotLatest -> { |
| if (context.isEnabled(TARGET_NEWER)) { |
| context.report( |
| TARGET_NEWER, |
| element, |
| location, |
| tsdk.message, |
| targetSdkLintFix(targetSdkVersion, tsdk.highestVersion), |
| ) |
| } |
| } |
| is TargetSdkCheckResult.NoIssue -> {} |
| } |
| } catch (ignore: NumberFormatException) { |
| // Ignore: AAPT will enforce this. |
| } |
| } |
| |
| private fun targetSdkLintFix(current: Int, target: Int) = |
| if (LintClient.isStudio) { |
| fix().data("currentTargetSdkVersion", current) |
| } else { |
| fix() |
| .name("Update targetSdkVersion to $target") |
| .set(ANDROID_URI, ATTR_TARGET_SDK_VERSION, target.toString()) |
| .build() |
| } |
| |
| // ---- Implements GradleScanner ---- |
| |
| private fun checkOctal(context: GradleContext, value: String, cookie: Any) { |
| // (This will never be the case in KTS; if you try to insert "010" as an integer in Kotlin, you |
| // get a compiler error, "Unsupported [literal prefixes and suffixes]".) |
| if ( |
| value.length >= 2 && |
| value[0] == '0' && |
| (value.length > 2 || value[1] >= '8' && isNonNegativeInteger(value)) && |
| context.isEnabled(ACCIDENTAL_OCTAL) |
| ) { |
| var message = |
| "The leading 0 turns this number into octal which is probably not what was intended" |
| message += |
| try { |
| val numericValue = java.lang.Long.decode(value) |
| " (interpreted as $numericValue)" |
| } catch (exception: NumberFormatException) { |
| " (and it is not a valid octal number)" |
| } |
| |
| report(context, cookie, ACCIDENTAL_OCTAL, message) |
| } |
| } |
| |
| /** Called with for example "android", "defaultConfig", "minSdkVersion", "7" */ |
| override fun checkDslPropertyAssignment( |
| context: GradleContext, |
| property: String, |
| value: String, |
| parent: String, |
| parentParent: String?, |
| propertyCookie: Any, |
| valueCookie: Any, |
| statementCookie: Any, |
| ) { |
| if (parent == "defaultConfig") { |
| if (property == "targetSdkVersion" || property == "targetSdk") { |
| val version = getSdkVersion(value, valueCookie) |
| if (version > 0 && version < context.client.highestKnownApiLevel) { |
| when (val tsdk = checkTargetSdk(context, calendar ?: Calendar.getInstance(), version)) { |
| is TargetSdkCheckResult.Expired -> { |
| // Don't report if already suppressed with EXPIRING |
| val alreadySuppressed = |
| context.containsCommentSuppress() && |
| context.isSuppressedWithComment(statementCookie, EXPIRED_TARGET_SDK_VERSION) |
| |
| if (!alreadySuppressed) { |
| report( |
| context, |
| statementCookie, |
| EXPIRED_TARGET_SDK_VERSION, |
| tsdk.message, |
| fix().data("currentTargetSdkVersion", version).takeIf { LintClient.isStudio }, |
| ) |
| } |
| } |
| is TargetSdkCheckResult.Expiring -> { |
| report( |
| context, |
| statementCookie, |
| EXPIRING_TARGET_SDK_VERSION, |
| tsdk.message, |
| fix().data("currentTargetSdkVersion", version).takeIf { LintClient.isStudio }, |
| ) |
| } |
| is TargetSdkCheckResult.NotLatest -> { |
| val highest = tsdk.highestVersion |
| val label = "Update targetSdkVersion to $highest" |
| val fix = |
| if (LintClient.isStudio) { |
| fix().data("currentTargetSdkVersion", version) |
| } else { |
| fix().name(label).replace().text(value).with(highest.toString()).build() |
| } |
| report(context, statementCookie, TARGET_NEWER, tsdk.message, fix) |
| } |
| is TargetSdkCheckResult.NoIssue -> {} |
| } |
| } |
| if (version > 0) { |
| if (LintClient.isStudio) { |
| //noinspection FileComparisons |
| if (lastTargetSdkVersion == -1 || lastTargetSdkVersionFile != context.file) { |
| lastTargetSdkVersion = version |
| lastTargetSdkVersionFile = context.file |
| } else if (version > lastTargetSdkVersion) { |
| val message = |
| "It looks like you just edited the `targetSdkVersion` from $lastTargetSdkVersion to $version in the editor. " + |
| "Be sure to consult the documentation on the behaviors that change as result of this. " + |
| "The Android SDK Upgrade Assistant can help with safely migrating." |
| report( |
| context, |
| statementCookie, |
| EDITED_TARGET_SDK_VERSION, |
| message, |
| fix().data("currentTargetSdkVersion", version), |
| ) |
| } |
| } |
| } else { |
| checkIntegerAsString(context, value, statementCookie, valueCookie) |
| } |
| } else if (property == "minSdkVersion" || property == "minSdk") { |
| val version = getSdkVersion(value, valueCookie) |
| if (version > 0) { |
| checkMinSdkVersion(context, version, statementCookie) |
| } else { |
| checkIntegerAsString(context, value, statementCookie, valueCookie) |
| } |
| } |
| |
| if (value.startsWith("0")) { |
| checkOctal(context, value, valueCookie) |
| } |
| |
| if ( |
| property == "versionName" || |
| property == "versionCode" && !isNonNegativeInteger(value) || |
| !isStringLiteral(value) |
| ) { |
| // Method call -- make sure it does not match one of the getters in the |
| // configuration! |
| if (value == "getVersionCode" || value == "getVersionName") { |
| val message = |
| "Bad method name: pick a unique method name which does not " + |
| "conflict with the implicit getters for the defaultConfig " + |
| "properties. For example, try using the prefix compute- " + |
| "instead of get-." |
| report(context, statementCookie, GRADLE_GETTER, message) |
| } |
| } else if (property == "packageName") { |
| val message = "Deprecated: Replace 'packageName' with 'applicationId'" |
| val fix = |
| fix() |
| .name("Replace 'packageName' with 'applicationId'", true) |
| .replace() |
| .text("packageName") |
| .with("applicationId") |
| .autoFix() |
| .build() |
| report(context, propertyCookie, DEPRECATED, message, fix) |
| } |
| if ( |
| property == "versionCode" && |
| context.isEnabled(HIGH_APP_VERSION_CODE) && |
| isNonNegativeInteger(value) |
| ) { |
| val version = getIntLiteralValue(value, -1) |
| if (version >= VERSION_CODE_HIGH_THRESHOLD) { |
| val message = "The 'versionCode' is very high and close to the max allowed value" |
| report(context, statementCookie, HIGH_APP_VERSION_CODE, message) |
| } |
| } |
| } else if ( |
| (property == "compileSdkVersion" || property == "compileSdk") && parent.startsWith("android") |
| ) { |
| var version = -1 |
| if (isStringLiteral(value)) { |
| // Try to resolve values like "android-O" |
| val hash = getStringLiteralValue(value, valueCookie) |
| if (hash != null && !isNumberString(hash)) { |
| if (property == "compileSdk") { |
| val message = |
| "`compileSdk` does not support strings; did you mean `compileSdkPreview` ?" |
| val fix = fix().replace().text("compileSdk").with("compileSdkPreview").build() |
| report(context, statementCookie, STRING_INTEGER, message, fix) |
| } |
| |
| val platformVersion = AndroidTargetHash.getPlatformVersion(hash) |
| if (platformVersion != null) { |
| version = platformVersion.featureLevel |
| } |
| } |
| } else { |
| version = getIntLiteralValue(value, -1) |
| } |
| if (version <= 0) { |
| checkIntegerAsString(context, value, statementCookie, valueCookie) |
| } else if (version < HIGHEST_KNOWN_STABLE_API) { |
| val message = |
| "A newer version of `compileSdkVersion` than $version is available: $HIGHEST_KNOWN_STABLE_API" |
| val fix = |
| fix() |
| .name("Set compileSdkVersion to $HIGHEST_KNOWN_STABLE_API") |
| .replace() |
| .text(version.toString()) |
| .with(HIGHEST_KNOWN_STABLE_API.toString()) |
| .build() |
| report(context, statementCookie, DEPENDENCY, message, fix) |
| } |
| } else if (parent == "plugins") { |
| val plugin = |
| when (property) { |
| "id" -> getStringLiteralValue(value, valueCookie) |
| "alias" -> getPluginFromVersionCatalog(value, context)?.coordinates?.substringBefore(':') |
| else -> null |
| } |
| |
| when (plugin) { |
| null -> { |
| // Ignore, we couldn't find a plugin ID |
| } |
| "kotlin-android", |
| "org.jetbrains.kotlin.android" -> { |
| mAppliedKotlinAndroidPlugin = true |
| } |
| "kotlin-kapt", |
| "org.jetbrains.kotlin.kapt" -> { |
| mAppliedKotlinKaptPlugin = true |
| } |
| "com.google.devtools.ksp" -> { |
| mAppliedKspPlugin = true |
| } |
| in JAVA_PLUGIN_IDS -> { |
| mAppliedJavaPlugin = true |
| mJavaPluginInfo = JavaPluginInfo(statementCookie) |
| } |
| OLD_APP_PLUGIN_ID, |
| OLD_LIB_PLUGIN_ID -> { |
| val isOldAppPlugin = OLD_APP_PLUGIN_ID == plugin |
| val replaceWith = if (isOldAppPlugin) APP_PLUGIN_ID else LIB_PLUGIN_ID |
| val message = "'$plugin' is deprecated; use '$replaceWith' instead" |
| val fix = |
| fix() |
| .sharedName("Replace plugin") |
| .replace() |
| .text(plugin) |
| .with(replaceWith) |
| .autoFix() |
| .build() |
| report(context, valueCookie, DEPRECATED, message, fix) |
| } |
| } |
| } else if (parentParent == "plugins" && property == "version") { |
| val version = getStringLiteralValue(value, valueCookie) |
| if (version != null) { |
| val gradleCoordinate = "$parent:$parent.gradle.plugin:$version" |
| val dependency = Dependency.parse(gradleCoordinate) |
| // Check dependencies without the PSI read lock, because we |
| // may need to make network requests to retrieve version info. |
| context.driver.runLaterOutsideReadAction { |
| checkDependency(context, dependency, false, valueCookie, statementCookie) |
| } |
| } |
| } else if (parent == "dependencies" || parent == "declarativeDependencies") { |
| if (value.startsWith("files") && value.matches("^files\\(['\"].*[\"']\\)$".toRegex())) { |
| val path = value.substring("files('".length, value.length - 2) |
| if (path.contains("\\\\")) { |
| val fix = fix().replace().text(path).with(path.replace("\\\\", "/")).build() |
| val message = "Do not use Windows file separators in .gradle files; use / instead" |
| report(context, valueCookie, PATH, message, fix) |
| } else if (path.startsWith("/") || File(path.replace('/', File.separatorChar)).isAbsolute) { |
| val message = "Avoid using absolute paths in .gradle files" |
| report(context, valueCookie, PATH, message) |
| } |
| } else { |
| var dependencyString = getStringLiteralValue(value, valueCookie) |
| if ( |
| dependencyString == null && |
| (listOf("platform", "testFixtures", "enforcedPlatform").any { |
| value.startsWith("$it(") |
| } && value.endsWith(")")) |
| ) { |
| val argumentString = value.substring(value.indexOf('(') + 1, value.length - 1) |
| dependencyString = |
| if (valueCookie is UCallExpression && valueCookie.valueArguments.size == 1) { |
| getStringLiteralValue(argumentString, valueCookie.valueArguments.first()) |
| } else { |
| getStringLiteralValue(argumentString, valueCookie) |
| } |
| } |
| if (dependencyString == null) { |
| dependencyString = getNamedDependency(value) |
| } |
| // If the dependency is a GString (i.e. it uses Groovy variable substitution, |
| // with a $variable_name syntax) then don't try to parse it. |
| if (dependencyString != null) { |
| var dependency: Dependency? = Dependency.parse(dependencyString) |
| var isResolved = false |
| if (dependency != null && dependency.version?.toIdentifier()?.contains("$") == true) { |
| if ( |
| value.startsWith("'") && value.endsWith("'") && context.isEnabled(NOT_INTERPOLATED) |
| ) { |
| val message = |
| "It looks like you are trying to substitute a " + |
| "version variable, but using single quotes ('). For Groovy " + |
| "string interpolation you must use double quotes (\")." |
| val fix = |
| fix() |
| .name("Replace single quotes with double quotes") |
| .replace() |
| .text(value) |
| .with("\"" + value.substring(1, value.length - 1) + "\"") |
| .build() |
| report(context, statementCookie, NOT_INTERPOLATED, message, fix) |
| } |
| |
| dependency = resolveCoordinate(context, property, dependency) |
| isResolved = true |
| } else if (dependency?.version?.toIdentifier()?.let { !value.contains(it) } != false) { |
| isResolved = true |
| } |
| if (dependency != null) { |
| if ( |
| dependency.version?.run { require ?: strictly }?.toIdentifier()?.endsWith("+") == true |
| ) { |
| val message = |
| "Avoid using + in version numbers; can lead " + |
| "to unpredictable and unrepeatable builds (" + |
| dependencyString + |
| ")" |
| val fix = |
| fix() |
| .data( |
| KEY_COORDINATE, |
| dependency.toString(), |
| KEY_REVISION, |
| dependency.version?.toIdentifier(), |
| ) |
| report(context, valueCookie, PLUS, message, fix) |
| } |
| |
| val tomlLibraries = context.getTomlValue(VC_LIBRARIES) |
| if ( |
| tomlLibraries != null && |
| !context.file.name.startsWith("settings.gradle") && |
| !dependencyString.contains("+") && |
| (!dependencyString.contains("$") || isResolved) && |
| dependency.group?.isNotBlank() == true |
| ) { |
| val versionVar = getVersionVariable(value) |
| val result = |
| createMoveToTomlFix(context, tomlLibraries, dependency, valueCookie, versionVar) |
| if (result != null) { |
| val message = result.first ?: "Use version catalog instead" |
| val fix = result.second |
| report(context, valueCookie, SWITCH_TO_TOML, message, fix) |
| } |
| } |
| |
| // Check dependencies without the PSI read lock, because we |
| // may need to make network requests to retrieve version info. |
| context.driver.runLaterOutsideReadAction { |
| checkDependency(context, dependency, isResolved, valueCookie, statementCookie) |
| } |
| } |
| if ( |
| hasLifecycleAnnotationProcessor(dependencyString) && targetJava8Plus(context.project) |
| ) { |
| report( |
| context, |
| valueCookie, |
| LIFECYCLE_ANNOTATION_PROCESSOR_WITH_JAVA8, |
| "Use the Lifecycle Java 8 API provided by the " + |
| "`lifecycle-common` library instead of Lifecycle annotations " + |
| "for faster incremental build.", |
| null, |
| ) |
| } |
| checkAnnotationProcessorOnCompilePath(property, dependencyString, context, propertyCookie) |
| } |
| checkDeprecatedConfigurations(property, context, propertyCookie) |
| |
| // If we haven't managed to parse the dependency yet, try getting it from version catalog |
| var libTomlValue: LintTomlValue? = null |
| if (dependencyString == null) { |
| val dependencyFromVc = getDependencyFromVersionCatalog(value, context) |
| if (dependencyFromVc != null) { |
| dependencyString = dependencyFromVc.coordinates |
| libTomlValue = dependencyFromVc.tomlValue |
| } |
| } |
| |
| if (dependencyString != null) { |
| if (property == "kapt") { |
| checkKaptUsage(dependencyString, libTomlValue, context, statementCookie) |
| } |
| checkForBomUsageWithoutPlatform(property, dependencyString, value, context, valueCookie) |
| } |
| } |
| } else if (property == "packageNameSuffix") { |
| val message = "Deprecated: Replace 'packageNameSuffix' with 'applicationIdSuffix'" |
| val fix = |
| fix() |
| .name("Replace 'packageNameSuffix' with 'applicationIdSuffix'", true) |
| .replace() |
| .text("packageNameSuffix") |
| .with("applicationIdSuffix") |
| .autoFix() |
| .build() |
| report(context, propertyCookie, DEPRECATED, message, fix) |
| } else if (property == "applicationIdSuffix") { |
| val suffix = getStringLiteralValue(value, valueCookie) |
| if (suffix != null && !suffix.startsWith(".")) { |
| val message = "Application ID suffix should probably start with a \".\"" |
| report(context, statementCookie, PATH, message) |
| } |
| } else if ( |
| (property == "minSdkVersion" || property == "minSdk") && |
| parent == "dev" && |
| "21" == value && |
| // Don't flag this error from Gradle; users invoking lint from Gradle may |
| // still want dev mode for command line usage |
| LintClient.CLIENT_GRADLE != LintClient.clientName |
| ) { |
| report( |
| context, |
| statementCookie, |
| DEV_MODE_OBSOLETE, |
| "You no longer need a `dev` mode to enable multi-dexing during development, and this can break API version checks", |
| ) |
| } else if ( |
| parent == "dataBinding" && ((property == "enabled" || property == "isEnabled")) || |
| (parent == "buildFeatures" && property == "dataBinding") |
| ) { |
| // Note: "enabled" is used by build.gradle and "isEnabled" is used by build.gradle.kts |
| if (value == SdkConstants.VALUE_TRUE) { |
| if (mAppliedKotlinAndroidPlugin && !mAppliedKotlinKaptPlugin) { |
| val message = |
| "If you plan to use data binding in a Kotlin project, you should apply the kotlin-kapt plugin." |
| report(context, statementCookie, DATA_BINDING_WITHOUT_KAPT, message, null) |
| } |
| } |
| } else if ((parent == "" || parent == "java") && property == "sourceCompatibility") { |
| mDeclaredSourceCompatibility = true |
| } else if ((parent == "" || parent == "java") && property == "targetCompatibility") { |
| mDeclaredTargetCompatibility = true |
| } else if ( |
| property == "include" && parent == "abi" || property == "abiFilters" && parent == "ndk" |
| ) { |
| checkForChromeOSAbiSplits(context, valueCookie, value) |
| } else if (parent == "toolchain" && property == "languageVersion") { |
| mDeclaredSourceCompatibility = true |
| mDeclaredTargetCompatibility = true |
| } |
| } |
| |
| /** |
| * Given a dependency string, returns the name of the version variable, if any, assuming it's a |
| * single variable which represents the whole revision. For example, for `foo:bar:$version` and |
| * `foo:bar:${version}` and `foo:bar:${version}@jar` it would return "version". For `foo:bar:1.0` |
| * or `foo:bar:${version}-alpha` it would return null. |
| */ |
| private fun getVersionVariable(dependency: String): String? { |
| if (!dependency.contains("\$")) { |
| return null |
| } |
| var value = dependency.removeSurrounding("'").removeSurrounding("\"").substringAfterLast(':') |
| if (value.startsWith("\$")) { |
| if (value.startsWith("\${")) { |
| val end = value.indexOf('}') |
| if (end == -1 || end < value.length - 1 && value[end + 1] != '@') { |
| return null |
| } |
| value = value.removePrefix("\${").removeSuffix("}") |
| } else { |
| value = value.removePrefix("\$") |
| } |
| } else { |
| return null |
| } |
| if (value.all { it.isLetter() }) { |
| return value |
| } else { |
| return null |
| } |
| } |
| |
| /** |
| * For ChromeOS performance, we want to check if a developer has turned on abiSplits or abiFilters |
| * as they target specific ABIs. If the developer has included `x86_64` no warning will show. |
| * However, if it is missing, the warning will pop up. |
| * |
| * If the user has not included `abiSplits` or `abiFilters` this logic will not be called. |
| */ |
| private fun checkForChromeOSAbiSplits(context: GradleContext, valueCookie: Any, value: String) { |
| val abis = value.split(',') |
| var hasX8664 = false |
| for (i in abis.indices) { |
| if (abis[i].contains("\"x86_64\"") || abis[i].contains("\'x86_64\'")) { |
| hasX8664 = true |
| } |
| } |
| |
| val message: String? = |
| if (!hasX8664) { |
| "Missing x86_64 ABI support for ChromeOS" |
| } else { |
| null |
| } |
| |
| message?.let { m -> report(context, valueCookie, CHROMEOS_ABI_SUPPORT, m) } |
| } |
| |
| private enum class DeprecatedConfiguration( |
| private val deprecatedName: String, |
| private val replacementName: String, |
| ) { |
| COMPILE("compile", "implementation"), |
| PROVIDED("provided", "compileOnly"), |
| APK("apk", "runtimeOnly"), |
| ; |
| |
| private val deprecatedSuffix: String = deprecatedName.usLocaleCapitalize() |
| private val replacementSuffix: String = replacementName.usLocaleCapitalize() |
| |
| fun matches(configurationName: String): Boolean { |
| return configurationName == deprecatedName || configurationName.endsWith(deprecatedSuffix) |
| } |
| |
| fun replacement(configurationName: String): String { |
| return if (configurationName == deprecatedName) { |
| replacementName |
| } else { |
| configurationName.removeSuffix(deprecatedSuffix) + replacementSuffix |
| } |
| } |
| } |
| |
| private fun checkDeprecatedConfigurations( |
| configuration: String, |
| context: GradleContext, |
| propertyCookie: Any, |
| ) { |
| if (context.project.gradleModelVersion?.isAtLeastIncludingPreviews(3, 0, 0) == false) { |
| // All of these deprecations were made in AGP 3.0.0 |
| return |
| } |
| |
| for (deprecatedConfiguration in DeprecatedConfiguration.values()) { |
| if (deprecatedConfiguration.matches(configuration)) { |
| // Compile was replaced by API and Implementation, but only suggest API if it was used |
| if ( |
| deprecatedConfiguration == DeprecatedConfiguration.COMPILE && |
| suggestApiConfigurationUse(context.project, configuration) |
| ) { |
| val implementation: String |
| val api: String |
| if (configuration == "compile") { |
| implementation = "implementation" |
| api = "api" |
| } else { |
| val prefix = configuration.removeSuffix("Compile") |
| implementation = "${prefix}Implementation" |
| api = "${prefix}Api" |
| } |
| |
| val message = |
| "`$configuration` is deprecated; " + |
| "replace with either `$api` to maintain current behavior, " + |
| "or `$implementation` to improve build performance " + |
| "by not sharing this dependency transitively." |
| val apiFix = |
| fix() |
| .name("Replace '$configuration' with '$api'") |
| .family("Replace compile with api") |
| .replace() |
| .text(configuration) |
| .with(api) |
| .autoFix() |
| .build() |
| val implementationFix = |
| fix() |
| .name("Replace '$configuration' with '$implementation'") |
| .family("Replace compile with implementation") |
| .replace() |
| .text(configuration) |
| .with(implementation) |
| .autoFix() |
| .build() |
| |
| val fixes = |
| fix() |
| .alternatives() |
| .name("Replace '$configuration' with '$api' or '$implementation'") |
| .add(apiFix) |
| .add(implementationFix) |
| .build() |
| |
| report(context, propertyCookie, DEPRECATED_CONFIGURATION, message, fixes) |
| } else { |
| // Unambiguous replacement case |
| val replacement = deprecatedConfiguration.replacement(configuration) |
| val message = "`$configuration` is deprecated; replace with `$replacement`" |
| val fix = |
| fix() |
| .name("Replace '$configuration' with '$replacement'") |
| .family("Replace deprecated configurations") |
| .replace() |
| .text(configuration) |
| .with(replacement) |
| .autoFix() |
| .build() |
| report(context, propertyCookie, DEPRECATED_CONFIGURATION, message, fix) |
| } |
| } |
| } |
| } |
| |
| private fun checkAnnotationProcessorOnCompilePath( |
| configuration: String, |
| dependency: String, |
| context: GradleContext, |
| propertyCookie: Any, |
| ) { |
| for (compileConfiguration in CompileConfiguration.values()) { |
| if (compileConfiguration.matches(configuration) && isCommonAnnotationProcessor(dependency)) { |
| val replacement: String = compileConfiguration.replacement(configuration) |
| val fix = |
| fix() |
| .name("Replace $configuration with $replacement") |
| .family("Replace compile classpath with annotationProcessor") |
| .replace() |
| .text(configuration) |
| .with(replacement) |
| .autoFix() |
| .build() |
| val message = |
| "Add annotation processor to processor path using `$replacement`" + |
| " instead of `$configuration`" |
| report(context, propertyCookie, ANNOTATION_PROCESSOR_ON_COMPILE_PATH, message, fix) |
| } |
| } |
| } |
| |
| private fun checkMinSdkVersion(context: GradleContext, version: Int, valueCookie: Any) { |
| if (version in 1 until LOWEST_ACTIVE_API) { |
| val message = |
| "The value of minSdkVersion is too low. It can be incremented " + |
| "without noticeably reducing the number of supported devices." |
| |
| val label = "Update minSdkVersion to $LOWEST_ACTIVE_API" |
| val fix = |
| fix() |
| .name(label) |
| .replace() |
| .text(version.toString()) |
| .with(LOWEST_ACTIVE_API.toString()) |
| .build() |
| report(context, valueCookie, MIN_SDK_TOO_LOW, message, fix) |
| } |
| } |
| |
| private fun checkIntegerAsString( |
| context: GradleContext, |
| value: String, |
| cookie: Any, |
| valueCookie: Any, |
| ) { |
| // When done developing with a preview platform you might be tempted to switch from |
| // compileSdkVersion 'android-G' |
| // to |
| // compileSdkVersion '19' |
| // but that won't work; it needs to be |
| // compileSdkVersion 19 |
| val string = getStringLiteralValue(value, valueCookie) |
| if (isNumberString(string)) { |
| val message = "Use an integer rather than a string here (replace $value with just $string)" |
| val fix = fix().name("Replace with integer", true).replace().text(value).with(string).build() |
| report(context, cookie, STRING_INTEGER, message, fix) |
| } |
| } |
| |
| override fun checkMethodCall( |
| context: GradleContext, |
| statement: String, |
| parent: String?, |
| parentParent: String?, |
| namedArguments: Map<String, String>, |
| unnamedArguments: List<String>, |
| cookie: Any, |
| ) { |
| val plugin = namedArguments["plugin"] |
| if (statement == "apply" && parent == null) { |
| val isOldAppPlugin = OLD_APP_PLUGIN_ID == plugin |
| if (isOldAppPlugin || OLD_LIB_PLUGIN_ID == plugin) { |
| val replaceWith = if (isOldAppPlugin) APP_PLUGIN_ID else LIB_PLUGIN_ID |
| val message = "'$plugin' is deprecated; use '$replaceWith' instead" |
| val fix = |
| fix() |
| .sharedName("Replace plugin") |
| .replace() |
| .text(plugin) |
| .with(replaceWith) |
| .autoFix() |
| .build() |
| report(context, cookie, DEPRECATED, message, fix) |
| } |
| |
| if (plugin == "kotlin-android") { |
| mAppliedKotlinAndroidPlugin = true |
| } |
| if (plugin == "kotlin-kapt") { |
| mAppliedKotlinKaptPlugin = true |
| } |
| if (plugin == "com.google.devtools.ksp") { |
| mAppliedKspPlugin = true |
| } |
| if (JAVA_PLUGIN_IDS.contains(plugin)) { |
| mAppliedJavaPlugin = true |
| mJavaPluginInfo = JavaPluginInfo(cookie) |
| } |
| } |
| if (parent == "repositories" && parentParent == "buildscript") { |
| if (statement == "google") { |
| mDeclaredGoogleMavenRepository = true |
| } |
| mDeclaredBuildscriptRepository = true |
| } |
| if (statement == "jcenter" && parent == "repositories") { |
| val message = |
| "JCenter Maven repository is no longer receiving updates: newer library versions may be available elsewhere" |
| val replaceFix = |
| fix() |
| .name("Replace with mavenCentral") |
| .replace() |
| .text("jcenter") |
| .with("mavenCentral") |
| .build() |
| val deleteFix = |
| fix().name("Delete this repository declaration").replace().all().with("").build() |
| report( |
| context, |
| cookie, |
| JCENTER_REPOSITORY_OBSOLETE, |
| message, |
| fix().alternatives(replaceFix, deleteFix), |
| ) |
| } |
| } |
| |
| // Important: This is called without the PSI read lock, since it may make network requests. |
| // Any interaction with PSI or issue reporting should be wrapped in a read action. |
| private fun checkDependency( |
| context: Context, |
| dependency: Dependency, |
| isResolved: Boolean, |
| cookie: Any, |
| statementCookie: Any, |
| ) { |
| val version = dependency.version?.lowerBound ?: return |
| val groupId = dependency.group ?: return |
| val artifactId = dependency.name |
| val richVersionIdentifier = dependency.version?.toIdentifier() ?: return |
| var safeReplacement: Version? = null |
| var newerVersion: Version? = null |
| |
| val sdkIndex = getGooglePlaySdkIndex(context.client) |
| val versionFilter = getUpgradeVersionFilter(context, groupId, artifactId, version) |
| val sdkIndexFilter = getGooglePlaySdkIndexFilter(context, groupId, artifactId, sdkIndex) |
| fun Predicate<Version>?.and(other: Predicate<Version>?): Predicate<Version>? = |
| when { |
| this != null && other != null -> this.and(other) |
| else -> this ?: other |
| } |
| val filter = versionFilter.and(sdkIndexFilter) |
| |
| when (groupId) { |
| GMS_GROUP_ID, |
| FIREBASE_GROUP_ID, |
| GOOGLE_SUPPORT_GROUP_ID, |
| ANDROID_WEAR_GROUP_ID -> { |
| // Play services |
| |
| checkPlayServices(context, dependency, version, cookie, statementCookie) |
| } |
| "androidx.credentials" -> { |
| if (artifactId == "credentials") { |
| checkCredentialDependency(context, cookie, statementCookie) |
| } |
| } |
| "com.android.base", |
| "com.android.application", |
| "com.android.library", |
| "com.android.test", |
| "com.android.instant-app", |
| "com.android.feature", |
| "com.android.dynamic-feature", |
| "com.android.settings", |
| "com.android.tools.build" -> { |
| if ("gradle" == artifactId || "$groupId.gradle.plugin" == artifactId) { |
| if (checkGradlePluginDependency(context, dependency, statementCookie)) { |
| return |
| } |
| |
| // If it's available in maven.google.com, fetch latest available version |
| newerVersion = |
| newerVersion maxAgpOrNull getGoogleMavenRepoVersion(context, dependency, filter) |
| |
| // Compare with what's in the Gradle cache, except when lint is invoked from |
| // Gradle (because checking the Gradle cache is incompatible with Gradle task |
| // cacheability). |
| if (!LintClient.isGradle) { |
| newerVersion = newerVersion maxAgpOrNull findCachedNewerVersion(dependency, filter) |
| } |
| |
| // Compare with IDE's repository cache, if available. |
| newerVersion = |
| newerVersion maxAgpOrNull context.client.getHighestKnownVersion(dependency, filter) |
| |
| // Don't just offer the latest available version, but if that is more than |
| // a micro-level different, and there is a newer micro version of the |
| // version that the user is currently using, offer that one as well as it |
| // may be easier to upgrade to. |
| if ( |
| newerVersion != null && |
| !version.isPreview && |
| newerVersion != version && |
| (version.major != newerVersion.major || version.minor != newerVersion.minor) |
| ) { |
| safeReplacement = |
| getGoogleMavenRepoVersion(context, dependency) { filterVersion -> |
| filterVersion.major != null && |
| filterVersion.major == version.major && |
| filterVersion.minor != null && |
| filterVersion.minor == version.minor && |
| filterVersion.micro?.let { m -> version.micro?.let { m > it } } == true && |
| !filterVersion.isPreview && |
| filterVersion < newerVersion!! && |
| !filterVersion.isSnapshot |
| } |
| } |
| if (newerVersion != null && newerVersion.isAgpNewerThan(dependency)) { |
| agpVersionCheckInfo = |
| AgpVersionCheckInfo( |
| newerVersion, |
| newerVersion.major == version.major && newerVersion.minor == version.minor, |
| safeReplacement, |
| dependency, |
| isResolved, |
| cookie, |
| ) |
| maybeReportAgpVersionIssue(context) |
| } |
| return |
| } |
| } |
| "com.google.guava" -> { |
| // TODO: 24.0-android |
| if ("guava" == artifactId) { |
| newerVersion = getNewerVersion(version, 21, 0) |
| } |
| } |
| "com.google.code.gson" -> { |
| if ("gson" == artifactId) { |
| newerVersion = getNewerVersion(version, 2, 8, 2) |
| } |
| } |
| "org.apache.httpcomponents" -> { |
| if ("httpclient" == artifactId) { |
| newerVersion = getNewerVersion(version, 4, 5, 5) |
| } |
| } |
| "com.squareup.okhttp3" -> { |
| if ("okhttp" == artifactId) { |
| newerVersion = getNewerVersion(version, 3, 10, 0) |
| } |
| } |
| "com.github.bumptech.glide" -> { |
| if ("glide" == artifactId) { |
| newerVersion = getNewerVersion(version, 3, 7, 0) |
| } |
| } |
| "io.fabric.tools" -> { |
| if ("gradle" == artifactId) { |
| if (Version.parse("1.21.6").isNewerThan(dependency)) { |
| val fix = getUpdateDependencyFix(richVersionIdentifier, "1.22.1") |
| report( |
| context, |
| statementCookie, |
| DEPENDENCY, |
| "Use Fabric Gradle plugin version 1.21.6 or later to " + |
| "improve Instant Run performance (was $richVersionIdentifier)", |
| fix, |
| ) |
| } else { |
| // From |
| // https://s3.amazonaws.com/fabric-artifacts/public/io/fabric/tools/gradle/maven-metadata.xml |
| newerVersion = getNewerVersion(version, 1, 25, 1) |
| } |
| } |
| } |
| "com.bugsnag" -> { |
| if ("bugsnag-android-gradle-plugin" == artifactId) { |
| if (version < Version.parse("2.1.2")) { |
| val fix = getUpdateDependencyFix(richVersionIdentifier, "2.4.1") |
| report( |
| context, |
| statementCookie, |
| DEPENDENCY, |
| "Use BugSnag Gradle plugin version 2.1.2 or later to " + |
| "improve Instant Run performance (was $richVersionIdentifier)", |
| fix, |
| ) |
| } else { |
| // From http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22com.bugsnag%22%20AND |
| // %20a%3A%22bugsnag-android-gradle-plugin%22 |
| newerVersion = getNewerVersion(version, 3, 2, 5) |
| } |
| } |
| } |
| |
| // https://issuetracker.google.com/120098460 |
| "org.robolectric" -> { |
| if ("robolectric" == artifactId && currentPlatform() == PLATFORM_WINDOWS) { |
| if (version < Version.parse("4.2.1")) { |
| val fix = getUpdateDependencyFix(richVersionIdentifier, "4.2.1") |
| report( |
| context, |
| cookie, |
| DEPENDENCY, |
| "Use robolectric version 4.2.1 or later to " + |
| "fix issues with parsing of Windows paths", |
| fix, |
| ) |
| } |
| } |
| } |
| |
| // TODO: This is a hotfix to suppress Kotlin version warnings in Compose projects |
| // (b/194313332) |
| // and it should be removed eventually. |
| "org.jetbrains.kotlin" -> { |
| if (artifactId == "kotlin-gradle-plugin") { |
| return |
| } |
| } |
| } |
| |
| checkForKtxExtension(context, groupId, artifactId, version, cookie) |
| |
| val blockedDependencies = blockedDependencies[context.project] |
| if (blockedDependencies != null) { |
| val path = blockedDependencies.checkDependency(groupId, artifactId, true) |
| if (path != null) { |
| val message = getBlockedDependencyMessage(path) |
| val fix = fix().name("Delete dependency").replace().all().build() |
| // Provisional: have to check consuming app's targetSdkVersion |
| report(context, statementCookie, DUPLICATE_CLASSES, message, fix, partial = true) |
| } |
| } |
| |
| var hasSdkIndexIssues = false |
| if (sdkIndex.isReady()) { |
| val versionString = version.toString() |
| val buildFile = context.file |
| val isBlocking = sdkIndex.hasLibraryBlockingIssues(groupId, artifactId, versionString) |
| val severity = |
| if (isBlocking) { |
| Severity.ERROR |
| } else { |
| Severity.WARNING |
| } |
| // Report all SDK Index issues without grouping them following this order (b/316038712): |
| // - Policy |
| // - Critical (if blocking) |
| // - Outdated |
| var fix: LintFix? = null |
| if (sdkIndex.isLibraryNonCompliant(groupId, artifactId, versionString, buildFile)) { |
| fix = sdkIndex.generateSdkLinkLintFix(groupId, artifactId, versionString, buildFile) |
| val messages = |
| if (isBlocking) { |
| sdkIndex.generateBlockingPolicyMessages(groupId, artifactId, versionString) |
| } else { |
| sdkIndex.generatePolicyMessages(groupId, artifactId, versionString) |
| } |
| for (message in messages) { |
| hasSdkIndexIssues = |
| report( |
| context, |
| cookie, |
| PLAY_SDK_INDEX_NON_COMPLIANT, |
| message, |
| fix, |
| overrideSeverity = severity, |
| ) || hasSdkIndexIssues |
| } |
| } |
| if ( |
| isBlocking && |
| sdkIndex.hasLibraryCriticalIssues(groupId, artifactId, versionString, buildFile) |
| ) { |
| // Messages from developer that are not-blocking are not shown in lint |
| if (fix == null) { |
| fix = sdkIndex.generateSdkLinkLintFix(groupId, artifactId, versionString, buildFile) |
| } |
| val message = sdkIndex.generateBlockingCriticalMessage(groupId, artifactId, versionString) |
| hasSdkIndexIssues = |
| report(context, cookie, RISKY_LIBRARY, message, fix, overrideSeverity = severity) || |
| hasSdkIndexIssues |
| } |
| if (sdkIndex.isLibraryOutdated(groupId, artifactId, versionString, buildFile)) { |
| if (fix == null) { |
| fix = sdkIndex.generateSdkLinkLintFix(groupId, artifactId, versionString, buildFile) |
| } |
| val message = |
| if (isBlocking) { |
| sdkIndex.generateBlockingOutdatedMessage(groupId, artifactId, versionString) |
| } else { |
| sdkIndex.generateOutdatedMessage(groupId, artifactId, versionString) |
| } |
| hasSdkIndexIssues = |
| report(context, cookie, DEPRECATED_LIBRARY, message, fix, overrideSeverity = severity) || |
| hasSdkIndexIssues |
| } |
| } |
| |
| // Network check for really up to date libraries? Only done in batch mode. |
| var issue = DEPENDENCY |
| if ( |
| !Scope.checkSingleFile(context.scope) && |
| context.isEnabled(REMOTE_VERSION) && |
| // Common but served from maven.google.com so no point to |
| // ping other maven repositories about these |
| !getGoogleMavenRepository(context.client).hasGroupId(groupId) |
| ) { |
| val latest = |
| getLatestVersionFromRemoteRepo( |
| context.client, |
| dependency, |
| filter, |
| dependency.version?.lowerBound?.isPreview ?: true, |
| ) |
| if (latest != null && version < latest) { |
| newerVersion = latest |
| issue = REMOTE_VERSION |
| } |
| |
| val group = dependency.group |
| if (group != null && dependency.isGradlePlugin()) { |
| val pluginVersion = |
| getLatestVersionFromGradlePluginPortal( |
| context.client, |
| group, |
| filter, |
| dependency.version?.lowerBound?.isPreview ?: true, |
| ) |
| if (pluginVersion != null && version < pluginVersion) { |
| newerVersion = pluginVersion |
| issue = REMOTE_VERSION |
| } |
| } |
| } |
| |
| // Compare with what's in the Gradle cache. |
| newerVersion = newerVersion maxOrNull findCachedNewerVersion(dependency, filter) |
| |
| // Compare with IDE's repository cache, if available. |
| newerVersion = newerVersion maxOrNull context.client.getHighestKnownVersion(dependency, filter) |
| |
| // If it's available in maven.google.com, fetch latest available version. |
| newerVersion = newerVersion maxOrNull getGoogleMavenRepoVersion(context, dependency, filter) |
| |
| if ( |
| newerVersion != null && |
| version > Version.prefixInfimum("0") && |
| newerVersion.isNewerThan(dependency) |
| ) { |
| val versionString = newerVersion.toString() |
| var isCustomMessage = true |
| var message = |
| if ( |
| dependency.group == "androidx.slidingpanelayout" && dependency.name == "slidingpanelayout" |
| ) { |
| "Upgrade `androidx.slidingpanelayout` for keyboard and mouse support" |
| } else if ( |
| dependency.group == "androidx.compose.foundation" && dependency.name == "foundation" |
| ) { |
| "Upgrade `androidx.compose.foundation` for keyboard and mouse support" |
| } else { |
| getNewerVersionAvailableMessage(dependency, versionString, null).also { |
| isCustomMessage = false |
| } |
| } |
| |
| // Add details for play-services-maps. |
| if ( |
| dependency.group == "com.google.android.gms" && |
| dependency.name == "play-services-maps" && |
| Version.parse("18.2.0").let { version < it && newerVersion >= it } |
| ) { |
| message += |
| ". Upgrading to at least 18.2.0 is highly recommended to take advantage of the new renderer, " + |
| "which supports customization options like map styling, 3D tiles, " + |
| "and is more reliable, with better support going forward." |
| isCustomMessage = true |
| } |
| |
| // Only show update message if there is a custom message or SDK Index issues not present |
| // (b/301316600) |
| if (isCustomMessage || !hasSdkIndexIssues) { |
| val fix = |
| if (!isResolved) getUpdateDependencyFix(richVersionIdentifier, versionString) else null |
| report(context, cookie, issue, message, fix) |
| } |
| } |
| } |
| |
| private fun checkDuplication( |
| context: Context, |
| dependencies: Map<LintTomlValue, Dependency>, |
| extractor: (Dependency) -> String, // we can compare libraries by group:name and plugins by group |
| ) { |
| dependencies.entries |
| .toList() |
| .groupBy { entry -> extractor(entry.value) } |
| .filter { entry -> entry.value.size > 1 } |
| .forEach { entry -> |
| entry.value.forEach { tuple -> |
| val tomlValue = tuple.key |
| report( |
| context, |
| tomlValue, |
| MULTIPLE_VERSIONS_DEPENDENCY, |
| "There are multiple dependencies ${entry.key} but with different version", |
| ) |
| } |
| } |
| } |
| |
| private fun getGooglePlaySdkIndexFilter( |
| context: Context, |
| groupId: String, |
| artifactId: String, |
| sdkIndex: GooglePlaySdkIndex?, |
| ): Predicate<Version>? { |
| return sdkIndex?.let { |
| // Filter out versions with SDK Index errors or warnings (b/301295995) |
| Predicate { v -> |
| it.isReady() && !it.hasLibraryErrorOrWarning(groupId, artifactId, v.toString()) |
| } |
| } |
| } |
| |
| /** |
| * Returns a predicate that encapsulates version constraints for the given library, or null if |
| * there are no constraints. |
| */ |
| private fun getUpgradeVersionFilter( |
| context: Context, |
| groupId: String, |
| artifactId: String, |
| version: Version, |
| ): Predicate<Version>? { |
| if ( |
| (groupId == "com.android.tools.build" || ALL_PLUGIN_IDS.contains(groupId)) && |
| LintClient.isStudio |
| ) { |
| val clientRevision = context.client.getClientRevision() ?: return null |
| val ideVersion = Version.parse(clientRevision) |
| // TODO(b/145606749): this assumes that the IDE version and the AGP version are directly |
| // comparable |
| return Predicate { v -> |
| // Any higher IDE version that matches major and minor |
| // (e.g. from 3.3.0 offer 3.3.2 but not 3.4.0) |
| ((v.major == ideVersion.major && v.minor == ideVersion.minor) || |
| // Also allow matching latest current existing major/minor version |
| (v.major == version.major && v.minor == version.minor)) |
| } |
| } |
| |
| // Some special cases for specific artifacts that were versioned |
| // incorrectly (using a string suffix to delineate separate branches |
| // whereas Gradle will just use an alphabetical sort on these). See |
| // 171369798 for an example. |
| |
| // These cases must be considered before the generic logic related to not |
| // upgrading to other versions outside a preview series, because these |
| // pseudo-version strings look like preview versions even though they're not. |
| if (groupId == "com.google.guava") { |
| val suffix = version.toString() |
| val jre = Predicate<Version> { v -> v.toString().endsWith("-jre") } |
| val android = Predicate<Version> { v -> v.toString().endsWith("-android") } |
| val neither = Predicate<Version> { v -> !v.toString().endsWith("-jre") } |
| return when { |
| suffix.endsWith("-jre") -> jre |
| suffix.endsWith("-android") -> android |
| else -> neither |
| } |
| } else if (artifactId == "kotlinx-coroutines-core") { |
| val suffix = version.toString() |
| return when { |
| suffix.contains("-native-mt-2") -> |
| Predicate<Version> { v -> v.toString().contains("-native-mt-2") } |
| suffix.contains("-native-mt") -> |
| Predicate<Version> { v -> |
| v.toString().run { contains("native-mt") && !contains("native-mt-2") } |
| } |
| else -> Predicate<Version> { v -> !v.toString().contains("-native-mt") } |
| } |
| } |
| |
| if (version.major != null) { |
| // version.major not being null is something of a pun, but sensible anyway: |
| // if the whole version is non-numeric, the concept of "the current preview |
| // series" doesn't really exist. It also guards against the fact that the |
| // "revision" that we've parsed into a Version isn't known to be a version, |
| // and in fact has more of the character of a RichVersion. |
| val infimum = version.previewInfimum |
| val supremum = version.previewSupremum |
| if (infimum != null && supremum != null) { |
| return Predicate { v -> (if (v.isPreview) (infimum < v && v < supremum) else true) } |
| } |
| } |
| |
| return null |
| } |
| |
| /** Home in the Gradle cache for artifact caches. */ |
| @Suppress("MemberVisibilityCanBePrivate") // overridden in the IDE |
| protected fun getArtifactCacheHome(): File { |
| return artifactCacheHome |
| ?: run { |
| val home = |
| File( |
| gradleUserHome, |
| "caches" + File.separator + "modules-2" + File.separator + "files-2.1", |
| ) |
| artifactCacheHome = home |
| home |
| } |
| } |
| |
| private fun findCachedNewerVersion( |
| dependency: Dependency, |
| filter: Predicate<Version>?, |
| ): Version? { |
| val group = dependency.group ?: return null |
| val versionDir = |
| getArtifactCacheHome().toPath().resolve(group + File.separator + dependency.name) |
| val f = |
| when { |
| dependency.group?.startsWith("commons-") == true && |
| dependency.name?.startsWith("commons-") == true && |
| dependency.name == dependency.group -> { |
| // For a (long) while, users could get this spurious recommendation of an "upgrade" to |
| // commons-io, commons-codec etc to this very old version (with a very high version |
| // number). This recommendation is no longer given as of mid-2023, except if a |
| // user has previously installed it and the version is lurking in their Gradle cache. |
| // We need filter out all versions that has 8 digits in major version like: 20030203 |
| val commonsFilter: Predicate<Version> = Predicate { v -> !v.isOldApacheCommonsVersion() } |
| filter?.and(commonsFilter) ?: commonsFilter |
| } |
| else -> filter |
| } |
| val noSnapshotFilter: (Version) -> Boolean = { candidate -> |
| !candidate.isSnapshot && (f == null || f.test(candidate)) |
| } |
| return if (CancellableFileIo.exists(versionDir)) { |
| val name = dependency.name |
| val richVersion = dependency.version |
| val allowPreview = |
| when { |
| richVersion == null -> true |
| group == "com.google.guava" || name == "kotlinx-coroutines-core" -> true |
| else -> MavenRepositories.isPreview(Component(group, name, richVersion.lowerBound)) |
| } |
| MavenRepositories.getHighestVersion(versionDir, noSnapshotFilter, allowPreview) |
| } else null |
| } |
| |
| private fun Version.isOldApacheCommonsVersion() = major?.toString()?.length == 8 |
| |
| // Important: This is called without the PSI read lock, since it may make network requests. |
| // Any interaction with PSI or issue reporting should be wrapped in a read action. |
| private fun checkGradlePluginDependency( |
| context: Context, |
| dependency: Dependency, |
| cookie: Any, |
| ): Boolean { |
| val minimum = Version.parse(GRADLE_PLUGIN_MINIMUM_VERSION) |
| val dependencyVersion = dependency.version ?: return false |
| if (dependencyVersion.lowerBound >= minimum) return false |
| if (!dependencyVersion.contains(minimum)) { |
| val query = Dependency("com.android.tools.build", "gradle", RichVersion.require(minimum)) |
| val recommended = |
| Version.parse(GRADLE_PLUGIN_RECOMMENDED_VERSION).let { recommended -> |
| getGoogleMavenRepoVersion(context, query, null)?.takeIf { it > recommended } |
| ?: recommended |
| } |
| val message = |
| "You must use a newer version of the Android Gradle plugin. The " + |
| "minimum supported version is " + |
| GRADLE_PLUGIN_MINIMUM_VERSION + |
| " and the recommended version is " + |
| recommended |
| report(context, cookie, GRADLE_PLUGIN_COMPATIBILITY, message) |
| return true |
| } |
| return false |
| } |
| |
| private fun checkCredentialDependency(context: Context, valueCookie: Any, statementCookie: Any) { |
| if (context.project.dependsOn("androidx.credentials:credentials-play-services-auth") == true) { |
| return |
| } |
| |
| val cookie = if (context.file.path.endsWith(DOT_TOML)) statementCookie else valueCookie |
| report( |
| context, |
| cookie, |
| CREDENTIAL_DEP, |
| "In Android 13 or lower, `credentials-play-services-auth` is required when using `androidx.credentials:credentials`", |
| constraint = minSdkLessThan(34), |
| ) |
| } |
| |
| private fun checkPlayServices( |
| context: Context, |
| dependency: Dependency, |
| version: Version, |
| cookie: Any, |
| statementCookie: Any, |
| ) { |
| val groupId = dependency.group ?: return |
| val artifactId = dependency.name |
| val richVersion = dependency.version ?: return |
| val richVersionIdentifier = richVersion.toIdentifier() ?: return |
| |
| // 5.2.08 is not supported; special case and warn about this |
| if (Version.parse("5.2.08") == version && context.isEnabled(COMPATIBILITY)) { |
| // This specific version is actually a preview version which should |
| // not be used (https://code.google.com/p/android/issues/detail?id=75292) |
| val maxVersion = |
| Version.parse("10.2.1").let { v -> |
| getGoogleMavenRepoVersion(context, dependency, null)?.takeIf { it > v } ?: v |
| } |
| val fix = getUpdateDependencyFix(richVersionIdentifier, maxVersion.toString()) |
| val message = |
| "Version `5.2.08` should not be used; the app " + |
| "can not be published with this version. Use version `$maxVersion` " + |
| "instead." |
| reportFatalCompatibilityIssue(context, cookie, message, fix) |
| } |
| |
| if ( |
| context.isEnabled(BUNDLED_GMS) && |
| PLAY_SERVICES_V650.group == dependency.group && |
| PLAY_SERVICES_V650.name == dependency.name && |
| (richVersion.lowerBound >= PLAY_SERVICES_V650.version || |
| richVersion.contains(PLAY_SERVICES_V650.version)) |
| ) { |
| // Play services 6.5.0 is the first version to allow un-bundling, so if the user is |
| // at or above 6.5.0, recommend un-bundling |
| val message = "Avoid using bundled version of Google Play services SDK." |
| report(context, cookie, BUNDLED_GMS, message) |
| } |
| |
| if (GMS_GROUP_ID == groupId && "play-services-appindexing" == artifactId) { |
| val message = |
| "Deprecated: Replace '" + |
| GMS_GROUP_ID + |
| ":play-services-appindexing:" + |
| richVersionIdentifier + |
| "' with 'com.google.firebase:firebase-appindexing:10.0.0' or above. " + |
| "More info: http://firebase.google.com/docs/app-indexing/android/migrate" |
| val fix = |
| fix() |
| .name("Replace with Firebase") |
| .replace() |
| .text("$GMS_GROUP_ID:play-services-appindexing:$richVersionIdentifier") |
| .with("com.google.firebase:firebase-appindexing:10.2.1") |
| .build() |
| report(context, cookie, DEPRECATED, message, fix) |
| } |
| |
| if (GMS_GROUP_ID == groupId || FIREBASE_GROUP_ID == groupId) { |
| if (!mCheckedGms) { |
| mCheckedGms = true |
| // Incremental analysis only? If so, tie the check to |
| // a specific GMS play dependency if only, such that it's highlighted |
| // in the editor |
| if (!context.scope.contains(Scope.ALL_RESOURCE_FILES) && context.isGlobalAnalysis()) { |
| // Incremental editing: try flagging them in this file! |
| checkConsistentPlayServices(context, cookie) |
| } |
| } |
| } else { |
| if (!mCheckedWearableLibs) { |
| mCheckedWearableLibs = true |
| // Incremental analysis only? If so, tie the check to |
| // a specific GMS play dependency if only, such that it's highlighted |
| // in the editor |
| if (!context.scope.contains(Scope.ALL_RESOURCE_FILES) && context.isGlobalAnalysis()) { |
| // Incremental editing: try flagging them in this file! |
| checkConsistentWearableLibraries(context, cookie, statementCookie) |
| } |
| } |
| } |
| } |
| |
| private fun checkConsistentPlayServices(context: Context, cookie: Any?) { |
| checkConsistentLibraries(context, cookie, GMS_GROUP_ID, FIREBASE_GROUP_ID) |
| } |
| |
| private fun checkConsistentWearableLibraries( |
| context: Context, |
| cookie: Any?, |
| statementCookie: Any?, |
| ) { |
| // Make sure we have both |
| // compile 'com.google.android.support:wearable:2.0.0-alpha3' |
| // provided 'com.google.android.wearable:wearable:2.0.0-alpha3' |
| val project = context.mainProject |
| if (!project.isGradleProject) { |
| return |
| } |
| val supportVersions = HashSet<String>() |
| val wearableVersions = HashSet<String>() |
| for (library in getAllLibraries(project).filterIsInstance<LintModelExternalLibrary>()) { |
| val coordinates = library.resolvedCoordinates |
| if ( |
| WEARABLE_ARTIFACT_ID == coordinates.artifactId && |
| GOOGLE_SUPPORT_GROUP_ID == coordinates.groupId |
| ) { |
| supportVersions.add(coordinates.version) |
| } |
| |
| // Claims to be non-null but may not be after a failed gradle sync |
| if ( |
| WEARABLE_ARTIFACT_ID == coordinates.artifactId && |
| ANDROID_WEAR_GROUP_ID == coordinates.groupId |
| ) { |
| if (!library.provided) { |
| var message = "This dependency should be marked as `compileOnly`, not `compile`" |
| if (statementCookie != null) { |
| reportFatalCompatibilityIssue(context, statementCookie, message) |
| } else { |
| val location = getDependencyLocation(context, coordinates) |
| if (location.start == null) { |
| message = |
| "The $ANDROID_WEAR_GROUP_ID:$WEARABLE_ARTIFACT_ID dependency should be marked as `compileOnly`, not `compile`" |
| } |
| reportFatalCompatibilityIssue(context, location, message) |
| } |
| } |
| wearableVersions.add(coordinates.version) |
| } |
| } |
| |
| if (supportVersions.isNotEmpty()) { |
| if (wearableVersions.isEmpty()) { |
| val list = ArrayList(supportVersions) |
| val first = Collections.min(list) |
| val message = |
| "Project depends on $GOOGLE_SUPPORT_GROUP_ID:$WEARABLE_ARTIFACT_ID:$first, " + |
| "so it must also depend (as a provided dependency) on " + |
| "$ANDROID_WEAR_GROUP_ID:$WEARABLE_ARTIFACT_ID:$first" |
| if (cookie != null) { |
| reportFatalCompatibilityIssue(context, cookie, message) |
| } else { |
| val location = |
| getDependencyLocation(context, GOOGLE_SUPPORT_GROUP_ID, WEARABLE_ARTIFACT_ID, first) |
| reportFatalCompatibilityIssue(context, location, message) |
| } |
| } else { |
| // Check that they have the same versions |
| if (supportVersions != wearableVersions) { |
| val sortedSupportVersions = ArrayList(supportVersions) |
| sortedSupportVersions.sort() |
| val supportedWearableVersions = ArrayList(wearableVersions) |
| supportedWearableVersions.sort() |
| val message = |
| String.format( |
| "The wearable libraries for %1\$s and %2\$s " + |
| "must use **exactly** the same versions; found %3\$s " + |
| "and %4\$s", |
| GOOGLE_SUPPORT_GROUP_ID, |
| ANDROID_WEAR_GROUP_ID, |
| if (sortedSupportVersions.size == 1) sortedSupportVersions[0] |
| else sortedSupportVersions.toString(), |
| if (supportedWearableVersions.size == 1) supportedWearableVersions[0] |
| else supportedWearableVersions.toString(), |
| ) |
| if (cookie != null) { |
| reportFatalCompatibilityIssue(context, cookie, message) |
| } else { |
| val location = |
| getDependencyLocation( |
| context, |
| GOOGLE_SUPPORT_GROUP_ID, |
| WEARABLE_ARTIFACT_ID, |
| sortedSupportVersions[0], |
| ANDROID_WEAR_GROUP_ID, |
| WEARABLE_ARTIFACT_ID, |
| supportedWearableVersions[0], |
| ) |
| reportFatalCompatibilityIssue(context, location, message) |
| } |
| } |
| } |
| } |
| } |
| |
| private fun getAllLibraries(project: Project): List<LintModelLibrary> { |
| return project.buildVariant?.mainArtifact?.dependencies?.getAll() ?: emptyList() |
| } |
| |
| private fun checkConsistentLibraries( |
| context: Context, |
| cookie: Any?, |
| groupId: String, |
| groupId2: String?, |
| ) { |
| // Make sure we're using a consistent version across all play services libraries |
| // (b/22709708) |
| |
| val project = context.mainProject |
| val versionToCoordinate = ArrayListMultimap.create<String, LintModelMavenName>() |
| val allLibraries = getAllLibraries(project).filterIsInstance<LintModelExternalLibrary>() |
| for (library in allLibraries) { |
| val coordinates = library.resolvedCoordinates |
| if ( |
| (coordinates.groupId == groupId || coordinates.groupId == groupId2) && |
| // Historically the multidex library ended up in the support package but |
| // decided to do its own numbering (and isn't tied to the rest in terms |
| // of implementation dependencies) |
| !coordinates.artifactId.startsWith("multidex") && |
| // Renderscript has stated in b/37630182 that they are built and |
| // distributed separate from the rest and do not have any version |
| // dependencies |
| !coordinates.artifactId.startsWith("renderscript") && |
| // Similarly firebase job dispatcher doesn't follow normal firebase version |
| // numbering |
| !coordinates.artifactId.startsWith("firebase-jobdispatcher") && |
| // The Android annotations library is decoupled from the rest and doesn't |
| // need to be matched to the other exact support library versions |
| coordinates.artifactId != "support-annotations" |
| ) { |
| versionToCoordinate.put(coordinates.version, coordinates) |
| } |
| } |
| |
| val versions = versionToCoordinate.keySet() |
| if (versions.size > 1) { |
| val sortedVersions = ArrayList(versions) |
| sortedVersions.sortWith(Collections.reverseOrder()) |
| val c1 = findFirst(versionToCoordinate.get(sortedVersions[0])) |
| val c2 = findFirst(versionToCoordinate.get(sortedVersions[1])) |
| |
| // For GMS, the synced version requirement ends at version 14 |
| if (groupId == GMS_GROUP_ID || groupId == FIREBASE_GROUP_ID) { |
| // c2 is the smallest of all the versions; if it is at least 14, |
| // they all are |
| val version = Version.parse(c2.version) |
| if (version.major?.let { it >= 14 } != false) { |
| return |
| } |
| } |
| |
| // Not using toString because in the IDE, these are model proxies which display garbage output |
| val example1 = c1.groupId + ":" + c1.artifactId + ":" + c1.version |
| val example2 = c2.groupId + ":" + c2.artifactId + ":" + c2.version |
| val groupDesc = if (GMS_GROUP_ID == groupId) "gms/firebase" else groupId |
| val message = |
| "All " + |
| groupDesc + |
| " libraries must use the exact same " + |
| "version specification (mixing versions can lead to runtime crashes). " + |
| "Found versions " + |
| Joiner.on(", ").join(sortedVersions) + |
| ". " + |
| "Examples include `" + |
| example1 + |
| "` and `" + |
| example2 + |
| "`" |
| |
| if (cookie != null) { |
| reportNonFatalCompatibilityIssue(context, cookie, message) |
| } else { |
| val location = getDependencyLocation(context, c1, c2) |
| reportNonFatalCompatibilityIssue(context, location, message) |
| } |
| } |
| } |
| |
| override fun beforeCheckRootProject(context: Context) { |
| val project = context.project |
| blockedDependencies[project] = BlockedDependencies(project) |
| } |
| |
| override fun afterCheckRootProject(context: Context) { |
| val project = context.project |
| // Check for disallowed dependencies |
| checkBlockedDependencies(context, project) |
| if (!LintClient.isGradle) { |
| // In the IDE, in tests, etc, we can run the detectors repeatedly, |
| // and we don't want the reserved names to accumulate. In Gradle however |
| // we do want to make sure that we assign unique names, even across |
| // modules. |
| reservedQuickfixNames = null |
| } |
| } |
| |
| private fun checkLibraryConsistency(context: Context) { |
| checkConsistentPlayServices(context, null) |
| checkConsistentWearableLibraries(context, null, null) |
| } |
| |
| override fun visitTomlDocument(context: TomlContext, document: LintTomlDocument) { |
| // Look for version catalogs |
| val libraries = document.getValue(VC_LIBRARIES) as? LintTomlMapValue |
| if (libraries != null) { |
| val versions = document.getValue(VC_VERSIONS) as? LintTomlMapValue |
| val dependencyToElement = mutableMapOf<LintTomlValue, Dependency>() |
| for ((_, library) in libraries.getMappedValues()) { |
| val (coordinate, versionNode) = getLibraryFromTomlEntry(versions, library) ?: continue |
| val dependency = Dependency.parse(coordinate) |
| dependencyToElement[library] = dependency |
| // Check dependencies without the PSI read lock, because we |
| // may need to make network requests to retrieve version info. |
| context.driver.runLaterOutsideReadAction { |
| checkDependency(context, dependency, false, versionNode, library) |
| } |
| } |
| checkDuplication(context, dependencyToElement) { dep: Dependency -> |
| dep.group + ":" + dep.name |
| } |
| } |
| |
| val plugins = document.getValue(VC_PLUGINS) as? LintTomlMapValue |
| if (plugins != null) { |
| val versions = document.getValue(VC_VERSIONS) as? LintTomlMapValue |
| val dependencyToElement = mutableMapOf<LintTomlValue, Dependency>() |
| for ((_, plugin) in plugins.getMappedValues()) { |
| val (coordinate, versionNode) = getPluginFromTomlEntry(versions, plugin) ?: continue |
| val group = coordinate.substringBefore(':') |
| val gradleCoordinate = "$group:$group.gradle.plugin:${coordinate.substringAfterLast(':')}" |
| val dependency = Dependency.parse(gradleCoordinate) |
| dependencyToElement[plugin] = dependency |
| // Check dependencies without the PSI read lock, because we |
| // may need to make network requests to retrieve version info. |
| context.driver.runLaterOutsideReadAction { |
| checkDependency(context, dependency, false, versionNode, plugin) |
| } |
| } |
| checkDuplication(context, dependencyToElement) { dep: Dependency -> dep.group ?: "" } |
| } |
| } |
| |
| override fun afterCheckFile(context: Context) { |
| maybeReportJavaTargetCompatibilityIssue(context) |
| maybeReportAgpVersionIssue(context) |
| } |
| |
| private fun maybeReportJavaTargetCompatibilityIssue(context: Context) { |
| if (mAppliedJavaPlugin && !(mDeclaredSourceCompatibility && mDeclaredTargetCompatibility)) { |
| val file = context.file |
| val contents = context.client.readFile(file).toString() |
| val message = |
| when { |
| mDeclaredTargetCompatibility -> "no Java sourceCompatibility directive" |
| mDeclaredSourceCompatibility -> "no Java targetCompatibility directive" |
| else -> "no Java language level directives" |
| } |
| val fixDisplayName = |
| when { |
| mDeclaredTargetCompatibility -> "Insert sourceCompatibility directive for JDK8" |
| mDeclaredSourceCompatibility -> "Insert targetCompatibility directive for JDK8" |
| else -> "Insert JDK8 language level directives" |
| } |
| val insertion = |
| when { |
| // Note that these replacement texts must be valid in both Groovy and KotlinScript Gradle |
| // files |
| mDeclaredTargetCompatibility -> "\njava.sourceCompatibility = JavaVersion.VERSION_1_8" |
| mDeclaredSourceCompatibility -> "\njava.targetCompatibility = JavaVersion.VERSION_1_8" |
| else -> |
| """ |
| |
| java { |
| sourceCompatibility = JavaVersion.VERSION_1_8 |
| targetCompatibility = JavaVersion.VERSION_1_8 |
| } |
| """ |
| .trimIndent() |
| } |
| val fix = |
| fix() |
| .replace() |
| .name(fixDisplayName) |
| .range(Location.create(context.file, contents, 0, contents.length)) |
| .end() |
| .with(insertion) |
| .build() |
| report(context, mJavaPluginInfo!!.cookie, JAVA_PLUGIN_LANGUAGE_LEVEL, message, fix) |
| } |
| } |
| |
| private fun maybeReportAgpVersionIssue(context: Context) { |
| // b/144442233: hide check for outdated AGP if we are reasonably sure google() |
| // is not in plugin resolution repositories |
| if (mDeclaredGoogleMavenRepository || !mDeclaredBuildscriptRepository) { |
| agpVersionCheckInfo?.let { |
| val versionString = it.newerVersion.toString() |
| val currentIdentifier = it.dependency.version?.toIdentifier() |
| val message = |
| getNewerVersionAvailableMessage(it.dependency, versionString, it.safeReplacement) |
| val fix = |
| when { |
| it.isResolved -> null |
| currentIdentifier == null -> null |
| else -> |
| getUpdateDependencyFix( |
| currentIdentifier, |
| versionString, |
| it.newerVersionIsSafe, |
| it.safeReplacement, |
| ) |
| } |
| report(context, it.cookie, AGP_DEPENDENCY, message, fix) |
| } |
| } |
| } |
| |
| private fun checkKaptUsage( |
| dependency: String, |
| libTomlValue: LintTomlValue?, |
| context: GradleContext, |
| statementCookie: Any, |
| ) { |
| // Drop version, leaving "group:module" |
| val module = dependency.substringBeforeLast(':') |
| // See if we have a KSP replacement |
| val replacement = |
| annotationProcessorsWithKspReplacements[module] ?: return // No replacement to offer |
| |
| val fix = |
| if (!mAppliedKspPlugin) { |
| // KSP plugin not applied yet in this module, point to docs on how to enable it |
| fix() |
| .name( |
| "Learn about how to enable KSP and use the KSP processor for this dependency instead" |
| ) |
| .url("https://developer.android.com/studio/build/migrate-to-ksp") |
| .build() |
| } else { |
| if (libTomlValue != null) { // Dependency is from version catalog |
| val declaredWithGroupAndName = (libTomlValue as? LintTomlMapValue)?.get("group") != null |
| val catalogFix = |
| if (declaredWithGroupAndName) { |
| val (oldGroup, oldName) = module.split(":") |
| val (newGroup, newName) = replacement.split(":") |
| fix() |
| .replace() |
| .range(libTomlValue.getLocation()) |
| .pattern("((.*)$oldGroup(.*)$oldName(.*))") |
| .with("\\k<2>$newGroup\\k<3>$newName\\k<4>") |
| .build() |
| } else { |
| fix() |
| .replace() |
| .range(libTomlValue.getLocation()) |
| .text(module) |
| .with(replacement) |
| .build() |
| } |
| val usageFix = fix().replace().text("kapt").with("ksp").build() |
| |
| fix().name("Replace usage of kapt with KSP").composite(catalogFix, usageFix) |
| } else { // Dependency is declared locally in the build.gradle file |
| // Fix within just build.gradle file for locally declared dependency |
| fix() |
| .name("Replace usage of kapt with KSP") |
| .replace() |
| .pattern("((.*)kapt(.*)$module(.*))") |
| .with("\\k<2>ksp\\k<3>$replacement\\k<4>") |
| .build() |
| } |
| } |
| |
| report( |
| context = context, |
| cookie = statementCookie, |
| issue = KAPT_USAGE_INSTEAD_OF_KSP, |
| message = |
| "This library supports using KSP instead of kapt," + |
| " which greatly improves performance. Learn more: " + |
| "https://developer.android.com/studio/build/migrate-to-ksp", |
| fix = fix, |
| ) |
| } |
| |
| /** |
| * Checks to see if a KTX extension is available for the given library. If so, we offer a |
| * suggestion to switch the dependency to the KTX version. See |
| * https://developer.android.com/kotlin/ktx for details. |
| * |
| * This should be called outside of a read action, since it may trigger network requests. |
| */ |
| private fun checkForKtxExtension( |
| context: Context, |
| groupId: String, |
| artifactId: String, |
| version: Version, |
| cookie: Any, |
| ) { |
| if (!mAppliedKotlinAndroidPlugin) return |
| if (artifactId.endsWith("-ktx")) return |
| if (cookie is LintTomlValue) return |
| |
| val mavenName = "$groupId:$artifactId" |
| if (!libraryHasKtxExtension(mavenName)) { |
| return |
| } |
| |
| val artifact = context.project.buildVariant?.artifact ?: return |
| // Make sure the Kotlin stdlib is used by the main artifact (not just by tests). |
| if (artifact.type != LintModelArtifactType.MAIN) { |
| return |
| } |
| artifact.findCompileDependency("org.jetbrains.kotlin:kotlin-stdlib") ?: return |
| |
| // Make sure the KTX extension exists for this version of the library. |
| val repository = getGoogleMavenRepository(context.client) |
| repository.findVersion( |
| groupId, |
| "$artifactId-ktx", |
| filter = { it == version }, |
| allowPreview = true, |
| ) ?: return |
| |
| // Note: once b/155974293 is fixed, we can check whether the KTX extension is |
| // already a direct dependency. If it is, then we could offer a slightly better |
| // warning message along the lines of: "There is no need to declare this dependency |
| // because the corresponding KTX extension pulls it in automatically." |
| |
| val msg = "Add suffix `-ktx` to enable the Kotlin extensions for this library" |
| val fix = |
| fix() |
| .name("Replace with KTX dependency") |
| .replace() |
| .text(mavenName) |
| .with("$mavenName-ktx") |
| .build() |
| report(context, cookie, KTX_EXTENSION_AVAILABLE, msg, fix) |
| } |
| |
| private fun checkForBomUsageWithoutPlatform( |
| property: String, |
| dependency: String, |
| value: String, |
| context: GradleContext, |
| valueCookie: Any, |
| ) { |
| if (listOf("platform", "enforcedPlatform").any { value.startsWith("$it(") }) { |
| return |
| } |
| if ( |
| dependency.substringBeforeLast(':') in commonBoms && |
| (CompileConfiguration.IMPLEMENTATION.matches(property) || |
| CompileConfiguration.API.matches(property)) |
| ) { |
| val message = "BOM should be added with a call to platform()" |
| val fix = |
| fix() |
| .name("Add platform() to BOM declaration", true) |
| .replace() |
| .text(value) |
| .with("platform($value)") |
| .build() |
| report(context, valueCookie, BOM_WITHOUT_PLATFORM, message, fix) |
| } |
| } |
| |
| /** |
| * Report any blocked dependencies that weren't found in the build.gradle source file during |
| * processing (we don't have accurate position info at this point) |
| */ |
| private fun checkBlockedDependencies(context: Context, project: Project) { |
| val blockedDependencies = blockedDependencies[project] ?: return |
| val dependencies = blockedDependencies.getForbiddenDependencies() |
| if (dependencies.isNotEmpty()) { |
| for (path in dependencies) { |
| val message = getBlockedDependencyMessage(path) |
| val projectDir = context.project.dir |
| val gc = |
| path[0].findLibrary()?.let { |
| if (it is LintModelExternalLibrary) { |
| it.resolvedCoordinates |
| } else null |
| } |
| val location = |
| if (gc != null) { |
| getDependencyLocation(context, gc.groupId, gc.artifactId, gc.version) |
| } else { |
| val mavenName = path[0].artifactName |
| guessGradleLocation(context.client, projectDir, mavenName) |
| } |
| context.report(Incident(DUPLICATE_CLASSES, location, message), map()) |
| } |
| } |
| this.blockedDependencies.remove(project) |
| } |
| |
| private fun report( |
| context: Context, |
| cookie: Any, |
| issue: Issue, |
| message: String, |
| fix: LintFix? = null, |
| partial: Boolean = false, |
| overrideSeverity: Severity? = null, |
| constraint: Constraint? = null, |
| ): Boolean { |
| // Some methods in GradleDetector are run without the PSI read lock in order |
| // to accommodate network requests, so we grab the read lock here. |
| var reportCreated = false |
| context.client.runReadAction( |
| Runnable { |
| val enabled = context.isEnabled(issue) |
| if (enabled && context is GradleContext) { |
| val location = context.getLocation(cookie) |
| val incident = Incident(issue, cookie, location, message, fix) |
| overrideSeverity?.let { incident.overrideSeverity(it) } |
| if (constraint != null) { |
| context.report(incident, constraint) |
| } else if (partial) { |
| context.report(incident, map()) |
| } else { |
| context.report(incident) |
| } |
| reportCreated = true |
| } else if (enabled && context is TomlContext) { |
| val location = context.getLocation(cookie) |
| val start = location.start?.offset ?: 0 |
| val checkComments = context.containsCommentSuppress() |
| if (checkComments && context.isSuppressedWithComment(start, issue)) { |
| return@Runnable |
| } |
| val incident = Incident(issue, location, message, fix) |
| overrideSeverity?.let { incident.overrideSeverity(it) } |
| if (constraint != null) { |
| context.report(incident, constraint) |
| } else if (partial) { |
| context.report(incident, map()) |
| } else { |
| context.report(incident) |
| } |
| reportCreated = true |
| } |
| } |
| ) |
| return reportCreated |
| } |
| |
| /** |
| * Normally, all warnings reported for a given issue will have the same severity, so it isn't |
| * possible to have some of them reported as errors and others as warnings. And this is |
| * intentional, since users should get to designate whether an issue is an error or a warning (or |
| * ignored for that matter). |
| * |
| * However, for [COMPATIBILITY] we want to treat some issues as fatal (breaking the build) but not |
| * others. To achieve this we tweak things a little bit. All compatibility issues are now marked |
| * as fatal, and if we're *not* in the "fatal only" mode, all issues are reported as before (with |
| * severity fatal, which has the same visual appearance in the IDE as the previous severity, |
| * "error".) However, if we're in a "fatal-only" build, then we'll stop reporting the issues that |
| * aren't meant to be treated as fatal. That's what this method does; issues reported to it should |
| * always be reported as fatal. There is a corresponding method, |
| * [reportNonFatalCompatibilityIssue] which can be used to report errors that shouldn't break the |
| * build; those are ignored in fatal-only mode. |
| */ |
| private fun reportFatalCompatibilityIssue(context: Context, cookie: Any, message: String) { |
| report(context, cookie, COMPATIBILITY, message) |
| } |
| |
| private fun reportFatalCompatibilityIssue( |
| context: Context, |
| cookie: Any, |
| message: String, |
| fix: LintFix?, |
| ) { |
| report(context, cookie, COMPATIBILITY, message, fix) |
| } |
| |
| /** See [reportFatalCompatibilityIssue] for an explanation. */ |
| private fun reportNonFatalCompatibilityIssue( |
| context: Context, |
| cookie: Any, |
| message: String, |
| lintFix: LintFix? = null, |
| ) { |
| if (context.driver.fatalOnlyMode) { |
| return |
| } |
| |
| report(context, cookie, COMPATIBILITY, message, lintFix) |
| } |
| |
| private fun reportFatalCompatibilityIssue(context: Context, location: Location, message: String) { |
| // Some methods in GradleDetector are run without the PSI read lock in order |
| // to accommodate network requests, so we grab the read lock here. |
| context.client.runReadAction { context.report(COMPATIBILITY, location, message) } |
| } |
| |
| /** See [reportFatalCompatibilityIssue] for an explanation. */ |
| private fun reportNonFatalCompatibilityIssue( |
| context: Context, |
| location: Location, |
| message: String, |
| ) { |
| if (context.driver.fatalOnlyMode) { |
| return |
| } |
| |
| // Some methods in GradleDetector are run without the PSI read lock in order |
| // to accommodate network requests, so we grab the read lock here. |
| context.client.runReadAction { context.report(COMPATIBILITY, location, message) } |
| } |
| |
| private fun getSdkVersion(value: String, valueCookie: Any): Int { |
| var version = 0 |
| if (isStringLiteral(value)) { |
| val codeName = getStringLiteralValue(value, valueCookie) |
| if (codeName != null) { |
| if (isNumberString(codeName)) { |
| // Don't access numbered strings; should be literal numbers (lint will warn) |
| return -1 |
| } |
| val androidVersion = SdkVersionInfo.getVersion(codeName, null) |
| if (androidVersion != null) { |
| version = androidVersion.featureLevel |
| } |
| } |
| } else { |
| version = getIntLiteralValue(value, -1) |
| } |
| return version |
| } |
| |
| // TODO(b/279886738): resolving a Dependency against the project's artifacts should |
| // from a theoretical point of view return a Component. However, here, we're not |
| // really *conceptually* resolving a Dependency, because what this is actually used |
| // for is to guess what the value of a version variable in an interpolated String |
| // might be, and rather than model variables and their values, we pull the resolved |
| // version and hope for the best. For our purposes, that's not completely wrong. |
| @SuppressWarnings("ExpensiveAssertion") |
| private fun resolveCoordinate( |
| context: GradleContext, |
| property: String, |
| dependency: Dependency, |
| ): Dependency? { |
| fun Component.toDependency() = Dependency(group, name, RichVersion.require(version)) |
| assert(dependency.version?.toIdentifier()?.contains("$") ?: false) { |
| dependency.version.toString() |
| } |
| |
| val project = context.project |
| val variant = project.buildVariant |
| if (variant != null) { |
| val artifact = |
| when { |
| property.startsWith("androidTest") -> variant.androidTestArtifact |
| property.startsWith("testFixtures") -> variant.testFixturesArtifact |
| property.startsWith("test") -> variant.testArtifact |
| else -> variant.mainArtifact |
| } ?: return null |
| for (library in artifact.dependencies.getAll()) { |
| if (library is LintModelExternalLibrary) { |
| val mc = library.resolvedCoordinates |
| if (mc.groupId == dependency.group && mc.artifactId == dependency.name) { |
| val version = Version.parse(mc.version) |
| return Component(mc.groupId, mc.artifactId, version).toDependency() |
| } |
| } |
| } |
| } |
| return null |
| } |
| |
| /** True if the given project uses the legacy http library. */ |
| private fun usesLegacyHttpLibrary(project: Project): Boolean { |
| val model = project.buildModule ?: return false |
| for (file in model.bootClassPath) { |
| if (file.endsWith("org.apache.http.legacy.jar")) { |
| return true |
| } |
| } |
| |
| return false |
| } |
| |
| private fun getUpdateDependencyFix( |
| currentVersion: String, |
| suggestedVersion: String, |
| suggestedVersionIsSafe: Boolean = false, |
| safeReplacement: Version? = null, |
| ): LintFix { |
| val fix = |
| fix() |
| .name("Change to $suggestedVersion") |
| .sharedName("Update versions") |
| .replace() |
| .text(currentVersion) |
| .with(suggestedVersion) |
| .autoFix(suggestedVersionIsSafe, suggestedVersionIsSafe) |
| .build() |
| return if (safeReplacement != null) { |
| val stableVersion = safeReplacement.toString() |
| val stableFix = |
| fix() |
| .name("Change to $stableVersion") |
| .sharedName("Update versions") |
| .replace() |
| .text(currentVersion) |
| .with(stableVersion) |
| .autoFix() |
| .build() |
| fix().alternatives(fix, stableFix) |
| } else { |
| fix |
| } |
| } |
| |
| /** |
| * Returns the "group:artifact" address of a dependency, unless it's a Gradle plugin in which case |
| * it returns the plugin id. |
| */ |
| private fun Dependency.id(): String { |
| return if (isGradlePlugin()) { |
| group!! |
| } else { |
| "$group:$name" |
| } |
| } |
| |
| private fun Dependency.isGradlePlugin(): Boolean = |
| group != null && name.endsWith(".gradle.plugin") && name == "$group.gradle.plugin" |
| |
| private fun getNewerVersionAvailableMessage( |
| dependency: Dependency, |
| version: String, |
| stable: Version?, |
| ): String { |
| val message = StringBuilder() |
| with(message) { |
| append("A newer version of ") |
| append(dependency.id()) |
| append(" than ") |
| append("${dependency.version}") |
| append(" is available: ") |
| append(version) |
| if (stable != null) { |
| append(". (There is also a newer version of ") |
| append(stable.major.toString()) |
| append(".") |
| append(stable.minor.toString()) |
| // \uD835\uDC65 is 𝑥, unicode for Mathematical Italic Small X |
| append(".\uD835\uDC65 available, if upgrading to ") |
| append(version) |
| append(" is difficult: ") |
| append(stable.toString()) |
| append(")") |
| } |
| } |
| return message.toString() |
| } |
| |
| private fun findFirst(coordinates: Collection<LintModelMavenName>): LintModelMavenName { |
| return Collections.min(coordinates) { o1, o2 -> o1.toString().compareTo(o2.toString()) } |
| } |
| |
| override fun filterIncident(context: Context, incident: Incident, map: LintMap): Boolean { |
| val issue = incident.issue |
| if (issue === DUPLICATE_CLASSES) { |
| return context.mainProject.minSdk < 23 || usesLegacyHttpLibrary(context.mainProject) |
| } else { |
| error(issue.id) |
| } |
| } |
| |
| override fun checkMergedProject(context: Context) { |
| if (context.isGlobalAnalysis() && context.driver.isIsolated()) { |
| // Already performed on occurrences in the file being edited |
| return |
| } |
| checkLibraryConsistency(context) |
| } |
| |
| private fun getBlockedDependencyMessage(path: List<LintModelDependency>): String { |
| val direct = path.size == 1 |
| val message: String |
| val resolution = |
| "Solutions include " + |
| "finding newer versions or alternative libraries that don't have the " + |
| "same problem (for example, for `httpclient` use `HttpUrlConnection` or " + |
| "`okhttp` instead), or repackaging the library using something like " + |
| "`jarjar`." |
| if (direct) { |
| message = |
| "`${path[0].getArtifactId()}` defines classes that conflict with classes now provided by Android. $resolution" |
| } else { |
| val sb = StringBuilder() |
| var first = true |
| for (library in path) { |
| if (first) { |
| first = false |
| } else { |
| sb.append(" \u2192 ") // right arrow |
| } |
| val coordinates = library.artifactName |
| sb.append(coordinates) |
| } |
| sb.append(") ") |
| val chain = sb.toString() |
| message = |
| "`${path[0].getArtifactId()}` depends on a library " + |
| "(${path[path.size - 1].artifactName}) which defines " + |
| "classes that conflict with classes now provided by Android. $resolution " + |
| "Dependency chain: $chain" |
| } |
| return message |
| } |
| |
| private fun getNewerVersion(version1: Version, major: Int, minor: Int, micro: Int): Version? = |
| Version.parse("$major.$minor.$micro").takeIf { |
| version1 > Version.prefixInfimum("0") && version1 < it |
| } |
| |
| private fun getNewerVersion(version1: Version, major: Int, minor: Int): Version? = |
| Version.parse("$major.$minor").takeIf { version1 > Version.prefixInfimum("0") && version1 < it } |
| |
| private var googleMavenRepository: GoogleMavenRepository? = null |
| private var googlePlaySdkIndex: GooglePlaySdkIndex? = null |
| |
| private fun getGoogleMavenRepoVersion( |
| context: Context, |
| dependency: Dependency, |
| filter: Predicate<Version>?, |
| ): Version? { |
| val repository = getGoogleMavenRepository(context.client) |
| return repository.findVersion(dependency, filter, dependency.explicitlyIncludesPreview) |
| } |
| |
| fun getGoogleMavenRepository(client: LintClient): GoogleMavenRepository { |
| return googleMavenRepository |
| ?: run { |
| val cacheDir = client.getCacheDir(MAVEN_GOOGLE_CACHE_DIR_KEY, true) |
| val repository = |
| object : GoogleMavenRepository(cacheDir?.toPath()) { |
| |
| public override fun readUrlData( |
| url: String, |
| timeout: Int, |
| lastModified: Long, |
| ): ReadUrlDataResult = readUrlData(client, url, timeout, lastModified) |
| |
| public override fun error(throwable: Throwable, message: String?) = |
| client.log(throwable, message) |
| } |
| |
| googleMavenRepository = repository |
| repository |
| } |
| } |
| |
| private fun getGooglePlaySdkIndex(client: LintClient): GooglePlaySdkIndex { |
| return googlePlaySdkIndex |
| ?: run { |
| val cacheDir = client.getCacheDir(GOOGLE_PLAY_SDK_INDEX_KEY, true) |
| val repository = playSdkIndexFactory(cacheDir?.toPath(), client) |
| googlePlaySdkIndex = repository |
| repository |
| } |
| } |
| |
| companion object { |
| private var lastTargetSdkVersion: Int = -1 |
| private var lastTargetSdkVersionFile: File? = null |
| |
| /** If you invoke the target SDK version migration assistant, stop flagging edits. */ |
| fun stopFlaggingTargetSdkEdits() { |
| lastTargetSdkVersion = Integer.MAX_VALUE |
| lastTargetSdkVersionFile = null |
| } |
| |
| /** Calendar to use to look up the current time (used by tests to set specific time). */ |
| var calendar: Calendar? = null |
| |
| const val KEY_COORDINATE = "coordinate" |
| const val KEY_REVISION = "revision" |
| |
| private const val VC_LIBRARY_PREFIX = "libs." |
| private const val VC_PLUGIN_PREFIX = "libs.plugins." |
| |
| private val IMPLEMENTATION = Implementation(GradleDetector::class.java, Scope.GRADLE_SCOPE) |
| private val IMPLEMENTATION_WITH_TOML = |
| Implementation( |
| GradleDetector::class.java, |
| Scope.GRADLE_AND_TOML_SCOPE, |
| Scope.GRADLE_SCOPE, |
| Scope.TOML_SCOPE, |
| ) |
| private val IMPLEMENTATION_WITH_MANIFEST = |
| Implementation( |
| GradleDetector::class.java, |
| EnumSet.of(Scope.GRADLE_FILE, Scope.MANIFEST), |
| Scope.GRADLE_SCOPE, |
| Scope.MANIFEST_SCOPE, |
| ) |
| |
| /** Obsolete dependencies. */ |
| @JvmField |
| val DEPENDENCY = |
| Issue.create( |
| id = "GradleDependency", |
| briefDescription = "Obsolete Gradle Dependency", |
| explanation = |
| """ |
| This detector looks for usages of libraries where the version you are using \ |
| is not the current stable release. Using older versions is fine, and there \ |
| are cases where you deliberately want to stick with an older version. \ |
| However, you may simply not be aware that a more recent version is \ |
| available, and that is what this lint check helps find.""", |
| category = Category.CORRECTNESS, |
| priority = 4, |
| severity = Severity.WARNING, |
| implementation = IMPLEMENTATION_WITH_TOML, |
| ) |
| |
| /** Project imports a dependency with different versions. */ |
| @JvmField |
| val MULTIPLE_VERSIONS_DEPENDENCY = |
| Issue.create( |
| id = "SimilarGradleDependency", |
| briefDescription = "Multiple Versions Gradle Dependency", |
| explanation = |
| """ |
| This detector looks for usages of libraries when name and group are the same \ |
| but versions are different. Using multiple versions in big project is fine, \ |
| and there are cases where you deliberately want to stick with such approach. \ |
| However, you may simply not be aware that this situation happens, and that is \ |
| what this lint check helps find.""", |
| category = Category.CORRECTNESS, |
| priority = 4, |
| severity = Severity.INFORMATIONAL, |
| implementation = IMPLEMENTATION_WITH_TOML, |
| ) |
| |
| @JvmField |
| val CREDENTIAL_DEP = |
| Issue.create( |
| id = "CredentialDependency", |
| briefDescription = "`credentials-play-services-auth` is Required", |
| explanation = |
| """ |
| The dependency `androidx.credentials:credentials-play-services-auth` is required \ |
| to get support from Play services for the Credential Manager API to work. \ |
| For Android 14 or higher, this is optional. Please check release notes for the \ |
| latest version. |
| """, |
| moreInfo = "https://developer.android.com/jetpack/androidx/releases/credentials", |
| category = Category.CORRECTNESS, |
| priority = 5, |
| severity = Severity.WARNING, |
| implementation = IMPLEMENTATION_WITH_TOML, |
| androidSpecific = true, |
| ) |
| |
| /** |
| * Using a gradle group:artifact:id directly instead of placing it in the version catalog TOML |
| * file |
| */ |
| @JvmField |
| val SWITCH_TO_TOML = |
| Issue.create( |
| id = "UseTomlInstead", |
| briefDescription = "Use TOML Version Catalog Instead", |
| explanation = |
| """ |
| If your project is using a `libs.versions.toml` file, you should place \ |
| all Gradle dependencies in the TOML file. This lint check looks for \ |
| version declarations outside of the TOML file and suggests moving them \ |
| (and in the IDE, provides a quickfix to performing the operation automatically). |
| """, |
| category = Category.PRODUCTIVITY, |
| priority = 4, |
| severity = Severity.WARNING, |
| implementation = IMPLEMENTATION_WITH_TOML, |
| ) |
| |
| /** A dependency on an obsolete version of the Android Gradle Plugin. */ |
| @JvmField |
| val AGP_DEPENDENCY = |
| Issue.create( |
| id = "AndroidGradlePluginVersion", |
| briefDescription = "Obsolete Android Gradle Plugin Version", |
| explanation = |
| """ |
| This detector looks for usage of the Android Gradle Plugin where the version \ |
| you are using is not the current stable release. Using older versions is fine, \ |
| and there are cases where you deliberately want to stick with an older version. \ |
| However, you may simply not be aware that a more recent version is available, \ |
| and that is what this lint check helps find.""", |
| category = Category.CORRECTNESS, |
| priority = 4, |
| severity = Severity.WARNING, |
| androidSpecific = true, |
| implementation = IMPLEMENTATION_WITH_TOML, |
| ) |
| |
| /** Deprecated Gradle constructs. */ |
| @JvmField |
| val DEPRECATED = |
| Issue.create( |
| id = "GradleDeprecated", |
| briefDescription = "Deprecated Gradle Construct", |
| explanation = |
| """ |
| This detector looks for deprecated Gradle constructs which currently work \ |
| but will likely stop working in a future update.""", |
| category = Category.CORRECTNESS, |
| priority = 6, |
| androidSpecific = true, |
| severity = Severity.WARNING, |
| implementation = IMPLEMENTATION, |
| ) |
| |
| /** Deprecated Gradle configurations. */ |
| @JvmField |
| val DEPRECATED_CONFIGURATION = |
| Issue.create( |
| id = "GradleDeprecatedConfiguration", |
| briefDescription = "Deprecated Gradle Configuration", |
| explanation = |
| """ |
| Some Gradle configurations have been deprecated since Android Gradle Plugin 3.0.0 \ |
| and will be removed in a future version of the Android Gradle Plugin. |
| """, |
| category = Category.CORRECTNESS, |
| moreInfo = "https://d.android.com/r/tools/update-dependency-configurations", |
| priority = 6, |
| severity = Severity.WARNING, |
| implementation = IMPLEMENTATION, |
| ) |
| |
| /** Incompatible Android Gradle plugin. */ |
| @JvmField |
| val GRADLE_PLUGIN_COMPATIBILITY = |
| Issue.create( |
| id = "GradlePluginVersion", |
| briefDescription = "Incompatible Android Gradle Plugin", |
| explanation = |
| """ |
| Not all versions of the Android Gradle plugin are compatible with all \ |
| versions of the SDK. If you update your tools, or if you are trying to \ |
| open a project that was built with an old version of the tools, you may \ |
| need to update your plugin version number.""", |
| category = Category.CORRECTNESS, |
| priority = 8, |
| severity = Severity.ERROR, |
| androidSpecific = true, |
| implementation = IMPLEMENTATION, |
| ) |
| |
| /** Invalid or dangerous paths. */ |
| @JvmField |
| val PATH = |
| Issue.create( |
| id = "GradlePath", |
| briefDescription = "Gradle Path Issues", |
| explanation = |
| """ |
| Gradle build scripts are meant to be cross platform, so file paths use \ |
| Unix-style path separators (a forward slash) rather than Windows path \ |
| separators (a backslash). Similarly, to keep projects portable and \ |
| repeatable, avoid using absolute paths on the system; keep files within \ |
| the project instead. To share code between projects, consider creating \ |
| an android-library and an AAR dependency""", |
| category = Category.CORRECTNESS, |
| priority = 4, |
| severity = Severity.WARNING, |
| implementation = IMPLEMENTATION, |
| ) |
| |
| /** Constructs the IDE support struggles with. */ |
| @JvmField |
| val IDE_SUPPORT = |
| Issue.create( |
| id = "GradleIdeError", |
| briefDescription = "Gradle IDE Support Issues", |
| explanation = |
| """ |
| Gradle is highly flexible, and there are things you can do in Gradle \ |
| files which can make it hard or impossible for IDEs to properly handle \ |
| the project. This lint check looks for constructs that potentially \ |
| break IDE support.""", |
| category = Category.CORRECTNESS, |
| priority = 4, |
| severity = Severity.ERROR, |
| implementation = IMPLEMENTATION, |
| ) |
| |
| /** Using + in versions. */ |
| @JvmField |
| val PLUS = |
| Issue.create( |
| id = "GradleDynamicVersion", |
| briefDescription = "Gradle Dynamic Version", |
| explanation = |
| """ |
| Using `+` in dependencies lets you automatically pick up the latest \ |
| available version rather than a specific, named version. However, \ |
| this is not recommended; your builds are not repeatable; you may have \ |
| tested with a slightly different version than what the build server \ |
| used. (Using a dynamic version as the major version number is more \ |
| problematic than using it in the minor version position.)""", |
| category = Category.CORRECTNESS, |
| priority = 4, |
| severity = Severity.WARNING, |
| implementation = IMPLEMENTATION, |
| ) |
| |
| /** Accidentally calling a getter instead of your own methods. */ |
| @JvmField |
| val GRADLE_GETTER = |
| Issue.create( |
| id = "GradleGetter", |
| briefDescription = "Gradle Implicit Getter Call", |
| explanation = |
| """ |
| Gradle will let you replace specific constants in your build scripts \ |
| with method calls, so you can for example dynamically compute a version \ |
| string based on your current version control revision number, rather \ |
| than hardcoding a number. |
| |
| When computing a version name, it's tempting to for example call the \ |
| method to do that `getVersionName`. However, when you put that method \ |
| call inside the `defaultConfig` block, you will actually be calling the \ |
| Groovy getter for the `versionName` property instead. Therefore, you \ |
| need to name your method something which does not conflict with the \ |
| existing implicit getters. Consider using `compute` as a prefix instead \ |
| of `get`.""", |
| category = Category.CORRECTNESS, |
| priority = 6, |
| severity = Severity.ERROR, |
| androidSpecific = true, |
| implementation = IMPLEMENTATION, |
| ) |
| |
| /** Using incompatible versions. */ |
| @JvmField |
| val COMPATIBILITY = |
| Issue.create( |
| id = "GradleCompatible", |
| briefDescription = "Incompatible Gradle Versions", |
| explanation = |
| """ |
| There are some combinations of libraries, or tools and libraries, that \ |
| are incompatible, or can lead to bugs. One such incompatibility is \ |
| compiling with a version of the Android support libraries that is not \ |
| the latest version (or in particular, a version lower than your \ |
| `targetSdkVersion`).""", |
| category = Category.CORRECTNESS, |
| priority = 8, |
| severity = Severity.FATAL, |
| androidSpecific = true, |
| implementation = IMPLEMENTATION, |
| ) |
| |
| /** Using a string where an integer is expected. */ |
| @JvmField |
| val STRING_INTEGER = |
| Issue.create( |
| id = "StringShouldBeInt", |
| briefDescription = "String should be int", |
| explanation = |
| """ |
| The properties `compileSdkVersion`, `minSdkVersion` and `targetSdkVersion` \ |
| are usually numbers, but can be strings when you are using an add-on (in \ |
| the case of `compileSdkVersion`) or a preview platform (for the other two \ |
| properties). |
| |
| However, you can not use a number as a string (e.g. "19" instead of 19); \ |
| that will result in a platform not found error message at build/sync \ |
| time.""", |
| category = Category.CORRECTNESS, |
| priority = 8, |
| severity = Severity.ERROR, |
| androidSpecific = true, |
| implementation = IMPLEMENTATION, |
| ) |
| |
| /** Attempting to use substitution with single quotes. */ |
| @JvmField |
| val NOT_INTERPOLATED = |
| Issue.create( |
| id = "NotInterpolated", |
| briefDescription = "Incorrect Interpolation", |
| explanation = |
| """ |
| To insert the value of a variable, you can use `${"$"}{variable}` inside a \ |
| string literal, but **only** if you are using double quotes!""", |
| moreInfo = "https://www.groovy-lang.org/syntax.html#_string_interpolation", |
| category = Category.CORRECTNESS, |
| priority = 8, |
| severity = Severity.ERROR, |
| implementation = IMPLEMENTATION, |
| ) |
| |
| /** A newer version is available on a remote server. */ |
| @JvmField |
| val REMOTE_VERSION = |
| Issue.create( |
| id = "NewerVersionAvailable", |
| briefDescription = "Newer Library Versions Available", |
| explanation = |
| """ |
| This detector checks with a central repository to see if there are newer \ |
| versions available for the dependencies used by this project. This is \ |
| similar to the `GradleDependency` check, which checks for newer versions \ |
| available in the Android SDK tools and libraries, but this works with any \ |
| MavenCentral dependency, and connects to the library every time, which \ |
| makes it more flexible but also **much** slower.""", |
| category = Category.CORRECTNESS, |
| priority = 4, |
| severity = Severity.WARNING, |
| implementation = IMPLEMENTATION_WITH_TOML, |
| enabledByDefault = false, |
| ) |
| |
| /** The API version is set too low. */ |
| @JvmField |
| val MIN_SDK_TOO_LOW = |
| Issue.create( |
| id = "MinSdkTooLow", |
| briefDescription = "API Version Too Low", |
| explanation = |
| """ |
| The value of the `minSdkVersion` property is too low and can be \ |
| incremented without noticeably reducing the number of supported \ |
| devices.""", |
| category = Category.CORRECTNESS, |
| priority = 4, |
| severity = Severity.WARNING, |
| implementation = IMPLEMENTATION, |
| androidSpecific = true, |
| enabledByDefault = false, |
| ) |
| |
| /** Accidentally using octal numbers. */ |
| @JvmField |
| val ACCIDENTAL_OCTAL = |
| Issue.create( |
| id = "AccidentalOctal", |
| briefDescription = "Accidental Octal", |
| explanation = |
| """ |
| In Groovy, an integer literal that starts with a leading 0 will be \ |
| interpreted as an octal number. That is usually (always?) an accident \ |
| and can lead to subtle bugs, for example when used in the `versionCode` \ |
| of an app.""", |
| category = Category.CORRECTNESS, |
| priority = 2, |
| severity = Severity.ERROR, |
| implementation = IMPLEMENTATION, |
| ) |
| |
| @JvmField |
| val BUNDLED_GMS = |
| Issue.create( |
| id = "UseOfBundledGooglePlayServices", |
| briefDescription = "Use of bundled version of Google Play services", |
| explanation = |
| """ |
| Google Play services SDK's can be selectively included, which enables a \ |
| smaller APK size. Consider declaring dependencies on individual Google \ |
| Play services SDK's. If you are using Firebase API's \ |
| (https://firebase.google.com/docs/android/setup), Android Studio's \ |
| Tools → Firebase assistant window can automatically add just the \ |
| dependencies needed for each feature.""", |
| moreInfo = "https://developers.google.com/android/guides/setup#split", |
| category = Category.PERFORMANCE, |
| priority = 4, |
| severity = Severity.WARNING, |
| androidSpecific = true, |
| implementation = IMPLEMENTATION, |
| ) |
| |
| /** Using a versionCode that is very high. */ |
| @JvmField |
| val HIGH_APP_VERSION_CODE = |
| Issue.create( |
| id = "HighAppVersionCode", |
| briefDescription = "VersionCode too high", |
| explanation = |
| """ |
| The declared `versionCode` is an Integer. Ensure that the version number is \ |
| not close to the limit. It is recommended to monotonically increase this \ |
| number each minor or major release of the app. Note that updating an app \ |
| with a versionCode over `Integer.MAX_VALUE` is not possible.""", |
| moreInfo = "https://developer.android.com/studio/publish/versioning.html", |
| category = Category.CORRECTNESS, |
| priority = 8, |
| severity = Severity.ERROR, |
| androidSpecific = true, |
| implementation = IMPLEMENTATION, |
| ) |
| |
| /** Dev mode is no longer relevant. */ |
| @JvmField |
| val DEV_MODE_OBSOLETE = |
| Issue.create( |
| id = "DevModeObsolete", |
| briefDescription = "Dev Mode Obsolete", |
| explanation = |
| """ |
| In the past, our documentation recommended creating a `dev` product flavor \ |
| with has a minSdkVersion of 21, in order to enable multidexing to speed up \ |
| builds significantly during development. |
| |
| That workaround is no longer necessary, and it has some serious downsides, \ |
| such as breaking API access checking (since the true `minSdkVersion` is no \ |
| longer known). |
| |
| In recent versions of the IDE and the Gradle plugin, the IDE automatically \ |
| passes the API level of the connected device used for deployment, and if \ |
| that device is at least API 21, then multidexing is automatically turned \ |
| on, meaning that you get the same speed benefits as the `dev` product \ |
| flavor but without the downsides.""", |
| category = Category.PERFORMANCE, |
| priority = 2, |
| severity = Severity.WARNING, |
| androidSpecific = true, |
| implementation = IMPLEMENTATION, |
| ) |
| |
| /** Duplicate HTTP classes. */ |
| @JvmField |
| val DUPLICATE_CLASSES = |
| Issue.create( |
| id = "DuplicatePlatformClasses", |
| briefDescription = "Duplicate Platform Classes", |
| explanation = |
| """ |
| There are a number of libraries that duplicate not just functionality \ |
| of the Android platform but using the exact same class names as the ones \ |
| provided in Android -- for example the apache http classes. This can \ |
| lead to unexpected crashes. |
| |
| To solve this, you need to either find a newer version of the library \ |
| which no longer has this problem, or to repackage the library (and all \ |
| of its dependencies) using something like the `jarjar` tool, or finally, \ |
| rewriting the code to use different APIs (for example, for http code, \ |
| consider using `HttpUrlConnection` or a library like `okhttp`).""", |
| category = Category.CORRECTNESS, |
| priority = 8, |
| severity = Severity.FATAL, |
| androidSpecific = true, |
| implementation = IMPLEMENTATION, |
| ) |
| |
| /** |
| * Reserved variable names used by [pickLibraryVariableName] and [pickVersionVariableName] |
| * suggesting library and version variable names; we need to make sure we keep track of previous |
| * suggestions made such that we don't have multiple quickfixes making the same suggestion and |
| * creating a clash if all fixes are applied. |
| */ |
| var reservedQuickfixNames: MutableMap<String, MutableSet<String>>? = null |
| |
| /** targetSdkVersion about to expire */ |
| @JvmField |
| val EXPIRING_TARGET_SDK_VERSION = |
| Issue.create( |
| id = "ExpiringTargetSdkVersion", |
| briefDescription = "TargetSdkVersion Soon Expiring", |
| explanation = |
| """ |
| Configuring your app to target a recent API level ensures that users benefit \ |
| from significant security and performance improvements, while still allowing \ |
| your app to run on older Android versions (down to the `minSdkVersion`). |
| |
| To update your `targetSdkVersion`, follow the steps from \ |
| "Meeting Google Play requirements for target API level", \ |
| https://developer.android.com/distribute/best-practices/develop/target-sdk.html |
| """, |
| category = Category.COMPLIANCE, |
| priority = 8, |
| severity = Severity.WARNING, |
| androidSpecific = true, |
| implementation = IMPLEMENTATION_WITH_MANIFEST, |
| ) |
| .addMoreInfo( |
| "https://support.google.com/googleplay/android-developer/answer/113469#targetsdk" |
| ) |
| .addMoreInfo( |
| "https://developer.android.com/distribute/best-practices/develop/target-sdk.html" |
| ) |
| |
| /** targetSdkVersion no longer supported */ |
| @JvmField |
| val EXPIRED_TARGET_SDK_VERSION = |
| Issue.create( |
| id = "ExpiredTargetSdkVersion", |
| briefDescription = "TargetSdkVersion No Longer Supported", |
| moreInfo = |
| "https://support.google.com/googleplay/android-developer/answer/113469#targetsdk", |
| explanation = |
| """ |
| Configuring your app to target a recent API level ensures that users benefit \ |
| from significant security and performance improvements, while still allowing \ |
| your app to run on older Android versions (down to the `minSdkVersion`). |
| |
| To update your `targetSdkVersion`, follow the steps from \ |
| "Meeting Google Play requirements for target API level", \ |
| https://developer.android.com/distribute/best-practices/develop/target-sdk.html |
| """, |
| category = Category.COMPLIANCE, |
| priority = 8, |
| severity = Severity.FATAL, |
| androidSpecific = true, |
| implementation = IMPLEMENTATION_WITH_MANIFEST, |
| ) |
| .addMoreInfo( |
| "https://developer.android.com/distribute/best-practices/develop/target-sdk.html" |
| ) |
| |
| /** Using a targetSdkVersion that isn't recent */ |
| @JvmField |
| val TARGET_NEWER = |
| Issue.create( |
| id = "OldTargetApi", |
| briefDescription = "Target SDK attribute is not targeting latest version", |
| explanation = |
| """ |
| When your application runs on a version of Android that is more recent than your \ |
| `targetSdkVersion` specifies that it has been tested with, various compatibility modes \ |
| kick in. This ensures that your application continues to work, but it may look out of \ |
| place. For example, if the `targetSdkVersion` is less than 14, your app may get an \ |
| option button in the UI. |
| |
| To fix this issue, set the `targetSdkVersion` to the highest available value. Then test \ |
| your app to make sure everything works correctly. You may want to consult the \ |
| compatibility notes to see what changes apply to each version you are adding support \ |
| for: https://developer.android.com/reference/android/os/Build.VERSION_CODES.html as well \ |
| as follow this guide: |
| https://developer.android.com/distribute/best-practices/develop/target-sdk.html |
| """, |
| category = Category.CORRECTNESS, |
| priority = 6, |
| severity = Severity.WARNING, |
| implementation = IMPLEMENTATION_WITH_MANIFEST, |
| ) |
| .addMoreInfo( |
| "https://developer.android.com/distribute/best-practices/develop/target-sdk.html" |
| ) |
| .addMoreInfo("https://developer.android.com/reference/android/os/Build.VERSION_CODES.html") |
| |
| /** targetSdkVersion was manually edited */ |
| @JvmField |
| val EDITED_TARGET_SDK_VERSION = |
| Issue.create( |
| id = "EditedTargetSdkVersion", |
| briefDescription = "Manually Edited TargetSdkVersion", |
| explanation = |
| """ |
| Updating the `targetSdkVersion` of an app is seemingly easy: just increment the \ |
| `targetSdkVersion` number in the manifest file! |
| |
| But that's not actually safe. The `targetSdkVersion` controls a wide range of \ |
| behaviors that change from release to release, and to update, you should carefully \ |
| consult the documentation to see what has changed, how your app may need to adjust, \ |
| and then of course, carefully test everything. |
| |
| In new versions of Android Studio, there is a special migration assistant, available \ |
| from the tools menu (and as a quickfix from this lint warning) which analyzes your \ |
| specific app and filters the set of applicable migration steps to those needed for \ |
| your app. |
| |
| This lint check does something very simple: it just detects whether it looks like \ |
| you've manually edited the targetSdkVersion field in a build.gradle file. Obviously, \ |
| as part of doing the above careful steps, you may end up editing the value, which \ |
| would trigger the check -- and it's safe to ignore it; this lint check *only* runs \ |
| in the IDE, not from the command line; it's sole purpose to bring *awareness* to the \ |
| (many) developers who haven't been aware of this issue and have just bumped the \ |
| targetSdkVersion, recompiled, and uploaded their updated app to the Google Play Store, \ |
| sometimes leading to crashes or other problems on newer devices. |
| """, |
| category = Category.CORRECTNESS, |
| priority = 2, |
| severity = Severity.ERROR, |
| androidSpecific = true, |
| implementation = IMPLEMENTATION, |
| ) |
| |
| /** Using a deprecated library. */ |
| @JvmField |
| val DEPRECATED_LIBRARY = |
| Issue.create( |
| id = "OutdatedLibrary", |
| briefDescription = "Outdated Library", |
| explanation = |
| """ |
| Your app is using an outdated version of a library. This may cause violations \ |
| of Google Play policies (see https://play.google.com/about/monetization-ads/ads/) \ |
| and/or may affect your app’s visibility on the Play Store. |
| |
| Please try updating your app with an updated version of this library, or remove \ |
| it from your app. |
| """, |
| category = Category.COMPLIANCE, |
| priority = 5, |
| severity = Severity.WARNING, |
| androidSpecific = true, |
| implementation = IMPLEMENTATION_WITH_TOML, |
| moreInfo = GOOGLE_PLAY_SDK_INDEX_URL, |
| ) |
| |
| /** Using data binding with Kotlin but not Kotlin annotation processing. */ |
| @JvmField |
| val DATA_BINDING_WITHOUT_KAPT = |
| Issue.create( |
| id = "DataBindingWithoutKapt", |
| briefDescription = "Data Binding without Annotation Processing", |
| moreInfo = "https://kotlinlang.org/docs/reference/kapt.html", |
| explanation = |
| """ |
| Apps that use Kotlin and data binding should also apply the kotlin-kapt plugin. |
| """, |
| category = Category.CORRECTNESS, |
| priority = 1, |
| severity = Severity.WARNING, |
| androidSpecific = true, |
| implementation = IMPLEMENTATION, |
| ) |
| |
| /** Using Lifecycle annotation processor with java8. */ |
| @JvmField |
| val LIFECYCLE_ANNOTATION_PROCESSOR_WITH_JAVA8 = |
| Issue.create( |
| id = "LifecycleAnnotationProcessorWithJava8", |
| briefDescription = "Lifecycle Annotation Processor with Java 8 Compile Option", |
| moreInfo = "https://d.android.com/r/studio-ui/lifecycle-release-notes", |
| explanation = |
| """ |
| For faster incremental build, switch to the Lifecycle Java 8 API with these steps: |
| |
| First replace |
| ```gradle |
| annotationProcessor "androidx.lifecycle:lifecycle-compiler:*version*" |
| kapt "androidx.lifecycle:lifecycle-compiler:*version*" |
| ``` |
| with |
| ```gradle |
| implementation "androidx.lifecycle:lifecycle-common-java8:*version*" |
| ``` |
| Then remove any `OnLifecycleEvent` annotations from `Observer` classes \ |
| and make them implement the `DefaultLifecycleObserver` interface. |
| """, |
| category = Category.PERFORMANCE, |
| priority = 6, |
| severity = Severity.WARNING, |
| androidSpecific = true, |
| implementation = IMPLEMENTATION, |
| ) |
| |
| /** Using a vulnerable library. */ |
| @JvmField |
| val RISKY_LIBRARY = |
| Issue.create( |
| id = "RiskyLibrary", |
| briefDescription = "Libraries with Privacy or Security Risks", |
| explanation = |
| """ |
| Your app is using a version of a library that has been identified by \ |
| the library developer as a potential source of privacy and/or security risks. \ |
| This may be a violation of Google Play policies (see \ |
| https://play.google.com/about/monetization-ads/ads/) and/or affect your app’s \ |
| visibility on the Play Store. |
| |
| When available, the individual error messages from lint will include details \ |
| about the reasons for this advisory. |
| |
| Please try updating your app with an updated version of this library, or remove \ |
| it from your app. |
| """, |
| category = Category.SECURITY, |
| priority = 4, |
| severity = Severity.WARNING, |
| androidSpecific = true, |
| implementation = IMPLEMENTATION_WITH_TOML, |
| moreInfo = GOOGLE_PLAY_SDK_INDEX_URL, |
| ) |
| .addMoreInfo("https://goo.gle/RiskyLibrary") |
| |
| @JvmField |
| val ANNOTATION_PROCESSOR_ON_COMPILE_PATH = |
| Issue.create( |
| id = "AnnotationProcessorOnCompilePath", |
| briefDescription = "Annotation Processor on Compile Classpath", |
| explanation = |
| """ |
| This dependency is identified as an annotation processor. Consider adding it to the \ |
| processor path using `annotationProcessor` instead of including it to the \ |
| compile path. |
| """, |
| category = Category.PERFORMANCE, |
| priority = 8, |
| severity = Severity.WARNING, |
| androidSpecific = true, |
| implementation = IMPLEMENTATION, |
| ) |
| |
| @JvmField |
| val KTX_EXTENSION_AVAILABLE = |
| Issue.create( |
| id = "KtxExtensionAvailable", |
| briefDescription = "KTX Extension Available", |
| explanation = |
| """ |
| Android KTX extensions augment some libraries with support for modern Kotlin \ |
| language features like extension functions, extension properties, lambdas, named \ |
| parameters, coroutines, and more. |
| |
| In Kotlin projects, use the KTX version of a library by replacing the \ |
| dependency in your `build.gradle` file. For example, you can replace \ |
| `androidx.fragment:fragment` with `androidx.fragment:fragment-ktx`. |
| """, |
| category = Category.PRODUCTIVITY, |
| priority = 4, |
| severity = Severity.INFORMATIONAL, |
| androidSpecific = true, |
| implementation = IMPLEMENTATION, |
| moreInfo = "https://developer.android.com/kotlin/ktx", |
| ) |
| |
| @JvmField |
| val KAPT_USAGE_INSTEAD_OF_KSP = |
| Issue.create( |
| id = "KaptUsageInsteadOfKsp", |
| briefDescription = "Kapt usage should be replaced with KSP", |
| explanation = |
| """ |
| KSP is a more efficient replacement for kapt. For libraries that support both, \ |
| KSP should be used to improve build times. |
| """, |
| category = Category.PERFORMANCE, |
| priority = 4, |
| severity = Severity.WARNING, |
| androidSpecific = true, |
| implementation = IMPLEMENTATION, |
| moreInfo = "https://developer.android.com/studio/build/migrate-to-ksp", |
| ) |
| |
| @JvmField |
| val BOM_WITHOUT_PLATFORM = |
| Issue.create( |
| id = "BomWithoutPlatform", |
| briefDescription = "Using a BOM without platform call", |
| explanation = |
| """ |
| When including a BOM, the dependency's coordinates must be wrapped \ |
| in a call to `platform()` for Gradle to interpret it correctly. |
| """, |
| category = Category.CORRECTNESS, |
| priority = 4, |
| severity = Severity.WARNING, |
| androidSpecific = true, |
| implementation = IMPLEMENTATION_WITH_TOML, |
| moreInfo = "https://developer.android.com/r/tools/gradle-bom-docs", |
| ) |
| |
| @JvmField |
| val JAVA_PLUGIN_LANGUAGE_LEVEL = |
| Issue.create( |
| id = "JavaPluginLanguageLevel", |
| briefDescription = "No Explicit Java Language Level Given", |
| explanation = |
| """ |
| In modules using plugins deriving from the Gradle `java` plugin (e.g. \ |
| `java-library` or `application`), the java source and target compatibility \ |
| default to the version of the JDK being used to run Gradle, which may cause \ |
| compatibility problems with Android (or other) modules. |
| |
| You can specify an explicit sourceCompatibility and targetCompatibility in this \ |
| module to maintain compatibility no matter which JDK is used to run Gradle. |
| """, |
| category = Category.INTEROPERABILITY, |
| priority = 6, |
| severity = Severity.WARNING, |
| implementation = IMPLEMENTATION, |
| ) |
| |
| @JvmField |
| val JCENTER_REPOSITORY_OBSOLETE = |
| Issue.create( |
| id = "JcenterRepositoryObsolete", |
| briefDescription = "JCenter Maven repository is read-only", |
| explanation = |
| """ |
| The JCenter Maven repository is no longer accepting submissions of Maven \ |
| artifacts since 31st March 2021. Ensure that the project is configured \ |
| to search in repositories with the latest versions of its dependencies. |
| """, |
| category = Category.CORRECTNESS, |
| priority = 8, |
| severity = Severity.WARNING, |
| implementation = IMPLEMENTATION, |
| moreInfo = "https://developer.android.com/r/tools/jcenter-end-of-service", |
| ) |
| |
| @JvmField |
| val PLAY_SDK_INDEX_NON_COMPLIANT = |
| Issue.create( |
| id = "PlaySdkIndexNonCompliant", |
| briefDescription = "Library has policy issues in SDK Index", |
| explanation = |
| "This library version has policy issues that will block publishing in the Google Play Store.", |
| category = Category.COMPLIANCE, |
| priority = 8, |
| severity = Severity.ERROR, |
| implementation = IMPLEMENTATION_WITH_TOML, |
| moreInfo = GOOGLE_PLAY_SDK_INDEX_URL, |
| androidSpecific = true, |
| ) |
| |
| @JvmField |
| val PLAY_SDK_INDEX_GENERIC_ISSUES = |
| Issue.create( |
| id = "PlaySdkIndexGenericIssues", |
| briefDescription = "Library has issues in SDK Index", |
| explanation = |
| "This library version has issues that could block publishing in the Google Play Store.", |
| category = Category.COMPLIANCE, |
| priority = 8, |
| severity = Severity.ERROR, |
| implementation = IMPLEMENTATION_WITH_TOML, |
| moreInfo = GOOGLE_PLAY_SDK_INDEX_URL, |
| androidSpecific = true, |
| ) |
| |
| @JvmField |
| val CHROMEOS_ABI_SUPPORT = |
| Issue.create( |
| id = "ChromeOsAbiSupport", |
| briefDescription = "Missing ABI Support for ChromeOS", |
| explanation = |
| """ |
| To properly support ChromeOS, your Android application should have an x86 and/or x86_64 binary \ |
| as part of the build configuration. To fix the issue, ensure your files are properly optimized \ |
| for ARM; the binary translator will then ensure compatibility with x86. Alternatively, add an \ |
| `abiSplit` for x86 within your `build.gradle` file and create the required x86 dependencies. |
| """, |
| category = Category.CHROME_OS, |
| priority = 4, |
| severity = Severity.WARNING, |
| implementation = IMPLEMENTATION, |
| moreInfo = "https://developer.android.com/ndk/guides/abis", |
| androidSpecific = true, |
| ) |
| |
| /** Gradle plugin IDs based on the Java plugin. */ |
| val JAVA_PLUGIN_IDS = |
| listOf("java", "java-library", "application").flatMap { listOf(it, "org.gradle.$it") } |
| |
| /** The Gradle plugin ID for Android applications. */ |
| const val APP_PLUGIN_ID = "com.android.application" |
| |
| /** The Gradle plugin ID for Android libraries. */ |
| const val LIB_PLUGIN_ID = "com.android.library" |
| |
| /** Previous plugin id for applications. */ |
| const val OLD_APP_PLUGIN_ID = "android" |
| |
| /** Previous plugin id for libraries. */ |
| const val OLD_LIB_PLUGIN_ID = "android-library" |
| |
| /** All the plugin ids from the Android Gradle Plugin */ |
| val ALL_PLUGIN_IDS = |
| setOf( |
| "com.android.base", |
| "com.android.application", |
| "com.android.library", |
| "com.android.test", |
| "com.android.instant-app", |
| "com.android.feature", |
| "com.android.dynamic-feature", |
| "com.android.settings", |
| ) |
| |
| /** Group ID for GMS. */ |
| const val GMS_GROUP_ID = "com.google.android.gms" |
| |
| const val FIREBASE_GROUP_ID = "com.google.firebase" |
| const val GOOGLE_SUPPORT_GROUP_ID = "com.google.android.support" |
| const val ANDROID_WEAR_GROUP_ID = "com.google.android.wearable" |
| private const val WEARABLE_ARTIFACT_ID = "wearable" |
| |
| private val PLAY_SERVICES_V650 = Component.parse("$GMS_GROUP_ID:play-services:6.5.0") |
| |
| /** |
| * Threshold to consider a versionCode very high and issue a warning. |
| * https://developer.android.com/studio/publish/versioning.html indicates that the highest value |
| * accepted by Google Play is 2100000000. |
| */ |
| private const val VERSION_CODE_HIGH_THRESHOLD = 2000000000 |
| |
| /** Returns the best guess for where a dependency is declared in the given project. */ |
| fun getDependencyLocation(context: Context, c: LintModelMavenName): Location { |
| return getDependencyLocation(context, c.groupId, c.artifactId, c.version) |
| } |
| |
| /** Returns the best guess for where a dependency is declared in the given project. */ |
| fun getDependencyLocation( |
| context: Context, |
| groupId: String, |
| artifactId: String, |
| version: String, |
| ): Location { |
| val client = context.client |
| val projectDir = context.project.dir |
| val withoutQuotes = "$groupId:$artifactId:$version" |
| var location = guessGradleLocation(client, projectDir, withoutQuotes) |
| if (location.start != null) return location |
| // Try with just the group+artifact (relevant for example when using |
| // version variables) |
| location = guessGradleLocation(client, projectDir, "$groupId:$artifactId:") |
| if (location.start != null) return location |
| // Just the artifact -- important when using the other dependency syntax, |
| // e.g. variations of |
| // group: 'comh.android.support', name: 'support-v4', version: '21.0.+' |
| location = guessGradleLocation(client, projectDir, artifactId) |
| if (location.start != null) return location |
| // just the group: less precise but better than just the gradle file |
| location = guessGradleLocation(client, projectDir, groupId) |
| return location |
| } |
| |
| /** Returns the best guess for where two dependencies are declared in a project. */ |
| fun getDependencyLocation( |
| context: Context, |
| address1: LintModelMavenName, |
| address2: LintModelMavenName, |
| ): Location { |
| return getDependencyLocation( |
| context, |
| address1.groupId, |
| address1.artifactId, |
| address1.version, |
| address2.groupId, |
| address2.artifactId, |
| address2.version, |
| ) |
| } |
| |
| /** Returns the best guess for where two dependencies are declared in a project. */ |
| fun getDependencyLocation( |
| context: Context, |
| groupId1: String, |
| artifactId1: String, |
| version1: String, |
| groupId2: String, |
| artifactId2: String, |
| version2: String, |
| message: String? = null, |
| ): Location { |
| val location1 = getDependencyLocation(context, groupId1, artifactId1, version1) |
| val location2 = getDependencyLocation(context, groupId2, artifactId2, version2) |
| //noinspection FileComparisons |
| if (location2.start != null || location1.file != location2.file) { |
| location1.secondary = location2 |
| message?.let { location2.message = it } |
| } |
| return location1 |
| } |
| |
| fun getLatestVersionFromGradlePluginPortal( |
| client: LintClient, |
| pluginId: String, |
| filter: Predicate<Version>?, |
| allowPreview: Boolean, |
| ): Version? { |
| val pluginPath = pluginId.replace(".", "/") |
| val url = |
| "https://plugins.gradle.org/m2/$pluginPath/$pluginId.gradle.plugin/maven-metadata.xml" |
| val updates = |
| try { |
| readUrlDataAsString(client, url, 20000) |
| } catch (e: IOException) { |
| client.log( |
| null, |
| "Could not connect to %1\$s to get the latest available version for plugin %2\$s", |
| url, |
| pluginId, |
| ) |
| null |
| } |
| if ( |
| // for missing dependencies it answers with a json document |
| updates != null && !updates.startsWith("{") |
| ) { |
| val document = XmlUtils.parseDocumentSilently(updates, false) |
| if (document != null) { |
| val versionsList = document.getElementsByTagName("versions") |
| val versions = mutableListOf<Version>() |
| |
| for (i in 0 until versionsList.length) { |
| val element = versionsList.item(i) as Element |
| for (child in element) { |
| if (child.tagName == "version") { |
| val s = child.textContent |
| if (s.isNotBlank()) { |
| val revision = Version.parse(s) |
| versions.add(revision) |
| } |
| } |
| } |
| } |
| |
| return versions |
| .filter { filter == null || filter.test(it) } |
| .filter { allowPreview || !it.isPreview } |
| .maxOrNull() |
| } |
| } |
| return null |
| } |
| |
| /** TODO: Cache these results somewhere! */ |
| @JvmStatic |
| fun getLatestVersionFromRemoteRepo( |
| client: LintClient, |
| dependency: Dependency, |
| filter: Predicate<Version>?, |
| allowPreview: Boolean, |
| ): Version? { |
| val group = dependency.group ?: return null |
| val name = dependency.name |
| val richVersion = dependency.version ?: return null |
| val query = StringBuilder() |
| val encoding = UTF_8.name() |
| var allowPreview = allowPreview |
| try { |
| query.append("https://search.maven.org/solrsearch/select?q=g:%22") |
| query.append(URLEncoder.encode(group, encoding)) |
| query.append("%22+AND+a:%22") |
| query.append(URLEncoder.encode(name, encoding)) |
| } catch (e: UnsupportedEncodingException) { |
| return null |
| } |
| |
| query.append("%22&core=gav") |
| if (group == "com.google.guava" || name == "kotlinx-coroutines-core") { |
| // These libraries aren't releasing previews in their version strings; |
| // instead, the suffix is used to indicate different variants (JRE vs Android, |
| // JVM vs Kotlin Native). Turn on allowPreview for the search. |
| allowPreview = true |
| } else if (filter == null && allowPreview) { |
| query.append("&rows=1") |
| } |
| query.append("&wt=json") |
| |
| val response: String? |
| try { |
| response = readUrlDataAsString(client, query.toString(), 20000) |
| if (response == null) { |
| return null |
| } |
| } catch (e: IOException) { |
| client.log( |
| null, |
| "Could not connect to maven central to look up the latest " + |
| "available version for %1\$s", |
| dependency, |
| ) |
| return null |
| } |
| |
| // Sample response: |
| // { |
| // "responseHeader": { |
| // "status": 0, |
| // "QTime": 0, |
| // "params": { |
| // "fl": "id,g,a,v,p,ec,timestamp,tags", |
| // "sort": "score desc,timestamp desc,g asc,a asc,v desc", |
| // "indent": "off", |
| // "q": "g:\"com.google.guava\" AND a:\"guava\"", |
| // "core": "gav", |
| // "wt": "json", |
| // "rows": "1", |
| // "version": "2.2" |
| // } |
| // }, |
| // "response": { |
| // "numFound": 37, |
| // "start": 0, |
| // "docs": [{ |
| // "id": "com.google.guava:guava:17.0", |
| // "g": "com.google.guava", |
| // "a": "guava", |
| // "v": "17.0", |
| // "p": "bundle", |
| // "timestamp": 1398199666000, |
| // "tags": ["spec", "libraries", "classes", "google", "code"], |
| // "ec": ["-javadoc.jar", "-sources.jar", ".jar", "-site.jar", ".pom"] |
| // }] |
| // } |
| // } |
| |
| // Look for version info: This is just a cheap skim of the above JSON results. |
| var index = response.indexOf("\"response\"") |
| val versions = mutableListOf<Version>() |
| while (index != -1) { |
| index = response.indexOf("\"v\":", index) |
| if (index != -1) { |
| index += 4 |
| val start = response.indexOf('"', index) + 1 |
| val end = response.indexOf('"', start + 1) |
| if (start in 0 until end) { |
| val substring = response.substring(start, end) |
| val revision = Version.parse(substring) |
| if (revision != null) { |
| versions.add(revision) |
| } |
| } |
| } |
| } |
| |
| return versions |
| .filter { filter == null || filter.test(it) } |
| .filter { allowPreview || !it.isPreview } |
| .maxOrNull() |
| } |
| |
| private data class VersionCatalogDependency( |
| val coordinates: String, |
| val tomlValue: LintTomlValue, |
| ) |
| |
| /** |
| * For the given library reference [expression] in the "libs.some.library.name" format, returns |
| * the fully resolved coordinates of the library (including the version) and the corresponding |
| * library declaration value in the version catalog. |
| */ |
| private fun getDependencyFromVersionCatalog( |
| expression: String, |
| context: GradleContext, |
| ): VersionCatalogDependency? { |
| if (!expression.startsWith(VC_LIBRARY_PREFIX)) return null |
| |
| // Remove the "libs." prefix |
| val libName = expression.substring(VC_LIBRARY_PREFIX.length) |
| |
| // Find current library declaration in catalog, accounting for the declaration |
| // possibly using - and _ characters in the name |
| val library = |
| (context.getTomlValue(VC_LIBRARIES) as? LintTomlMapValue) |
| ?.getMappedValues() |
| ?.asIterable() |
| ?.find { it.key.replace('-', '.').replace('_', '.') == libName } |
| ?.value ?: return null |
| |
| // Find full coordinates of lib, including version |
| val versions = context.getTomlValue(VC_VERSIONS) as? LintTomlMapValue |
| val (coordinate, _) = getLibraryFromTomlEntry(versions, library) ?: return null |
| |
| return VersionCatalogDependency(coordinate, library) |
| } |
| |
| /** |
| * For the given plugin reference [expression] in the "libs.plugins.some.plugin.name" format, |
| * returns the fully resolved coordinates of the plugin (including the version) and the |
| * corresponding plugin declaration value in the version catalog. |
| */ |
| private fun getPluginFromVersionCatalog( |
| expression: String, |
| context: GradleContext, |
| ): VersionCatalogDependency? { |
| if (!expression.startsWith(VC_PLUGIN_PREFIX)) return null |
| |
| // Remove the "libs.plugins." prefix |
| val pluginName = expression.substring(VC_PLUGIN_PREFIX.length) |
| |
| // Find current plugin declaration in catalog, accounting for the declaration |
| // possibly using - and _ characters in the name |
| val plugin = |
| (context.getTomlValue(VC_PLUGINS) as? LintTomlMapValue) |
| ?.getMappedValues() |
| ?.asIterable() |
| ?.find { it.key.replace('-', '.').replace('_', '.') == pluginName } |
| ?.value ?: return null |
| |
| // Find full coordinates of plugin, including version |
| val versions = context.getTomlValue(VC_VERSIONS) as? LintTomlMapValue |
| val (coordinate, _) = getPluginFromTomlEntry(versions, plugin) ?: return null |
| |
| return VersionCatalogDependency(coordinate, plugin) |
| } |
| |
| // Convert a long-hand dependency, like |
| // group: 'com.android.support', name: 'support-v4', version: '21.0.+' |
| // into an equivalent short-hand dependency, like |
| // com.android.support:support-v4:21.0.+ |
| @JvmStatic |
| fun getNamedDependency(expression: String): String? { |
| // if (value.startsWith("group: 'com.android.support', name: 'support-v4', version: |
| // '21.0.+'")) |
| if (expression.indexOf(',') != -1 && expression.contains("version:")) { |
| var artifact: String? = null |
| var group: String? = null |
| var version: String? = null |
| val splitter = Splitter.on(',').omitEmptyStrings().trimResults() |
| for (property in splitter.split(expression)) { |
| val colon = property.indexOf(':') |
| if (colon == -1) { |
| return null |
| } |
| var quote = '\'' |
| var valueStart = property.indexOf(quote, colon + 1) |
| if (valueStart == -1) { |
| quote = '"' |
| valueStart = property.indexOf(quote, colon + 1) |
| } |
| if (valueStart == -1) { |
| // For example, "transitive: false" |
| continue |
| } |
| valueStart++ |
| val valueEnd = property.indexOf(quote, valueStart) |
| if (valueEnd == -1) { |
| return null |
| } |
| val value = property.substring(valueStart, valueEnd) |
| when { |
| property.startsWith("group:") -> group = value |
| property.startsWith("name:") -> artifact = value |
| property.startsWith("version:") -> version = value |
| } |
| } |
| |
| if (artifact != null && group != null && version != null) { |
| return "$group:$artifact:$version" |
| } |
| } |
| |
| return null |
| } |
| |
| private fun suggestApiConfigurationUse(project: Project, configuration: String): Boolean { |
| return when { |
| configuration.startsWith("test") || configuration.startsWith("androidTest") -> false |
| else -> |
| when (project.type) { |
| LintModelModuleType.APP -> |
| // Applications can only generally be consumed if there are dynamic features |
| // (Ignoring the test-only project for this purpose) |
| project.hasDynamicFeatures() |
| LintModelModuleType.LIBRARY -> true |
| LintModelModuleType.JAVA_LIBRARY -> true |
| LintModelModuleType.FEATURE, |
| LintModelModuleType.DYNAMIC_FEATURE -> true |
| LintModelModuleType.TEST -> false |
| LintModelModuleType.INSTANT_APP -> false |
| } |
| } |
| } |
| |
| private fun targetJava8Plus(project: Project): Boolean { |
| return getLanguageLevel(project, JDK_1_7).isAtLeast(JDK_1_8) |
| } |
| |
| private fun hasLifecycleAnnotationProcessor(dependency: String) = |
| dependency.contains("android.arch.lifecycle:compiler") || |
| dependency.contains("androidx.lifecycle:lifecycle-compiler") |
| |
| private fun isCommonAnnotationProcessor(dependency: String): Boolean = |
| when (val index = dependency.lastIndexOf(":")) { |
| -1 -> false |
| else -> dependency.substring(0, index) in commonAnnotationProcessors |
| } |
| |
| private enum class CompileConfiguration(private val compileConfigName: String) { |
| API("api"), |
| COMPILE("compile"), |
| IMPLEMENTATION("implementation"), |
| COMPILE_ONLY("compileOnly"); |
| |
| private val annotationProcessor = "annotationProcessor" |
| private val compileConfigSuffix = compileConfigName.usLocaleCapitalize() |
| |
| fun matches(configurationName: String): Boolean { |
| return configurationName == compileConfigName || |
| configurationName.endsWith(compileConfigSuffix) |
| } |
| |
| fun replacement(configurationName: String): String { |
| return if (configurationName == compileConfigName) { |
| annotationProcessor |
| } else { |
| configurationName.removeSuffix(compileConfigSuffix).appendCapitalized(annotationProcessor) |
| } |
| } |
| } |
| |
| private val commonAnnotationProcessors: Set<String> = |
| setOf( |
| "com.jakewharton:butterknife-compiler", |
| "com.github.bumptech.glide:compiler", |
| "androidx.databinding:databinding-compiler", |
| "com.google.dagger:dagger-compiler", |
| "com.google.auto.service:auto-service", |
| "android.arch.persistence.room:compiler", |
| "android.arch.lifecycle:compiler", |
| "io.realm:realm-annotations-processor", |
| "com.google.dagger:dagger-android-processor", |
| "androidx.room:room-compiler", |
| "com.android.databinding:compiler", |
| "androidx.lifecycle:lifecycle-compiler", |
| "org.projectlombok:lombok", |
| "com.google.auto.value:auto-value", |
| "org.parceler:parceler", |
| "com.github.hotchemi:permissionsdispatcher-processor", |
| "com.alibaba:arouter-compiler", |
| "org.androidannotations:androidannotations", |
| "com.github.Raizlabs.DBFlow:dbflow-processor", |
| "frankiesardo:icepick-processor", |
| "org.greenrobot:eventbus-annotation-processor", |
| "com.ryanharter.auto.value:auto-value-gson", |
| "io.objectbox:objectbox-processor", |
| "com.arello-mobile:moxy-compiler", |
| "com.squareup.dagger:dagger-compiler", |
| "io.realm:realm-android", |
| "com.bluelinelabs:logansquare-compiler", |
| "com.tencent.tinker:tinker-android-anno", |
| "com.raizlabs.android:DBFlow-Compiler", |
| "com.google.auto.factory:auto-factory", |
| "com.airbnb:deeplinkdispatch-processor", |
| "com.alipay.android.tools:androidannotations", |
| "org.permissionsdispatcher:permissionsdispatcher-processor", |
| "com.airbnb.android:epoxy-processor", |
| "org.immutables:value", |
| "com.github.stephanenicolas.toothpick:toothpick-compiler", |
| "com.mindorks.android:placeholderview-compiler", |
| "com.github.frankiesardo:auto-parcel-processor", |
| "com.hannesdorfmann.fragmentargs:processor", |
| "com.evernote:android-state-processor", |
| "org.mapstruct:mapstruct-processor", |
| "com.iqiyi.component.router:qyrouter-compiler", |
| "com.iqiyi.component.mm:mm-compiler", |
| "dk.ilios:realmfieldnameshelper", |
| "com.lianjia.common.android.router2:compiler", |
| "com.smile.gifshow.annotation:invoker_processor", |
| "com.f2prateek.dart:dart-processor", |
| "com.sankuai.waimai.router:compiler", |
| "org.qiyi.card:card-action-compiler", |
| "com.iqiyi.video:eventbus-annotation-processor", |
| "ly.img.android.pesdk:build-processor", |
| "org.apache.logging.log4j:log4j-core", |
| "com.github.jokermonn:permissions4m", |
| "com.arialyy.aria:aria-compiler", |
| "com.smile.gifshow.annotation:provide_processor", |
| "com.smile.gifshow.annotation:preference_processor", |
| "com.smile.gifshow.annotation:plugin_processor", |
| "org.inferred:freebuilder", |
| "com.smile.gifshow.annotation:router_processor", |
| ) |
| |
| // From https://kotlinlang.org/docs/ksp-overview.html#supported-libraries |
| private val annotationProcessorsWithKspReplacements: Map<String, String> = |
| mapOf( |
| // Note: this is the only dependency where coordinates actually have to be updated |
| "com.github.bumptech.glide:compiler" to "com.github.bumptech.glide:ksp", |
| "androidx.room:room-compiler" to "androidx.room:room-compiler", |
| "com.squareup.moshi:moshi-kotlin-codegen" to "com.squareup.moshi:moshi-kotlin-codegen", |
| "com.github.liujingxing.rxhttp:rxhttp-compiler" to |
| "com.github.liujingxing.rxhttp:rxhttp-compiler", |
| "se.ansman.kotshi:compiler" to "se.ansman.kotshi:compiler", |
| "com.linecorp.lich:savedstate-compiler" to "com.linecorp.lich:savedstate-compiler", |
| "io.github.amrdeveloper:easyadapter-compiler" to |
| "io.github.amrdeveloper:easyadapter-compiler", |
| "com.airbnb:deeplinkdispatch-processor" to "com.airbnb:deeplinkdispatch-processor", |
| "com.airbnb.android:epoxy-processor" to "com.airbnb.android:epoxy-processor", |
| "com.airbnb.android:paris-processor" to "com.airbnb.android:paris-processor", |
| ) |
| |
| private val commonBoms: Set<String> = |
| setOf( |
| // Google |
| "androidx.compose:compose-bom", |
| "com.google.firebase:firebase-bom", |
| // JetBrains |
| "org.jetbrains.kotlin:kotlin-bom", |
| "org.jetbrains.kotlinx:kotlinx-coroutines-bom", |
| "io.ktor:ktor-bom", |
| // Network and serialization |
| "com.squareup.okio:okio-bom", |
| "com.squareup.okhttp3:okhttp-bom", |
| "com.squareup.wire:wire-bom", |
| "com.fasterxml.jackson:jackson-bom", |
| "io.grpc:grpc-bom", |
| "org.http4k:http4k-bom", |
| "org.http4k:http4k-connect-bom", |
| // Testing |
| "org.junit:junit-bom", |
| "io.kotest:kotest-bom", |
| "io.cucumber:cucumber-bom", |
| // Others |
| "io.arrow-kt:arrow-stack", |
| "io.sentry:sentry-bom", |
| "dev.chrisbanes.compose:compose-bom", |
| "org.ow2.asm:asm-bom", |
| "software.amazon.awssdk:bom", |
| "com.walletconnect:android-bom", |
| ) |
| |
| private fun libraryHasKtxExtension(mavenName: String): Boolean { |
| // From https://developer.android.com/kotlin/ktx/extensions-list. |
| return when (mavenName) { |
| "androidx.activity:activity", |
| "androidx.collection:collection", |
| "androidx.core:core", |
| "androidx.dynamicanimation:dynamicanimation", |
| "androidx.fragment:fragment", |
| "androidx.lifecycle:lifecycle-livedata-core", |
| "androidx.lifecycle:lifecycle-livedata", |
| "androidx.lifecycle:lifecycle-reactivestreams", |
| "androidx.lifecycle:lifecycle-runtime", |
| "androidx.lifecycle:lifecycle-viewmodel", |
| "androidx.navigation:navigation-runtime", |
| "androidx.navigation:navigation-fragment", |
| "androidx.navigation:navigation-ui", |
| "androidx.paging:paging-common", |
| "androidx.paging:paging-runtime", |
| "androidx.paging:paging-rxjava2", |
| "androidx.palette:palette", |
| "androidx.preference:preference", |
| "androidx.slice:slice-builders", |
| "androidx.sqlite:sqlite", |
| "com.google.android.play:core" -> true |
| else -> false |
| } |
| } |
| |
| @JvmStatic |
| var playSdkIndexFactory: (Path?, LintClient) -> GooglePlaySdkIndex = |
| { path: Path?, client: LintClient -> |
| val index = |
| object : GooglePlaySdkIndex(path) { |
| public override fun readUrlData(url: String, timeout: Int, lastModified: Long) = |
| readUrlData(client, url, timeout, lastModified) |
| |
| override fun error(throwable: Throwable, message: String?) { |
| client.log(throwable, message) |
| } |
| } |
| index.initialize() |
| index |
| } |
| } |
| } |
| |
| private infix fun <T : Comparable<T>> T?.maxOrNull(other: T?): T? = |
| when { |
| this == null -> other |
| other == null -> this |
| else -> if (this > other) this else other |
| } |
| |
| private infix fun Version?.maxAgpOrNull(other: Version?): Version? = |
| (this?.let { AgpVersion.tryParse(it.toString()) } maxOrNull |
| other?.let { AgpVersion.tryParse(it.toString()) }) |
| ?.let { Version.parse(it.toString()) } |
| |
| /** |
| * This exists to smooth over the fact that we represent the Version of a prefix matcher as the |
| * least possible version that would match, but we want here to find newer versions that would not |
| * match (e.g. if [dependency] has a version specification of 1.0.+ we should return false for a |
| * [Version] of 1.0.2, but true for a [Version] of 1.1.0. |
| * |
| * A clearer implementation fix for this is to have two Version getters for GradleCoordinate: |
| * getLowerBoundVersion and getUpperBoundVersion (both of which are computable) and to use the |
| * appropriate one in the right context (in most of this file, the upper bound). |
| */ |
| private fun Version?.isNewerThan(dependency: Dependency): Boolean { |
| val richVersion = dependency.version |
| val maybeSingleton = dependency.explicitSingletonVersion |
| return when { |
| this == null -> false |
| richVersion == null -> true |
| maybeSingleton != null -> this > maybeSingleton |
| richVersion.lowerBound > this -> false |
| else -> !richVersion.contains(this) |
| } |
| } |
| |
| private fun Version?.isAgpNewerThan(dependency: Dependency): Boolean { |
| val richVersion = dependency.version |
| val maybeSingleton = |
| dependency.explicitSingletonVersion?.let { AgpVersion.tryParse(it.toString()) } |
| val thisAgpVersion = this?.let { AgpVersion.tryParse(it.toString()) } |
| val lowerBoundAgpVersion = richVersion?.lowerBound?.let { AgpVersion.tryParse(it.toString()) } |
| return when { |
| this == null || thisAgpVersion == null -> false |
| richVersion == null -> true |
| maybeSingleton != null -> thisAgpVersion > maybeSingleton |
| // these are an approximation. If we can parse the version's lower bound as |
| // an AgpVersion, good, use it to compare; it's probably a pseudo-singleton. |
| // If we can't, then fall back to the Gradle-based version containment logic. |
| lowerBoundAgpVersion != null && lowerBoundAgpVersion > thisAgpVersion -> false |
| else -> !richVersion.contains(this) |
| } |
| } |