Custom Lint Rules
Игорь Таланкин
1
Tinkoff.ru
О чем доклад?
2
Tinkoff.ru
Статический анализ
3
Tinkoff.ru
Android Lint
4
Tinkoff.ru
Android Lint
5
Tinkoff.ru
Timber
6
Tinkoff.ru
$ ./gradlew lint
Ran lint on variant debug: 2 issues found
Ran lint on variant release: 1 issues found
Wrote HTML report to file:///.../build/reports/lint-results.html
Wrote XML report to file:///.../build/reports/lint-results.xml
7
<?xml version="1.0" encoding="UTF-8"?>
<issues format="5" by="lint 3.3.1">
<issue
id="UnusedResources"
severity="Warning"
message="`R.layout.unused_layout` appears to be unused"
category="Performance"
priority="3"
summary="Unused resources"
explanation="..."
errorLine1="<LinearLayout"
errorLine2="^">
<location
file="/.../unused_layout.xml"
line="2"
column="1"/>
</issue>
</issues>
8
HTML
9
Tinkoff.ru
IDE
10
Tinkoff.ru
android {
lintOptions {
baseline file('baseline.xml')
checkDependencies true
}
}
11
android {
lintOptions {
baseline file('baseline.xml')
checkDependencies true
}
}
12
android {
lintOptions {
baseline file('baseline.xml')
checkDependencies true
}
}
13
Custom Lint Rules
14
Tinkoff.ru
Custom Lint Rules: зачем?
15
Tinkoff.ru
Custom Lint Rules: зачем?
16
Tinkoff.ru
Custom Lint Rules: зачем?
17
Tinkoff.ru
Custom Lint Rules: зачем?
18
Tinkoff.ru
Custom Lint Rules: зачем?
19
Tinkoff.ru
Custom Lint Rules: зачем?
20
Tinkoff.ru
Custom Lint Rules
21
Tinkoff.ru
// custom-lint-rules/build.gradle
apply plugin: 'kotlin'��dependencies {� compileOnly "com.android.tools.lint:lint-api:$lintVersion"� testCompile "com.android.tools.lint:lint-tests:$lintVersion"�}��jar {� manifest {� attributes("Lint-Registry-v2": "ru.tinkoff.CustomIssueRegistry")� }�}
22
// custom-lint-rules/build.gradle
apply plugin: 'kotlin'��dependencies {� compileOnly "com.android.tools.lint:lint-api:$lintVersion"� testCompile "com.android.tools.lint:lint-tests:$lintVersion"�}��jar {� manifest {� attributes("Lint-Registry-v2": "ru.tinkoff.CustomIssueRegistry")� }�}
23
lintVersion = androidGradlePluginVersion + 23.0.0
// например
androidGradlePluginVersion = "3.5.0"
lintVersion = "26.5.0"
24
// custom-lint-rules/build.gradle
apply plugin: 'kotlin'��dependencies {� compileOnly "com.android.tools.lint:lint-api:$lintVersion"� testCompile "com.android.tools.lint:lint-tests:$lintVersion"�}��jar {� manifest {� attributes("Lint-Registry-v2": "ru.tinkoff.CustomIssueRegistry")� }�}
25
Custom Lint Rules
26
Tinkoff.ru
class CustomIssueRegistry : IssueRegistry() {
override val api = CURRENT_API
override val issues: List<Issue> = listOf()
}
27
class CustomIssueRegistry : IssueRegistry() {
override val api = CURRENT_API
override val issues: List<Issue> = listOf()
}
28
import com.android.tools.lint.detector.api.CURRENT_API
29
class CustomIssueRegistry : IssueRegistry() {
override val api = CURRENT_API
override val issues: List<Issue> = listOf()
}
30
Custom Lint Rules
31
Tinkoff.ru
// app/build.gradle
dependencies {
lintChecks project(':custom-lint-rules')
}
32
// some-kotlin-module/build.gradle
apply plugin: 'kotlin'
apply plugin: 'com.android.lint'
dependencies {
lintChecks project(':custom-lint-rules');
}
33
Custom Lint Rules
34
Tinkoff.ru
Пример
35
Tinkoff.ru
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE) {
CropImage.ActivityResult result = CropImage.getActivityResult(data);
if (requestCode == Activity.RESULT_OK) {
Uri resultUri = result.getUri();
try {
Bitmap bitmap = getBitmap(getContentResolver(), resultUri);
profile_image.setImageBitmap(bitmap);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
36
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE) {
CropImage.ActivityResult result = CropImage.getActivityResult(data);
if (requestCode == Activity.RESULT_OK) {
Uri resultUri = result.getUri();
try {
Bitmap bitmap = getBitmap(getContentResolver(), resultUri);
profile_image.setImageBitmap(bitmap);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
37
Интерфейсы
38
Tinkoff.ru
SourceCodeScanner
39
Tinkoff.ru
UAST
40
Tinkoff.ru
// MyClass.java
public class MyClass {
String hello = "Hello!";
void bar() { }
}
// MyClass.kt
class MyClass {
val hello = "Hello!"
fun bar() { }
}
41
// MyClass.java
public class MyClass {
String hello = "Hello!";
void bar() { }
}
// MyClass.kt
class MyClass {
val hello = "Hello!"
fun bar() { }
}
42
UFile
UClass
(name = MyClass)
UField
(name = hello)
UMethod
(name = bar)
UBlockExpression
ULiteralExpression
(value = "Hello!")
Алгоритм
43
Tinkoff.ru
class ActivityResultDetector: Detector(), SourceCodeScanner {
companion object {
val ISSUE = Issue.create(...)
}
override fun getApplicableReferenceNames(): List<String>?
override fun visitReference(
context: JavaContext,
reference: UReferenceExpression,
referenced: PsiElement)
}
44
class ActivityResultDetector: Detector(), SourceCodeScanner {
companion object {
val ISSUE = Issue.create(...)
}
override fun getApplicableReferenceNames(): List<String>?
override fun visitReference(
context: JavaContext,
reference: UReferenceExpression,
referenced: PsiElement)
}
45
val ISSUE = Issue.create(
id = "ActivityResultDetector",
briefDescription = "Comparing result code to request code",
explanation = "<...>",
category = Category.CORRECTNESS,
priority = 5,
severity = Severity.ERROR,
implementation = Implementation(
ActivityResultDetector::class.java,
Scope.JAVA_FILE_SCOPE)
)
46
val ISSUE = Issue.create(
id = "ActivityResultDetector",
briefDescription = "Comparing result code to request code",
explanation = "<...>",
category = Category.CORRECTNESS,
priority = 5,
severity = Severity.ERROR,
implementation = Implementation(
ActivityResultDetector::class.java,
Scope.JAVA_FILE_SCOPE)
)
47
val ISSUE = Issue.create(
id = "ActivityResultDetector",
briefDescription = "Comparing result code to request code",
explanation = "<...>",
category = Category.CORRECTNESS,
priority = 5,
severity = Severity.ERROR,
implementation = Implementation(
ActivityResultDetector::class.java,
Scope.JAVA_FILE_SCOPE)
)
48
val ISSUE = Issue.create(
id = "ActivityResultDetector",
briefDescription = "Comparing result code to request code",
explanation = "<...>",
category = Category.CORRECTNESS,
priority = 5,
severity = Severity.ERROR,
implementation = Implementation(
ActivityResultDetector::class.java,
Scope.JAVA_FILE_SCOPE)
)
49
val ISSUE = Issue.create(
id = "ActivityResultDetector",
briefDescription = "Comparing result code to request code",
explanation = "<...>",
category = Category.CORRECTNESS,
priority = 5,
severity = Severity.ERROR,
implementation = Implementation(
ActivityResultDetector::class.java,
Scope.JAVA_FILE_SCOPE)
)
50
enum class Severity {
FATAL("Fatal"),
ERROR("Error"),
WARNING("Warning"),
INFORMATIONAL("Information"),
IGNORE("Ignore")
}
51
val ISSUE = Issue.create(
id = "ActivityResultDetector",
briefDescription = "Comparing result code to request code",
explanation = "<...>",
category = Category.CORRECTNESS,
priority = 5,
severity = Severity.ERROR,
implementation = Implementation(
ActivityResultDetector::class.java,
Scope.JAVA_FILE_SCOPE)
)
52
enum class Scope {
RESOURCE_FILE,
JAVA_FILE,
CLASS_FILE,
MANIFEST,
PROGUARD_FILE,
GRADLE_FILE,
...
}
53
class ActivityResultDetector : Detector(), SourceCodeScanner {
companion object {
val ISSUE = Issue.create(...)
}
override fun getApplicableReferenceNames(): List<String>?
override fun visitReference(
context: JavaContext,
reference: UReferenceExpression,
referenced: PsiElement)
}
54
class ActivityResultDetector : Detector(), SourceCodeScanner {
companion object {
val ISSUE = Issue.create(...)
}
override fun getApplicableReferenceNames(): List<String>?
override fun visitReference(
context: JavaContext,
reference: UReferenceExpression,
referenced: PsiElement)
}
55
override fun getApplicableReferenceNames(): List<String>? {
return listOf("RESULT_OK", "RESULT_CANCELED")
}
56
override fun visitReference(
context: JavaContext,
reference: UReferenceExpression,
referenced: PsiElement) {
// TODO
}
57
override fun visitReference(
context: JavaContext,
reference: UReferenceExpression,
referenced: PsiElement) {
// TODO
}
58
override fun visitReference(
context: JavaContext,
reference: UReferenceExpression,
referenced: PsiElement) {
// TODO
}
59
@Override
void onActivityResult(int requestCode, int resultCode, Intent data) {
// ...
if (requestCode == Activity.RESULT_OK) {
Uri resultUri = result.getUri();
// ...
}
}
60
reference: UReferenceExpression
override fun visitReference(
context: JavaContext,
reference: UReferenceExpression,
referenced: PsiElement) {
// TODO
}
61
public class Activity extends ContextThemeWrapper {
/** Standard activity result: operation succeeded. */
public static final int RESULT_OK = -1;
}
62
referenced: PsiElement
PSI
63
Tinkoff.ru
@Override
void onActivityResult(int requestCode, int resultCode, Intent data) {
// ...
if (requestCode == Activity.RESULT_OK) {
Uri resultUri = result.getUri();
// ...
}
}
64
65
UBinaryExpression
UQualifiedReferenceExpression
UReferenceExpression
(requestCode)
UReferenceExpression (Activity)
UReferenceExpression
(RESULT_OK)
@Override
void onActivityResult(int requestCode, int resultCode, Intent data) {
// ...
if (requestCode == Activity.RESULT_OK) {
Uri resultUri = result.getUri();
// ...
}
}
66
reference: UReferenceExpression
@Override
void onActivityResult(int requestCode, int resultCode, Intent data) {
// ...
if (requestCode == Activity.RESULT_OK) {
Uri resultUri = result.getUri();
// ...
}
}
67
UQualifiedReferenceExpression
@Override
void onActivityResult(int requestCode, int resultCode, Intent data) {
// ...
if (requestCode == Activity.RESULT_OK) {
Uri resultUri = result.getUri();
// ...
}
}
68
UBinaryExpression
val comparison: UBinaryExpression =
reference.getParentOfType(UBinaryExpression::class.java)
69
@Override
void onActivityResult(int requestCode, int resultCode, Intent data) {
// ...
if (requestCode == Activity.RESULT_OK) {
Uri resultUri = result.getUri();
// ...
}
}
70
leftOperand: UExpression
rightOperand: UExpression
val operand =
comparison.leftOperand as? UReferenceExpression
71
@Override
void onActivityResult(int requestCode, int resultCode, Intent data) {
// ...
if (requestCode == Activity.RESULT_OK) {
Uri resultUri = result.getUri();
// ...
}
}
72
method: UMethod
@Override
void onActivityResult(int requestCode, int resultCode, Intent data) {
// ...
if (requestCode == Activity.RESULT_OK) {
Uri resultUri = result.getUri();
// ...
}
}
73
method.parameterList.parameters[0]
val method: UMethod? =
reference.getParentOfType(UMethod::class.java)
val paramRequestCode = method.parameterList.parameters[0]
74
if (operand.resolve() == paramRequestCode) {
context.report(
issue = ActivityResultDetector.ISSUE,
location = context.getLocation(comparison),
message = "Error!"
)
}
75
if (operand.resolve() == paramRequestCode) {
context.report(
issue = ActivityResultDetector.ISSUE,
location = context.getLocation(comparison),
message = "Error!"
)
}
76
@Override
void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == Activity.RESULT_OK) {
Uri uri = data.getData();
// ...
}
}
77
operand: UReferenceExpression
@Override
void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == Activity.RESULT_OK) {
Uri uri = data.getData();
// ...
}
}
78
resolve()
if (operand.resolve() == paramRequestCode) {
context.report(
issue = ActivityResultDetector.ISSUE,
location = context.getLocation(comparison),
message = "Error!"
)
}
79
if (operand.resolve() == paramRequestCode) {
context.report(
issue = ActivityResultDetector.ISSUE,
location = context.getLocation(comparison),
message = "Error!"
)
}
80
class CustomIssueRegistry : IssueRegistry() {
override val api = CURRENT_API
override val issues: List<Issue> = listOf(
ActivityResultDetector.ISSUE
)
}
81
class CustomIssueRegistry : IssueRegistry() {
override val api = CURRENT_API
override val issues: List<Issue> = listOf(
ActivityResultDetector.ISSUE
)
}
82
HTML
83
Tinkoff.ru
IDE
84
Tinkoff.ru
IDE
85
Tinkoff.ru
Проблема
86
Tinkoff.ru
@Override
void onActivityResult(int requestCode, int resultCode, Intent data) {
// ...
if (Activity.RESULT_OK == requestCode) {
Uri resultUri = result.getUri();
// ...
}
}
87
@Override
void onActivityResult(int requestCode, int resultCode, Intent data) {
// ...
if (Activity.RESULT_OK == requestCode) {
Uri resultUri = result.getUri();
// ...
}
}
88
val operand =
comparison.leftOperand as? UReferenceExpression
89
val operand: UReferenceExpression? =
comparison.leftOperand as? UReferenceExpression
getAnotherOperand(comparison, reference)
90
private fun getAnotherOperand(
comparison: UBinaryExpression,
referenceToField: UReferenceExpression
): UReferenceExpression? {
return if (comparison.leftOperand == referenceToField) {
comparison.rightOperand
} else {
comparison.leftOperand
} as? UReferenceExpression
}
91
IDE
92
Tinkoff.ru
Проблема №2
93
Tinkoff.ru
94
val method = reference.getParentOfType(UMethod::class.java)
if (!isOnActivityResult(context, method)) {
return
}
95
fun isOnActivityResult(
context: JavaContext,
method: UMethod
): Boolean {
return context.evaluator.methodMatches(
method,
"android.app.Activity",
false,
"int", "int", "android.content.Intent"
)
}
96
Utils
97
Tinkoff.ru
SdkConstants
98
Tinkoff.ru
Utils
99
Tinkoff.ru
Отладка
100
Tinkoff.ru
fun UElement.asRecursiveLogString(): String
101
UMethod (name = onActivityResult)
UAnnotation (fqName = java.lang.Override)
UParameter (name = requestCode)
UParameter (name = resultCode)
UParameter (name = data)
UBlockExpression
UIfExpression
UBinaryExpression (operator = ===)
USimpleNameReferenceExpression (id = requestCode)
UQualifiedReferenceExpression
USimpleNameReferenceExpression (id = Activity)
USimpleNameReferenceExpression (id = RESULT_OK)
102
PsiViewer plugin
103
Tinkoff.ru
# ~/.AndroidStudio3.x/config/idea.properties
idea.is.internal=true
104
View PSI structure
105
Tinkoff.ru
Отладка
106
Tinkoff.ru
$ ./gradle lintDebug \
-Dorg.gradle.debug=true \
--no-daemon
107
Отладка
108
Tinkoff.ru
Отладка
109
Tinkoff.ru
LintFix
110
Tinkoff.ru
val paramResultCode = method.parameterList.parameters[1]
val fix = LintFix.create()
.replace()
.text(paramRequestCode.name)
.with(paramResultCode.name)
.build()
111
val paramResultCode = method.parameterList.parameters[1]
val fix = LintFix.create()
.replace()
.text(paramRequestCode.name)
.with(paramResultCode.name)
.build()
112
context.report(
issue = ActivityResultDetector.ISSUE,
location = context.getLocation(comparison),
message = "Error!",
quickfixData = fix
)
113
LintFix
114
Tinkoff.ru
LintFix
115
Tinkoff.ru
Тесты
116
Tinkoff.ru
val testData = """
import android.app.Activity;
import android.content.Intent;
class MyActivity extends Activity {
@Override
public void onActivityResult(<...>) {
if (requestCode == Activity.RESULT_OK) {
}
}
}
""".trimIndent()
117
val expectedText = """
|src/MyActivity.java:7: Error: Error!
| if (requestCode == Activity.RESULT_OK) {
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|1 errors, 0 warnings
""".trimMargin()
118
@Test
fun basicScenario() {
val testData = "<...>"
val expectedText = "<...>"
TestLintTask.lint()
.files(java(testData))
.issues(ActivityResultDetector.ISSUE)
.run()
.expect(expectedText)
}
119
val expectedFixDiffs = """
|Fix for src/MyActivity.java line 7: Replace:
|@@ -7 +7
|- if (requestCode == Activity.RESULT_OK) {
|+ if (resultCode == Activity.RESULT_OK) {
""".trimMargin()
120
@Test
fun quickfix() {
val testData = "<...>"
val expectedFixDiffs = "<...>"
TestLintTask.lint()
.files(java(testData))
.issues(ActivityResultDetector.ISSUE)
.run()
.expectErrorCount(1)
.expectFixDiffs(expectedFixDiffs)
}
121
Решение проблем
122
Tinkoff.ru
java.lang.AssertionError: This test requires an Android SDK: No SDK configured.
123
Запуск тестов
124
Tinkoff.ru
class MyClass {
@MyAnnotation
fun foobar() {
}
}
125
UFile (package = )
UClass (name = MyClass)
UAnnotationMethod (name = foobar)
UBlockExpression
UAnnotationMethod (name = MyClass)
126
UFile (package = )
UClass (name = MyClass)
UAnnotationMethod (name = foobar)
UBlockExpression
UAnnotationMethod (name = MyClass)
127
Решения
128
Tinkoff.ru
val myAnnotation = "annotation class MyAnnotation"
TestLintTask.lint()
.files(kotlin(testData), kotlin(myAnnotation))
.issues(ISSUE)
.run()
.expect(expectedText)
129
val myAnnotation = "annotation class MyAnnotation"
TestLintTask.lint()
.files(kotlin(testData), kotlin(myAnnotation))
.issues(ISSUE)
.run()
.expect(expectedText)
130
UFile (package = )
UClass (name = MyClass)
UAnnotationMethod (name = foobar)
UAnnotation (fqName = MyAnnotation)
UBlockExpression
UAnnotationMethod (name = MyClass)
131
UFile (package = )
UClass (name = MyClass)
UAnnotationMethod (name = foobar)
UAnnotation (fqName = MyAnnotation)
UBlockExpression
UAnnotationMethod (name = MyClass)
132
Решения
133
Tinkoff.ru
val targetPath = "libs/my-lib.jar"
val producer = object : TestFile.BytecodeProducer() {
override fun produce() = // read bytes from resources
}
val myLib = bytecode(targetPath, producer)
TestLintTask.lint()
.files(kotlin(file), classpath(targetPath), myLib)
.issues(ISSUE)
.run()
.expect(expectedText)
134
Недостатки
135
Tinkoff.ru
Недостатки
136
Tinkoff.ru
Заключение
137
Tinkoff.ru
Заключение
138
Tinkoff.ru
Полезные материалы
139
Tinkoff.ru
Android Lint performance probe
140
Tinkoff.ru
Вопросы?
t.me/italankin
github.com/italankin
141
142