Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Brushing up on Lint (NYC Android Meetup, April 2017)

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
Tweet

More Decks by John Rodriguez

Other Decks in Programming

Transcript

  1. Dagger -> Dagger 2 @Module public static class PaymentModule {

    @Provides PaymentTypePresenter providePresenter() { return new PaymentTypePresenter(...); } }
  2. Dagger -> Dagger 2 @Module public static class PaymentModule {

    @Provides PaymentTypePresenter providePresenter() { return new PaymentTypePresenter(...); } }
  3. Dagger -> Dagger 2 @Module2 public static class PaymentModule {

    @Provides2 PaymentTypePresenter providePresenter() { return new PaymentTypePresenter(...); } }
  4. package com.example;
 
 public class Example { private void foo(int

    b) {
 if (b != 5) {
 b = 3;
 } else {
 throw new RuntimeException();
 }
 }
 }
  5. package com.example;
 
 public class Example { private void foo(int

    b) {
 if (b != 5) {
 b = 3;
 } else {
 throw new RuntimeException();
 }
 }
 } File
  6. package com.example;
 
 public class Example { private void foo(int

    b) {
 if (b != 5) {
 b = 3;
 } else {
 throw new RuntimeException();
 }
 }
 } PackageStatement File
  7. package com.example;
 
 public class Example { private void foo(int

    b) {
 if (b != 5) {
 b = 3;
 } else {
 throw new RuntimeException();
 }
 }
 } PackageStatement File Class
  8. package com.example;
 
 public class Example { private void foo(int

    b) {
 if (b != 5) {
 b = 3;
 } else {
 throw new RuntimeException();
 }
 }
 } PackageStatement File Class ModifierList
  9. 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
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. package com.example;
 
 public class Example { private void foo(int

    b) {
 if (b != 5) {
 b = 3;
 } else {
 throw new RuntimeException();
 }
 }
 } IfStatement
  20. package com.example;
 
 public class Example { private void foo(int

    b) {
 if (b != 5) {
 b = 3;
 } else {
 throw new RuntimeException();
 }
 }
 } IfStatement BinaryExpression
  21. package com.example;
 
 public class Example { private void foo(int

    b) {
 if (b != 5) {
 b = 3;
 } else {
 throw new RuntimeException();
 }
 }
 } IfStatement BlockStatement BinaryExpression
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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);
 }
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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
  33. 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
  34. 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
  35. 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
  36. 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
  37. 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
  38. 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(…));
  39. 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");
 }
  40. :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
  41. $ 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
  42. $ 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