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. Brushing up on Lint John Rodriguez

  2. None
  3. None
  4. Examples • library developer • Butterknife • Timber • domain-specific

    rules
  5. Dagger -> Dagger 2 goo.gl/rExt9t

  6. @Provides2 @Inject2
 @Module2 Dagger -> Dagger 2 @Provides @Inject
 @Module

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

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

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

    @Provides2 PaymentTypePresenter providePresenter() { return new PaymentTypePresenter(...); } }
  10. What’s in a custom check? • Issues • Detectors •

    Implementations • Registries
  11. None
  12. Abstract Syntax Trees

  13. package com.example;
 
 public class Example { private void foo(int

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

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

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

    b) {
 if (b != 5) {
 b = 3;
 } else {
 throw new RuntimeException();
 }
 }
 } PackageStatement File Class
  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
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. package com.example;
 
 public class Example { private void foo(int

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

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

    b) {
 if (b != 5) {
 b = 3;
 } else {
 throw new RuntimeException();
 }
 }
 } IfStatement BlockStatement BinaryExpression
  31. 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
  32. 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
  33. 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
  34. 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
  35. 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
  36. Writing a custom check

  37. class DaggerUsageDetector extends Detector implements Detector.JavaPsiScanner { } Writing a

    custom check
  38. None
  39. 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);
 }
  40. class DaggerUsageDetector extends Detector implements Detector.JavaPsiScanner { } Writing a

    custom check
  41. 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
  42. class DaggerUsageDetector extends Detector implements Detector.JavaPsiScanner { public static final

    Issue NEW_DAGGER_1_MODULE = … } Writing a custom check
  43. 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
  44. class DaggerUsageDetector extends Detector implements Detector.JavaPsiScanner { public static final

    Issue NEW_DAGGER_1_MODULE = … } Writing a custom check
  45. 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
  46. 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
  47. Setup

  48. 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
  49. 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
  50. None
  51. None
  52. Testing a custom check

  53. public class DaggerUsageDetectorTest extends LintDetectorTest { } Testing a custom

    check
  54. 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
  55. public class DaggerUsageDetectorTest extends LintDetectorTest { … } Testing a

    custom check
  56. 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
  57. public class DaggerUsageDetectorTest extends LintDetectorTest { … } Testing a

    custom check
  58. 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
  59. 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
  60. Timber Example

  61. 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(…));
  62. None
  63. 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");
 }
  64. Lint internals

  65. :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
  66. $ 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
  67. $ 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
  68. None
  69. None
  70. 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
  71. Brushing up on Lint @jrodbx