Lint: fix some more detectors

This commit is contained in:
Yan Zhulanow
2016-03-23 16:01:49 +03:00
parent aa4f2e426b
commit 984f10ddff
10 changed files with 500 additions and 2223 deletions
-1
View File
@@ -75,7 +75,6 @@
<globalInspection hasStaticDescription="true" shortName="AndroidKLintStringFormatMatches" displayName="String.format string doesn&apos;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="&apos;import android.R&apos; 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;
}
+1
View File
@@ -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
@@ -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))
}
}
@@ -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);
}
}
}
@@ -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;
}
@@ -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