Lint: fix some more detectors
This commit is contained in:
@@ -75,7 +75,6 @@
|
||||
<globalInspection hasStaticDescription="true" shortName="AndroidKLintStringFormatMatches" displayName="String.format string doesn't match the XML format string" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="ERROR" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintStringFormatMatchesInspection"/>
|
||||
<globalInspection hasStaticDescription="true" shortName="AndroidKLintSuspiciousImport" displayName="'import android.R' statement" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintSuspiciousImportInspection"/>
|
||||
<globalInspection hasStaticDescription="true" shortName="AndroidKLintUnlocalizedSms" displayName="SMS phone number missing country code" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintUnlocalizedSmsInspection"/>
|
||||
<globalInspection hasStaticDescription="true" shortName="AndroidKLintUnusedAttribute" displayName="Attribute unused on older versions" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintUnusedAttributeInspection"/>
|
||||
<globalInspection hasStaticDescription="true" shortName="AndroidKLintUnusedIds" displayName="Unused id" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="false" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintUnusedIdsInspection"/>
|
||||
<globalInspection hasStaticDescription="true" shortName="AndroidKLintUnusedResources" displayName="Unused resources" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintUnusedResourcesInspection"/>
|
||||
<globalInspection hasStaticDescription="true" shortName="AndroidKLintUseSparseArrays" displayName="HashMap can be replaced with SparseArray" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintUseSparseArraysInspection"/>
|
||||
|
||||
@@ -784,7 +784,7 @@ public class LintUtils {
|
||||
|
||||
boolean imported = false;
|
||||
for (UImportStatement importStatement : compilationUnit.getImportStatements()) {
|
||||
String fqn = importStatement.getNameToImport();
|
||||
String fqn = importStatement.getFqNameToImport();
|
||||
if (fqn == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -13,5 +13,6 @@
|
||||
<orderEntry type="library" name="guava" level="project" />
|
||||
<orderEntry type="module" module-name="uast-android" />
|
||||
<orderEntry type="library" name="intellij-core" level="project" />
|
||||
<orderEntry type="library" name="kotlin-runtime" level="project" />
|
||||
</component>
|
||||
</module>
|
||||
File diff suppressed because it is too large
Load Diff
+434
@@ -0,0 +1,434 @@
|
||||
/*
|
||||
* Copyright (C) 2012 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.klint.checks
|
||||
|
||||
import com.android.SdkConstants.TARGET_API
|
||||
import com.android.annotations.NonNull
|
||||
import com.android.sdklib.SdkVersionInfo
|
||||
import com.android.tools.klint.client.api.IssueRegistry
|
||||
import com.android.tools.klint.detector.api.*
|
||||
import org.jetbrains.uast.*
|
||||
import org.jetbrains.uast.check.UastAndroidContext
|
||||
import org.jetbrains.uast.check.UastScanner
|
||||
import org.jetbrains.uast.visitor.UastVisitor
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Looks for usages of APIs that are not supported in all the versions targeted
|
||||
* by this application (according to its minimum API requirement in the manifest).
|
||||
*/
|
||||
open class ApiDetector : Detector(), UastScanner {
|
||||
|
||||
protected var mApiDatabase: ApiLookup? = null
|
||||
private var mWarnedMissingDb: Boolean = false
|
||||
|
||||
@NonNull
|
||||
override fun getSpeed(): Speed {
|
||||
return Speed.SLOW
|
||||
}
|
||||
|
||||
override fun beforeCheckProject(@NonNull context: Context) {
|
||||
mApiDatabase = ApiLookup.get(context.client)
|
||||
|
||||
if (mApiDatabase == null && !mWarnedMissingDb) {
|
||||
mWarnedMissingDb = true
|
||||
context.report(IssueRegistry.LINT_ERROR, Location.create(context.file),
|
||||
"Can't find API database; API check not performed")
|
||||
}
|
||||
}
|
||||
|
||||
override fun createUastVisitor(context: UastAndroidContext): UastVisitor {
|
||||
return ApiVersionVisitor(context)
|
||||
}
|
||||
|
||||
private inner class ApiVersionVisitor(val context: UastAndroidContext) : UastVisitor() {
|
||||
private var mMinApi = -1
|
||||
|
||||
override fun visitCallExpression(node: UCallExpression): Boolean {
|
||||
when (node.kind) {
|
||||
UastCallKind.FUNCTION_CALL -> checkVersion(context, node, node.functionReference?.resolve(context))
|
||||
UastCallKind.CONSTRUCTOR_CALL -> checkVersion(context, node, node.classReference?.resolve(context))
|
||||
}
|
||||
|
||||
return super.visitCallExpression(node)
|
||||
}
|
||||
|
||||
override fun visitSimpleReferenceExpression(node: USimpleReferenceExpression): Boolean {
|
||||
checkVersion(context, node, node.resolve(context) as? UVariable)
|
||||
return super.visitSimpleReferenceExpression(node)
|
||||
}
|
||||
|
||||
override fun visitClass(node: UClass): Boolean {
|
||||
val nameElement = node.nameElement
|
||||
|
||||
if (nameElement != null) {
|
||||
for (type in node.superTypes) {
|
||||
checkVersion(context, nameElement, type.resolveClass(context))
|
||||
}
|
||||
}
|
||||
|
||||
return super.visitClass(node)
|
||||
}
|
||||
|
||||
override fun visitClassLiteralExpression(node: UClassLiteralExpression): Boolean {
|
||||
val clazz = node.type.resolve(context)
|
||||
if (clazz != null) {
|
||||
checkVersion(context, node, clazz)
|
||||
}
|
||||
return super.visitClassLiteralExpression(node)
|
||||
}
|
||||
|
||||
override fun visitFunction(node: UFunction): Boolean {
|
||||
if (!node.hasModifier(UastModifier.OVERRIDE)) {
|
||||
val parentClass = node.parent as? UClass ?: return false
|
||||
|
||||
val db = mApiDatabase ?: return false
|
||||
val desc = node.bytecodeDescriptor ?: return false
|
||||
|
||||
val buildSdk = context.lintContext.getMainProject().getBuildSdk()
|
||||
if (buildSdk == -1) return false
|
||||
|
||||
for (type in parentClass.superTypes) {
|
||||
val clazz = type.resolve(context) ?: continue
|
||||
if (!isSdkClass(clazz)) continue
|
||||
val internalName = clazz.internalName ?: continue
|
||||
|
||||
val methodSdkLevel = db.getCallVersion(internalName, node.name, desc)
|
||||
if (methodSdkLevel != -1 && methodSdkLevel > buildSdk) {
|
||||
val message = "This method is not overriding anything with the current build " +
|
||||
"target, but will in API level $methodSdkLevel (current target is $buildSdk): `${node.name}`"
|
||||
context.report(OVERRIDE, node, context.getLocation(node.nameElement), message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun checkIfSpecialCase(declaration: UDeclaration, owner: UClass): Boolean {
|
||||
if (declaration is UVariable && owner.fqName == "android.os.Build.VERSION_CODES") return true
|
||||
if (declaration.name == "MATCH_PARENT" && owner.fqName == "android.view.ViewGroup.LayoutParams") return true
|
||||
if ((declaration.name == "CHOICE_MODE_NONE" || declaration.name == "CHOICE_MODE_MULTIPLE"
|
||||
|| declaration.name == "CHOICE_MODE_SINGLE") && owner.fqName == "android.widget.AbsListView") return true
|
||||
if ((declaration.name == "START" || declaration.name == "END") && owner.fqName == "android.view.Gravity") return true
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun checkVersion(context: UastAndroidContext, node: UElement, declaration: UDeclaration?) {
|
||||
if (declaration == null) return
|
||||
val db = mApiDatabase ?: return
|
||||
|
||||
val projectMinSdk = getMinSdk(context)
|
||||
if (projectMinSdk == -1) return
|
||||
|
||||
fun check(declarationSdkLevel: Int, parentClass: UClass?) {
|
||||
if (declarationSdkLevel == -1) return
|
||||
if (parentClass != null && checkIfSpecialCase(declaration, parentClass)) return
|
||||
if (isCheckedExplicitly(context, declarationSdkLevel, node)) return
|
||||
|
||||
if (projectMinSdk < declarationSdkLevel) {
|
||||
val subject = when (declaration) {
|
||||
is UFunction -> "Call"
|
||||
is UVariable -> "Field"
|
||||
is UClass -> "Class"
|
||||
else -> "Call"
|
||||
}
|
||||
val message = "$subject requires API level $declarationSdkLevel" +
|
||||
" (current min is $projectMinSdk): `${declaration.name}`"
|
||||
context.report(UNSUPPORTED, node, context.getLocation(node), message)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fun checkAosp(clazz: UClass) = AOSP_BUILD && clazz.fqName?.startsWith("android.support.") ?: false
|
||||
|
||||
if (declaration is UClass) {
|
||||
if (!isSdkClass(declaration) || checkAosp(declaration)) return
|
||||
val internalName = declaration.internalName ?: return
|
||||
check(db.getClassVersion(internalName), null)
|
||||
}
|
||||
|
||||
val parentClass = declaration.parent as? UClass ?: return
|
||||
if (!isSdkClass(parentClass) || checkAosp(parentClass)) return
|
||||
val parentInternalName = parentClass.internalName ?: return
|
||||
|
||||
when (declaration) {
|
||||
is UFunction -> {
|
||||
val descriptor = declaration.bytecodeDescriptor ?: return
|
||||
check(db.getCallVersion(parentInternalName, declaration.name, descriptor), parentClass)
|
||||
}
|
||||
is UVariable -> {
|
||||
if (declaration.kind != UastVariableKind.MEMBER) return
|
||||
check(db.getFieldVersion(parentInternalName, declaration.name), parentClass)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isSdkClass(clazz: UClass): Boolean {
|
||||
val fqName = clazz.fqName ?: return false
|
||||
return fqName.startsWith("android.")
|
||||
|| fqName.startsWith("java.")
|
||||
|| fqName.startsWith("javax.")
|
||||
|| fqName.startsWith("dalvik.")
|
||||
}
|
||||
|
||||
protected fun getMinSdk(context: UastAndroidContext): Int {
|
||||
if (mMinApi == -1) {
|
||||
val minSdkVersion = context.lintContext.mainProject.minSdkVersion
|
||||
mMinApi = minSdkVersion.featureLevel
|
||||
}
|
||||
|
||||
return mMinApi
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val AOSP_BUILD = System.getenv("ANDROID_BUILD_TOP") != null
|
||||
private val SDK_INT_FQNAME = "android.os.Build.VERSION.SDK_INT"
|
||||
|
||||
tailrec fun getLocalMinSdk(scope: UElement?): Int {
|
||||
if (scope == null) return -1
|
||||
|
||||
if (scope is UAnnotated) {
|
||||
val targetApi = getTargetApi(scope.annotations)
|
||||
if (targetApi != -1) return targetApi
|
||||
}
|
||||
|
||||
return getLocalMinSdk(scope.parent)
|
||||
}
|
||||
|
||||
fun getTargetApi(annotations: List<UAnnotation>): Int {
|
||||
for (annotation in annotations) {
|
||||
if (annotation.matchesName(TARGET_API)) {
|
||||
for (element in annotation.valueArguments) {
|
||||
val valueNode = element.expression
|
||||
if (valueNode.isIntegralLiteral()) {
|
||||
return (valueNode as ULiteralExpression).getLongValue().toInt()
|
||||
} else if (valueNode.isStringLiteral()) {
|
||||
val value = (valueNode as ULiteralExpression).value as String
|
||||
return SdkVersionInfo.getApiByBuildCode(value, true)
|
||||
} else if (valueNode is UQualifiedExpression) {
|
||||
val codename = valueNode.selector.renderString()
|
||||
return SdkVersionInfo.getApiByBuildCode(codename, true)
|
||||
} else if (valueNode is USimpleReferenceExpression) {
|
||||
val codename = valueNode.identifier;
|
||||
return SdkVersionInfo.getApiByBuildCode(codename, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
fun isCheckedExplicitly(context: UastAndroidContext, requiredVersion: Int, node: UElement): Boolean {
|
||||
tailrec fun UExpression.isSdkIntReference(): Boolean = when (this) {
|
||||
is UParenthesizedExpression -> expression.isSdkIntReference()
|
||||
is USimpleReferenceExpression -> resolve(context)?.fqName == SDK_INT_FQNAME
|
||||
is UQualifiedExpression -> resolve(context)?.fqName == SDK_INT_FQNAME
|
||||
else -> false
|
||||
}
|
||||
|
||||
fun checkCondition(node: UExpression, invertCondition: Boolean = false): Boolean? {
|
||||
if (node is UBinaryExpression && node.operator is UastBinaryOperator.ComparationOperator) {
|
||||
var invert: Boolean = invertCondition
|
||||
|
||||
val value: UExpression
|
||||
if (node.leftOperand.isSdkIntReference()) {
|
||||
value = node.rightOperand
|
||||
}
|
||||
else if (node.rightOperand.isSdkIntReference()) {
|
||||
invert = !invert
|
||||
value = node.leftOperand
|
||||
}
|
||||
else return false
|
||||
fun inv(cond: Boolean) = if (invert) !cond else cond
|
||||
|
||||
tailrec fun evaluateValue(value: UExpression?): Int? {
|
||||
if (value == null) return null
|
||||
|
||||
return (value.evaluate() as? Number)?.toInt() ?: if (value is UResolvable) {
|
||||
val declaration = value.resolve(context) ?: return null
|
||||
if (declaration is UVariable)
|
||||
evaluateValue(declaration.initializer)
|
||||
else
|
||||
null
|
||||
} else null
|
||||
}
|
||||
|
||||
val sdkLevel = evaluateValue(value) ?: return null
|
||||
|
||||
return when (node.operator) {
|
||||
UastBinaryOperator.GREATER -> inv(sdkLevel > requiredVersion)
|
||||
UastBinaryOperator.GREATER_OR_EQUAL -> sdkLevel == requiredVersion || inv(sdkLevel > requiredVersion)
|
||||
UastBinaryOperator.LESS -> inv(sdkLevel < requiredVersion)
|
||||
UastBinaryOperator.LESS_OR_EQUAL -> sdkLevel == requiredVersion || inv(sdkLevel < requiredVersion)
|
||||
UastBinaryOperator.EQUALS -> requiredVersion == sdkLevel
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
return when (node) {
|
||||
is UBinaryExpression -> if (node.operator is UastBinaryOperator.LogicalOperator) {
|
||||
checkCondition(node.leftOperand) ?: checkCondition(node.rightOperand)
|
||||
} else null
|
||||
is UUnaryExpression -> if (node.operator == UastPrefixOperator.LOGICAL_NOT) {
|
||||
checkCondition(node.operand, true)
|
||||
} else null
|
||||
is UParenthesizedExpression -> checkCondition(node.expression)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
tailrec fun check(node: UElement?, prev: UElement?, context: UastAndroidContext): Boolean {
|
||||
return when (node) {
|
||||
null -> false
|
||||
is UIfExpression -> {
|
||||
val cond = checkCondition(node.condition) ?: return false
|
||||
if ((cond && prev == node.thenBranch) || (!cond && prev == node.elseBranch))
|
||||
true
|
||||
else
|
||||
check(node.parent, node, context)
|
||||
}
|
||||
is UExpressionSwitchClauseExpression -> {
|
||||
if (checkCondition(node.caseValue) == true)
|
||||
true
|
||||
else
|
||||
check(node.parent, node, context)
|
||||
}
|
||||
is UBinaryExpression -> {
|
||||
if (prev == node.rightOperand
|
||||
&& node.operator is UastBinaryOperator.LogicalOperator
|
||||
&& checkCondition(node.leftOperand) == true)
|
||||
true
|
||||
else
|
||||
check(node.parent, node, context)
|
||||
}
|
||||
is UFunction -> false
|
||||
is UVariable -> if (node.kind == UastVariableKind.MEMBER) false else check(node.parent, node, context)
|
||||
is UClass -> false
|
||||
else -> check(node.parent, node, context)
|
||||
}
|
||||
}
|
||||
|
||||
val minSdk = getLocalMinSdk(node)
|
||||
if (minSdk != -1 && minSdk >= requiredVersion) return true
|
||||
|
||||
return check(node, null, context)
|
||||
}
|
||||
|
||||
/** Accessing an unsupported API */
|
||||
@SuppressWarnings("unchecked")
|
||||
@JvmField
|
||||
val UNSUPPORTED = Issue.create(
|
||||
"NewApi", //$NON-NLS-1$
|
||||
"Calling new methods on older versions",
|
||||
|
||||
"This check scans through all the Android API calls in the application and " +
|
||||
"warns about any calls that are not available on *all* versions targeted " +
|
||||
"by this application (according to its minimum SDK attribute in the manifest).\n" +
|
||||
"\n" +
|
||||
"If you really want to use this API and don't need to support older devices just " +
|
||||
"set the `minSdkVersion` in your `build.gradle` or `AndroidManifest.xml` files.\n" +
|
||||
"\n" +
|
||||
"If your code is *deliberately* accessing newer APIs, and you have ensured " +
|
||||
"(e.g. with conditional execution) that this code will only ever be called on a " +
|
||||
"supported platform, then you can annotate your class or method with the " +
|
||||
"`@TargetApi` annotation specifying the local minimum SDK to apply, such as " +
|
||||
"`@TargetApi(11)`, such that this check considers 11 rather than your manifest " +
|
||||
"file's minimum SDK as the required API level.\n" +
|
||||
"\n" +
|
||||
"If you are deliberately setting `android:` attributes in style definitions, " +
|
||||
"make sure you place this in a `values-vNN` folder in order to avoid running " +
|
||||
"into runtime conflicts on certain devices where manufacturers have added " +
|
||||
"custom attributes whose ids conflict with the new ones on later platforms.\n" +
|
||||
"\n" +
|
||||
"Similarly, you can use tools:targetApi=\"11\" in an XML file to indicate that " +
|
||||
"the element will only be inflated in an adequate context.",
|
||||
Category.CORRECTNESS,
|
||||
6,
|
||||
Severity.ERROR,
|
||||
Implementation(
|
||||
ApiDetector::class.java,
|
||||
EnumSet.of(Scope.CLASS_FILE, Scope.RESOURCE_FILE, Scope.MANIFEST),
|
||||
Scope.RESOURCE_FILE_SCOPE,
|
||||
Scope.CLASS_FILE_SCOPE,
|
||||
Scope.MANIFEST_SCOPE))
|
||||
|
||||
/** Accessing an inlined API on older platforms */
|
||||
@JvmField
|
||||
val INLINED = Issue.create(
|
||||
"InlinedApi", //$NON-NLS-1$
|
||||
"Using inlined constants on older versions",
|
||||
|
||||
"This check scans through all the Android API field references in the application " +
|
||||
"and flags certain constants, such as static final integers and Strings, " +
|
||||
"which were introduced in later versions. These will actually be copied " +
|
||||
"into the class files rather than being referenced, which means that " +
|
||||
"the value is available even when running on older devices. In some " +
|
||||
"cases that's fine, and in other cases it can result in a runtime " +
|
||||
"crash or incorrect behavior. It depends on the context, so consider " +
|
||||
"the code carefully and device whether it's safe and can be suppressed " +
|
||||
"or whether the code needs tbe guarded.\n" +
|
||||
"\n" +
|
||||
"If you really want to use this API and don't need to support older devices just " +
|
||||
"set the `minSdkVersion` in your `build.gradle` or `AndroidManifest.xml` files." +
|
||||
"\n" +
|
||||
"If your code is *deliberately* accessing newer APIs, and you have ensured " +
|
||||
"(e.g. with conditional execution) that this code will only ever be called on a " +
|
||||
"supported platform, then you can annotate your class or method with the " +
|
||||
"`@TargetApi` annotation specifying the local minimum SDK to apply, such as " +
|
||||
"`@TargetApi(11)`, such that this check considers 11 rather than your manifest " +
|
||||
"file's minimum SDK as the required API level.\n",
|
||||
Category.CORRECTNESS,
|
||||
6,
|
||||
Severity.WARNING,
|
||||
Implementation(
|
||||
ApiDetector::class.java,
|
||||
Scope.JAVA_FILE_SCOPE))
|
||||
|
||||
/** Accessing an unsupported API */
|
||||
@JvmField
|
||||
val OVERRIDE = Issue.create(
|
||||
"Override", //$NON-NLS-1$
|
||||
"Method conflicts with new inherited method",
|
||||
|
||||
"Suppose you are building against Android API 8, and you've subclassed Activity. " +
|
||||
"In your subclass you add a new method called `isDestroyed`(). At some later point, " +
|
||||
"a method of the same name and signature is added to Android. Your method will " +
|
||||
"now override the Android method, and possibly break its contract. Your method " +
|
||||
"is not calling `super.isDestroyed()`, since your compilation target doesn't " +
|
||||
"know about the method.\n" +
|
||||
"\n" +
|
||||
"The above scenario is what this lint detector looks for. The above example is " +
|
||||
"real, since `isDestroyed()` was added in API 17, but it will be true for *any* " +
|
||||
"method you have added to a subclass of an Android class where your build target " +
|
||||
"is lower than the version the method was introduced in.\n" +
|
||||
"\n" +
|
||||
"To fix this, either rename your method, or if you are really trying to augment " +
|
||||
"the builtin method if available, switch to a higher build target where you can " +
|
||||
"deliberately add `@Override` on your overriding method, and call `super` if " +
|
||||
"appropriate etc.\n",
|
||||
Category.CORRECTNESS,
|
||||
6,
|
||||
Severity.ERROR,
|
||||
Implementation(
|
||||
ApiDetector::class.java,
|
||||
Scope.JAVA_FILE_SCOPE))
|
||||
}
|
||||
}
|
||||
+12
-13
@@ -201,9 +201,9 @@ public class JavaPerformanceDetector extends Detector implements UastScanner {
|
||||
// TODO: Should we handle factory method constructions of HashMaps as well,
|
||||
// e.g. via Guava? This is a bit trickier since we need to infer the type
|
||||
// arguments from the calling context.
|
||||
if (clazz.matchesFqName(HASH_MAP)) {
|
||||
if (clazz.matchesName(HASH_MAP)) {
|
||||
checkHashMap(node);
|
||||
} else if (clazz.matchesFqName(SPARSE_ARRAY)) {
|
||||
} else if (clazz.matchesName(SPARSE_ARRAY)) {
|
||||
checkSparseArray(node);
|
||||
}
|
||||
}
|
||||
@@ -481,29 +481,29 @@ public class JavaPerformanceDetector extends Detector implements UastScanner {
|
||||
List<UType> types = node.getTypeArguments();
|
||||
if (types.size() == 2) {
|
||||
UType first = types.get(0);
|
||||
String typeName = first.getName();
|
||||
|
||||
Project mainProject = mContext.getLintContext().getMainProject();
|
||||
int minSdk = mainProject.getMinSdk();
|
||||
|
||||
if (typeName.equals(INTEGER) || typeName.equals(BYTE)) {
|
||||
String valueType = types.get(1).getName();
|
||||
if (valueType.equals(INTEGER)) {
|
||||
if (first.isInt() || first.isByte()) {
|
||||
UType valueType = types.get(1);
|
||||
String valueTypeText = valueType.getName();
|
||||
if (valueType.isInt()) {
|
||||
mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node),
|
||||
"Use new `SparseIntArray(...)` instead for better performance");
|
||||
} else if (valueType.equals(LONG) && minSdk >= 18) {
|
||||
} else if (valueType.isLong() && minSdk >= 18) {
|
||||
mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node),
|
||||
"Use `new SparseLongArray(...)` instead for better performance");
|
||||
} else if (valueType.equals(BOOLEAN)) {
|
||||
} else if (valueType.isBoolean()) {
|
||||
mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node),
|
||||
"Use `new SparseBooleanArray(...)` instead for better performance");
|
||||
} else {
|
||||
mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node),
|
||||
String.format(
|
||||
"Use `new SparseArray<%1$s>(...)` instead for better performance",
|
||||
valueType));
|
||||
valueTypeText));
|
||||
}
|
||||
} else if (typeName.equals(LONG) && (minSdk >= 16 ||
|
||||
} else if (first.isLong() && (minSdk >= 16 ||
|
||||
Boolean.TRUE == mainProject.dependsOn(
|
||||
SUPPORT_LIB_ARTIFACT))) {
|
||||
boolean useBuiltin = minSdk >= 16;
|
||||
@@ -521,11 +521,10 @@ public class JavaPerformanceDetector extends Detector implements UastScanner {
|
||||
List<UType> types = node.getTypeArguments();
|
||||
if (types.size() == 1) {
|
||||
UType first = types.get(0);
|
||||
String valueType = first.getName();
|
||||
if (valueType.equals(INTEGER)) {
|
||||
if (first.isInt()) {
|
||||
mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node),
|
||||
"Use `new SparseIntArray(...)` instead for better performance");
|
||||
} else if (valueType.equals(BOOLEAN)) {
|
||||
} else if (first.isBoolean()) {
|
||||
mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node),
|
||||
"Use `new SparseBooleanArray(...)` instead for better performance");
|
||||
}
|
||||
|
||||
@@ -207,11 +207,17 @@ public class LogDetector extends Detector implements UastScanner {
|
||||
while (curr != null) {
|
||||
if (curr instanceof UIfExpression) {
|
||||
UIfExpression ifNode = (UIfExpression) curr;
|
||||
if (ifNode.getCondition() instanceof UCallExpression) {
|
||||
UCallExpression call = (UCallExpression) ifNode.getCondition();
|
||||
UExpression condition = ifNode.getCondition();
|
||||
if (condition instanceof UCallExpression) {
|
||||
UCallExpression call = (UCallExpression) condition;
|
||||
if (IS_LOGGABLE.equals(call.getFunctionName())) {
|
||||
checkTagConsistent(context, logCall, call);
|
||||
}
|
||||
} else if (condition instanceof UQualifiedExpression) {
|
||||
UCallExpression call = UastUtils.getCallElementFromQualified((UQualifiedExpression) condition);
|
||||
if (call != null && IS_LOGGABLE.equals(call.getFunctionName())) {
|
||||
checkTagConsistent(context, logCall, call);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -250,31 +256,39 @@ public class LogDetector extends Detector implements UastScanner {
|
||||
|
||||
JavaContext lintContext = context.getLintContext();
|
||||
if (logTag != null) {
|
||||
if (!isLoggableTag.toString().equals(logTag.toString()) &&
|
||||
isLoggableTag instanceof UResolvable &&
|
||||
logTag instanceof UResolvable) {
|
||||
UDeclaration resolved1 = ((UResolvable) isLoggableTag).resolve(context);
|
||||
UDeclaration resolved2 = ((UResolvable) logTag).resolve(context);
|
||||
if ((resolved1 == null || resolved2 == null || !resolved1.equals(resolved2))
|
||||
&& lintContext.isEnabled(WRONG_TAG)) {
|
||||
Location location = UastAndroidUtils.getLocation(logTag);
|
||||
Location alternate = UastAndroidUtils.getLocation(isLoggableTag);
|
||||
if (location != null && alternate != null) {
|
||||
alternate.setMessage("Conflicting tag");
|
||||
location.setSecondary(alternate);
|
||||
String isLoggableDescription = resolved1 != null ? resolved1
|
||||
.getName()
|
||||
: isLoggableTag.toString();
|
||||
String logCallDescription = resolved2 != null ? resolved2.getName()
|
||||
: logTag.toString();
|
||||
String message = String.format(
|
||||
"Mismatched tags: the `%1$s()` and `isLoggable()` calls typically " +
|
||||
"should pass the same tag: `%2$s` versus `%3$s`",
|
||||
logCallName,
|
||||
isLoggableDescription,
|
||||
logCallDescription);
|
||||
context.report(WRONG_TAG, call, location, message);
|
||||
}
|
||||
String isLoggableTagString = null;
|
||||
String logTagString = null;
|
||||
boolean isOk = true;
|
||||
|
||||
Object isLoggableTagValue = isLoggableTag.evaluate();
|
||||
Object logTagValue = logTag.evaluate();
|
||||
|
||||
if (isLoggableTagValue instanceof String && logTagValue instanceof String) {
|
||||
isLoggableTagString = (String) isLoggableTagValue;
|
||||
logTagString = (String) logTagValue;
|
||||
isOk = isLoggableTagString.equals(logTagString);
|
||||
} else if (isLoggableTag instanceof UResolvable && logTag instanceof UResolvable) {
|
||||
UDeclaration isLoggableTagResolved = ((UResolvable) isLoggableTag).resolve(context);
|
||||
UDeclaration logTagResolved = ((UResolvable) logTag).resolve(context);
|
||||
if (isLoggableTagResolved != null && logTagResolved != null) {
|
||||
isOk = isLoggableTagResolved.equals(logTagResolved);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isOk) {
|
||||
Location location = UastAndroidUtils.getLocation(logTag);
|
||||
Location alternate = UastAndroidUtils.getLocation(isLoggableTag);
|
||||
if (location != null && alternate != null) {
|
||||
alternate.setMessage("Conflicting tag");
|
||||
location.setSecondary(alternate);
|
||||
|
||||
String message = String.format(
|
||||
"Mismatched tags: the `%1$s()` and `isLoggable()` calls typically " +
|
||||
"should pass the same tag: `%2$s` versus `%3$s`",
|
||||
logCallName,
|
||||
isLoggableTagValue,
|
||||
logTagValue);
|
||||
context.report(WRONG_TAG, call, location, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -145,7 +145,7 @@ public class OverrideConcreteDetector extends Detector implements UastScanner {
|
||||
|
||||
private static int getTargetApi(UClass node) {
|
||||
while (node != null) {
|
||||
int targetApi = ApiDetector.getTargetApi(node.getAnnotations());
|
||||
int targetApi = ApiDetector.Companion.getTargetApi(node.getAnnotations());
|
||||
if (targetApi != -1) {
|
||||
return targetApi;
|
||||
}
|
||||
|
||||
+1
-1
@@ -90,7 +90,7 @@ public class WrongImportDetector extends Detector implements UastScanner {
|
||||
|
||||
@Override
|
||||
public boolean visitImportStatement(@NotNull UImportStatement node) {
|
||||
String fqn = node.getNameToImport();
|
||||
String fqn = node.getFqNameToImport();
|
||||
if (fqn != null && fqn.equals("android.R")) { //$NON-NLS-1$
|
||||
Location location = UastAndroidUtils.getLocation(node);
|
||||
mContext.report(ISSUE, node, location,
|
||||
|
||||
@@ -44,8 +44,6 @@ tailrec fun UElement?.getContainingDeclaration(): UDeclaration? {
|
||||
return parent.getContainingDeclaration()
|
||||
}
|
||||
|
||||
fun UClass.findProperty(name: String) = declarations.firstOrNull { it is UVariable && it.name == name } as? UVariable
|
||||
|
||||
fun UElement.handleTraverse(handler: UastHandler) {
|
||||
handler(this)
|
||||
this.traverse(handler)
|
||||
@@ -80,7 +78,6 @@ fun List<UElement>.handleTraverseList(handler: UastHandler) {
|
||||
fun UClass.findFunctions(name: String) = declarations.filter { it is UFunction && it.matchesName(name) } as List<UFunction>
|
||||
|
||||
fun UCallExpression.getReceiver(): UExpression? = (this.parent as? UQualifiedExpression)?.receiver
|
||||
fun UCallExpression.getReceiverOrEmpty(): UExpression = (this.parent as? UQualifiedExpression)?.receiver ?: EmptyExpression(this)
|
||||
|
||||
fun UElement.resolveIfCan(context: UastContext): UDeclaration? = (this as? UResolvable)?.resolve(context)
|
||||
|
||||
@@ -97,6 +94,15 @@ fun UClass.getAllDeclarations(context: UastContext): List<UDeclaration> = mutabl
|
||||
|
||||
fun UClass.getAllFunctions(context: UastContext) = getAllDeclarations(context).filterIsInstance<UFunction>()
|
||||
|
||||
tailrec fun UQualifiedExpression.getCallElementFromQualified(): UCallExpression? {
|
||||
val selector = this.selector
|
||||
return when (selector) {
|
||||
is UQualifiedExpression -> selector.getCallElementFromQualified()
|
||||
is UCallExpression -> selector
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun UCallExpression.getQualifiedCallElement(): UExpression {
|
||||
fun findParent(element: UExpression?): UExpression? = when (element) {
|
||||
is UQualifiedExpression -> findParent(element.parent as? UExpression) ?: element
|
||||
|
||||
Reference in New Issue
Block a user