Share
Embed
Brushing up on Lint (NYC Android Meetup, April 2017)
If you're an Android developer, then you're probably familiar with the Lint static analysis tool and the suite of checks it provides out of the box. But did you know you could write your own Lint checks? In this talk, we'll dive into some internals and learn how to write your own checks in no time...with tests to instill confidence!
Video: https://youtu.be/RQHYVj2k3NM?t=30m34s
John Rodriguez
April 19, 2017
Transcript
71 slides
-
Dagger -> Dagger 2 @Module public static class PaymentModule {
@Provides PaymentTypePresenter providePresenter() { return new PaymentTypePresenter(...); } } -
Dagger -> Dagger 2 @Module public static class PaymentModule {
@Provides PaymentTypePresenter providePresenter() { return new PaymentTypePresenter(...); } } -
Dagger -> Dagger 2 @Module2 public static class PaymentModule {
@Provides2 PaymentTypePresenter providePresenter() { return new PaymentTypePresenter(...); } } -
What’s in a custom check? • Issues • Detectors •
Implementations • Registries -
package com.example; public class Example { private void foo(int
b) { if (b != 5) { b = 3; } else { throw new RuntimeException(); } } } -
package com.example; public class Example { private void foo(int
b) { if (b != 5) { b = 3; } else { throw new RuntimeException(); } } } File -
package com.example; public class Example { private void foo(int
b) { if (b != 5) { b = 3; } else { throw new RuntimeException(); } } } PackageStatement File -
package com.example; public class Example { private void foo(int
b) { if (b != 5) { b = 3; } else { throw new RuntimeException(); } } } PackageStatement File Class -
package com.example; public class Example { private void foo(int
b) { if (b != 5) { b = 3; } else { throw new RuntimeException(); } } } PackageStatement File Class ModifierList -
package com.example; public class Example { private void foo(int
b) { if (b != 5) { b = 3; } else { throw new RuntimeException(); } } } PackageStatement File Class ModifierList Identifier -
package com.example; public class Example { private void foo(int
b) { if (b != 5) { b = 3; } else { throw new RuntimeException(); } } } PackageStatement File Class ModifierList Identifier Method -
package com.example; public class Example { private void foo(int
b) { if (b != 5) { b = 3; } else { throw new RuntimeException(); } } } PackageStatement File Class ModifierList Identifier Method ModifierList -
package com.example; public class Example { private void foo(int
b) { if (b != 5) { b = 3; } else { throw new RuntimeException(); } } } PackageStatement File Class ModifierList Identifier Method TypeElement ModifierList -
package com.example; public class Example { private void foo(int
b) { if (b != 5) { b = 3; } else { throw new RuntimeException(); } } } PackageStatement File Class ModifierList Identifier Method TypeElement ModifierList Identifier -
package com.example; public class Example { private void foo(int
b) { if (b != 5) { b = 3; } else { throw new RuntimeException(); } } } PackageStatement File Class ModifierList Identifier Method TypeElement ModifierList Parameter Identifier -
package com.example; public class Example { private void foo(int
b) { if (b != 5) { b = 3; } else { throw new RuntimeException(); } } } PackageStatement File Class ModifierList Identifier Method TypeElement ModifierList Parameter TypeElement Identifier -
package com.example; public class Example { private void foo(int
b) { if (b != 5) { b = 3; } else { throw new RuntimeException(); } } } PackageStatement File Class ModifierList Identifier Method TypeElement ModifierList Parameter TypeElement Identifier Identifier -
package com.example; public class Example { private void foo(int
b) { if (b != 5) { b = 3; } else { throw new RuntimeException(); } } } PackageStatement File Class ModifierList Identifier Method TypeElement ModifierList Parameter Identifier CodeBlock TypeElement Identifier -
package com.example; public class Example { private void foo(int
b) { if (b != 5) { b = 3; } else { throw new RuntimeException(); } } } IfStatement PackageStatement File Class ModifierList Identifier Method TypeElement ModifierList Parameter Identifier CodeBlock TypeElement Identifier -
package com.example; public class Example { private void foo(int
b) { if (b != 5) { b = 3; } else { throw new RuntimeException(); } } } IfStatement -
package com.example; public class Example { private void foo(int
b) { if (b != 5) { b = 3; } else { throw new RuntimeException(); } } } IfStatement BinaryExpression -
package com.example; public class Example { private void foo(int
b) { if (b != 5) { b = 3; } else { throw new RuntimeException(); } } } IfStatement BlockStatement BinaryExpression -
package com.example; public class Example { private void foo(int
b) { if (b != 5) { b = 3; } else { throw new RuntimeException(); } } } IfStatement BlockStatement BinaryExpression ExpressionStatement AssignmentExpression -
package com.example; public class Example { private void foo(int
b) { if (b != 5) { b = 3; } else { throw new RuntimeException(); } } } IfStatement BlockStatement BinaryExpression ExpressionStatement AssignmentExpression Identifier -
package com.example; public class Example { private void foo(int
b) { if (b != 5) { b = 3; } else { throw new RuntimeException(); } } } IfStatement BlockStatement BinaryExpression ExpressionStatement AssignmentExpression Identifier LiteralExpression -
package com.example; public class Example { private void foo(int
b) { if (b != 5) { b = 3; } else { throw new RuntimeException(); } } } IfStatement BlockStatement BinaryExpression BlockStatement ExpressionStatement AssignmentExpression Identifier LiteralExpression -
package com.example; public class Example { private void foo(int
b) { if (b != 5) { b = 3; } else { throw new RuntimeException(); } } } IfStatement BlockStatement BinaryExpression BlockStatement ExpressionStatement AssignmentExpression Identifier LiteralExpression ThrowStatement -
public interface JavaPsiScanner { /* required unless if return true
from {@link #appliesToResourceRefs()} or return non null from {@link #getApplicableMethodNames()}. */ JavaElementVisitor createPsiVisitor(JavaContext context); List<Class<? extends PsiElement>> getApplicablePsiTypes(); List<String> getApplicableMethodNames(); void visitMethod(JavaContext context, JavaElementVisitor visitor, PsiMethodCallExpression call, PsiMethod method); List<String> getApplicableConstructorTypes(); void visitConstructor(JavaContext context, JavaElementVisitor visitor, PsiNewExpression node, PsiMethod constructor); List<String> getApplicableReferenceNames(); void visitReference(JavaContext context, JavaElementVisitor visitor, PsiJavaCodeReferenceElement reference, PsiElement referenced); boolean appliesToResourceRefs(); void visitResourceReference(JavaContext context, JavaElementVisitor visitor, PsiElement node, ResourceType type, String name, boolean isFramework); List<String> applicableSuperClasses(); void checkClass(JavaContext context, PsiClass declaration); } -
class DaggerUsageDetector extends Detector implements Detector.JavaPsiScanner { public static final
Issue NEW_DAGGER_1_MODULE = Issue.create("NewDagger1ModuleDetected", "New Dagger1 Module detected", "Please create a Dagger2 Module and/or Component instead.", Category.CORRECTNESS, 10, Severity.ERROR, new Implementation(DaggerUsageDetector.class, Scope.JAVA_FILE_SCOPE)); } Writing a custom check -
class DaggerUsageDetector extends Detector implements Detector.JavaPsiScanner { public static final
Issue NEW_DAGGER_1_MODULE = … } Writing a custom check -
class DaggerUsageDetector extends Detector implements Detector.JavaPsiScanner { @Override public List<Class<?
extends PsiElement>> getApplicablePsiTypes() { return Collections.singletonList(PsiAnnotation.class); } @Override public EnumSet<Scope> getApplicableFiles() { return Scope.JAVA_FILE_SCOPE; } public static final Issue NEW_DAGGER_1_MODULE = … } Writing a custom check -
class DaggerUsageDetector extends Detector implements Detector.JavaPsiScanner { public static final
Issue NEW_DAGGER_1_MODULE = … } Writing a custom check -
class DaggerUsageDetector extends Detector implements Detector.JavaPsiScanner { @Override public JavaElementVisitor
createPsiVisitor(JavaContext context) { return new JavaElementVisitor() { @Override public void visitAnnotation(PsiAnnotation annotation) { String type = annotation.getQualifiedName(); if (!"dagger.Module".equals(type)) { return; } PsiElement modifierList = LintUtils.skipParentheses(annotation.getParent()); PsiElement markedModule = LintUtils.skipParentheses(modifierList.getParent()); if (!(markedModule instanceof PsiClass)) { return; } PsiClass moduleClass = (PsiClass) markedModule; context.report(NEW_DAGGER_1_MODULE, context.getLocation(moduleClass), "Don't add new Dagger1 Modules; use Dagger2 Modules/Components instead"); } }; } public static final Issue NEW_DAGGER_1_MODULE = … } Writing a custom check -
class CustomIssueRegistry extends IssueRegistry { @Override public List<Issue> getIssues() {
return Arrays.asList( StringNamesDetector.OUT_OF_ORDER, DaggerUsageDetector.NEW_DAGGER_1_MODULE, RxObservableCreateDetector.NEW_INVOCATION ); } } Writing a custom check -
apply plugin: 'java' dependencies { compile "com.android.tools.lint:lint-api:${lintVersion}" testCompile "com.android.tools.lint:lint:${lintVersion}"
testCompile "com.android.tools.lint:lint-tests:${lintVersion}" … } jar { manifest { attributes('Lint-Registry': 'com.example.lint.CustomIssueRegistry') } } Setup -
apply plugin: 'com.android.library' apply plugin: ‘com.kageiit.lintrules' android { lintOptions
{ … } } dependencies { … lintRules project(‘:your-lint-project') } Setup https://github.com/kageiit/gradle-lintrules-plugin -
public class DaggerUsageDetectorTest extends LintDetectorTest { @Override protected Detector getDetector()
{ return new DaggerUsageDetector(); } @Override protected List<Issue> getIssues() { return Collections.singletonList(NEW_DAGGER_1_MODULE); } } Testing a custom check -
public class DaggerUsageDetectorTest extends LintDetectorTest { public void testShouldNotTriggerNewDaggerModuleIssue() throws
Exception { String noModuleSource = "" + "package foobar;\n" + "public class NoModule {}"; String lintOutput = lintProject(java(noModuleSource)); assertThat(lintOutput).isEqualTo("No warnings."); } } Testing a custom check -
public class DaggerUsageDetectorTest extends LintDetectorTest { public void testShouldTriggerNewDaggerModuleIssue() throws
Exception { String newModuleSource = "" + "package foobar;\n" + "import dagger.Module;\n" + "@Module\n" + "public class NewModule {}"; String fakeAnnotationSource = "" + "package dagger;\n" + "public @interface Module {}"; String lintOutput = lintProject(java(newModuleSource), java(fakeAnnotationSource)); … } } Testing a custom check -
public class DaggerUsageDetectorTest extends LintDetectorTest { public void testShouldTriggerNewDaggerModuleIssue() throws
Exception { … String format = "" + "src/foobar/NewModule.java:3: Error: %s [%s]\n" + "@Module\n" + "^\n" + "1 errors, 0 warnings\n"; String warningMessage = String.format(format, "Don't add new Dagger1 Modules; use Dagger2 Modules/Components instead", NEW_DAGGER_1_MODULE.getId()); assertThat(lintOutput).isEqualTo(warningMessage); } } Testing a custom check -
public final class WrongTimberUsageDetector extends Detector implements JavaPsiScanner { @Override
public List<String> getApplicableMethodNames() { return asList("tag", "format", …); } @Override public void visitMethod(JavaContext context, JavaElementVisitor visitor, PsiMethodCallExpression call, PsiMethod method) { PsiReferenceExpression methodExpression = call.getMethodExpression(); String fullyQualifiedMethodName = methodExpression.getQualifiedName(); … if (fullyQualifiedMethodName.startsWith("timber.log.Timber.tag")) { checkTagLength(context, call); return; } … } private static void checkTagLength(JavaContext context, PsiMethodCallExpression call) { PsiExpression argument = call.getArgumentList().getExpressions()[0]; String tag = findLiteralValue(argument); if (tag != null && tag.length() > 23) { String message = String.format("Tag must be <= 23 characters, was %1$d (%2$s)”, tag.length(), tag); context.report(ISSUE_TAG_LENGTH, argument, context.getLocation(argument), message); } } Issue ISSUE_TAG_LENGTH = Issue.create("TimberTagLength", …, new Implementation(…)); -
public class WrongTimberUsageDetectorTest extends LintDetectorTest { private final TestFile timberStub
= java("" + "package timber.log;\n" + "public class Timber {\n" + " public static Tree tag(String s) { return new Tree(); }\n" + "}"); public void testTagTooLong() throws Exception { String source = "" + "package foo;\n" + "import timber.log.Timber;\n" + "public class Example {\n" + " public void log() {\n" + " Timber.tag(\"abcdefghijklmnopqrstuvwx\");\n" + " }\n" + “}"; assertThat(lintProject(java(source), timberStub)).isEqualTo("src/foo/Example.java:5: " + "Error: Tag must be <= 23 characters, was 24 (abcdefghijklmnopqrstuvwx) [TimberTagLength]\n" + " Timber.tag(\"abcdefghijklmnopqrstuvwx\");\n" + " ~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + "1 errors, 0 warnings\n"); } -
:buildEnvironment ------------------------------------------------------------ Root project ------------------------------------------------------------ classpath \--- com.android.tools.build:gradle:2.3.1 \--- com.android.tools.build:gradle-core:2.3.1
+--- com.android.tools.build:builder:2.3.1 | . . . +--- com.android.tools.lint:lint:25.3.1 | +--- com.android.tools.lint:lint-checks:25.3.1 | | +--- com.android.tools.lint:lint-api:25.3.1 Note the versions! v(Lint) = 23 + v(AGP) $ ./gradlew buildEnvironment -
$ pwd /Users/jrod/Library/Android/sdk/tools/ /Users/jrod/Library/Android/sdk/tools/bin # as of SDK Tools v25.2.3
$ archquery avdmanager jobb lint monkeyrunner screenshot2 sdkmanager uiautomatorviewer ls $ lint: version 25.2.3 lint --version -
$ file lint $ jarfile=lint.jar ... jarpath="$frameworkdir/$jarfile" exec "$javaCmd" \
... -classpath "$jarpath" \ com.android.tools.lint.Main “$@“ lint: Bourne-Again shell script text executable, ASCII text tail lint -
Learn more • Android Lint • https://android.googlesource.com/platform/tools/base/+/ studio-master-dev/lint • https://groups.google.com/forum/#!forum/lint-dev
• OSS Examples • https://github.com/JakeWharton/butterknife • https://github.com/JakeWharton/timber