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

Lint to the Finish Line (Droidcon NYC 2017)

John Rodriguez
September 25, 2017

Lint to the Finish Line (Droidcon NYC 2017)

With the release of Android Gradle Plugin 3.0, Lint will have changed quite a bit. In this talk, we'll:

* Learn about the Unified AST (UAST)
* Hash out the differences between UAST, PSI and Lombok
* Go through examples of custom lint checks with quick fixes
...and more!

You’ll learn enough to write your own lint checks in no time flat!

Video: https://www.youtube.com/watch?v=wFe0WZm_xm8

John Rodriguez

September 25, 2017
Tweet

More Decks by John Rodriguez

Other Decks in Programming

Transcript

  1. apply plugin:e'java'e dependenciese{e def toolsVersion = "25.3.3" provided "com.android.tools.lint:lint-api:${toolsVersion}" provided

    "com.android.tools.lint:lint-checks:${toolsVersion}" }e lint/build.gradle v(Lint) = 23 + v(AGP)
  2. import com.android.tools.lint.detector.api.Detector; import com.android.tools.lint.detector.api.Detector.UastScanner; class HungarianDetector extends Detector implements UastScanner

    {e static final Issue ISSUE_HUNGARIAN = Issue.create(“HungarianNotationDetected”, …); …e }e src/main/java/…/HungarianDetector.java
  3. import com.android.tools.lint.detector.api.Detector; import com.android.tools.lint.detector.api.Detector.UastScanner; class HungarianDetector extends Detector implements UastScanner

    {e static final Issue ISSUE_HUNGARIAN = Issue.create(“HungarianNotationDetected”, "Just say mNo to ” + "Hungarian notation. mmKay?”, …); …e }e src/main/java/…/HungarianDetector.java
  4. import com.android.tools.lint.detector.api.Detector; import com.android.tools.lint.detector.api.Detector.UastScanner; class HungarianDetector extends Detector implements UastScanner

    {e static final Issue ISSUE_HUNGARIAN = Issue.create(“HungarianNotationDetected”, "Just say mNo to ” + "Hungarian notation. mmKay?”, …, Severity.ERROR, …); …e }e src/main/java/…/HungarianDetector.java
  5. import com.android.tools.lint.detector.api.Detector; import com.android.tools.lint.detector.api.Detector.UastScanner; class HungarianDetector extends Detector implements UastScanner

    {e static final Issue ISSUE_HUNGARIAN = Issue.create(“HungarianNotationDetected”, "Just say mNo to ” + "Hungarian notation. mmKay?”, …, Severity.ERROR, new Implementation(HungarianDetector.class, Scope.JAVA_FILE_SCOPE)); …e }e src/main/java/…/HungarianDetector.java
  6. public interface UastScanner {e … List<String> getApplicableMethodNames(); List<String> getApplicableConstructorTypes(); List<String>

    getApplicableReferenceNames(); boolean appliesToResourceRefs(); List<String> applicableSuperClasses(); }e Detector.UastScanner
  7. public interface UastScanner {e … List<String> getApplicableMethodNames(); void visitMethod(JavaContext, UCallExpression,

    PsiMethod); List<String> getApplicableConstructorTypes(); void visitConstructor(JavaContext, UCallExpression, PsiMethod); List<String> getApplicableReferenceNames(); void visitReference(JavaContext, UReferenceExpression, PsiElement); boolean appliesToResourceRefs(); void visitResourceReference( JavaContext, UElement, ResourceType, String, boolean); List<String> applicableSuperClasses(); void visitClass(JavaContext, UClass); void visitClass(JavaContext, ULambdaExpression); }e Detector.UastScanner
  8. public interface XmlScanner { void visitDocument(XmlContext context, Document document); void

    visitElement(XmlContext context, Element element); void visitElementAfter(XmlContext context, Element element); void visitAttribute(XmlContext context, Attr attribute); Collection<String> getApplicableElements(); Collection<String> getApplicableAttributes(); } Detector.XmlScanner
  9. public interface ClassScanner { void checkClass(ClassContext context, ClassNode classNode); int[]

    getApplicableAsmNodeTypes(); void checkInstruction(ClassContext context, ClassNode classNode, MethodNode method, AbstractInsnNode instruction); List<String> getApplicableCallNames(); List<String> getApplicableCallOwners(); void checkCall(ClassContext context, ClassNode classNode, MethodNode method, MethodInsnNode call); } Detector.ClassScanner
  10. public interface GradleScanner { void visitBuildScript(Context context, Map<String, Object> data);

    } public interface BinaryResourceScanner { void checkBinaryResource(ResourceContext context); boolean appliesTo(ResourceFolderType folderType); } public interface ResourceFolderScanner { void checkFolder(ResourceContext context, String folderName); boolean appliesTo(ResourceFolderType folderType); } public interface OtherFileScanner { EnumSet<Scope> getApplicableFiles(); } Etc…
  11. public abstract class Detector { public void beforeCheckProject(Context context) {}

    public void afterCheckProject(Context context) {} public void beforeCheckLibraryProject(Context context) {} public void afterCheckLibraryProject(Context context) {} public void beforeCheckFile(Context context) {} public void afterCheckFile(Context context) {} }e Detector.java
  12. public abstracteclass Detector { public void beforeCheckProject(Context context) {} public

    void afterCheckProject(Context context) {} public void beforeCheckLibraryProject(Context context) {} public void afterCheckLibraryProject(Context context) {} public void beforeCheckFile(Context context) {} public void afterCheckFile(Context context) {} // dummy implementations of *Scanner interface methods }e Detector.java
  13. classeHungarianDetector extends Detector implements UastScanner {e … @Override public List<Class<?

    extends UElement>> getApplicableUastTypes() { return Collections.singletonList(UField.class); } }e src/main/java/…/HungarianDetector.java
  14. classeHungarianDetector extends Detector implements UastScanner {e … @Override public UElementHandler

    createUastHandler(JavaContext context) { return new UElementHandler() { @Override public void visitField(UField node) { } }; } }e src/main/java/…/HungarianDetector.java
  15. classeHungarianDetector extends Detector implements UastScanner {e … @Override public UElementHandler

    createUastHandler(JavaContext context) { return new UElementHandler() { @Override public void visitField(UField node) { String name = node.getName(); char[] c = name.toCharArray(); if (node.isStatic()) { if (!node.isFinal() && c[0] == 's' && isUpperCase(c[1])) { report(node, name, context); } } } }; } }e src/main/java/…/HungarianDetector.java
  16. classeHungarianDetector extends Detector implements UastScanner {e … @Override public UElementHandler

    createUastHandler(JavaContext context) { return new UElementHandler() { @Override public void visitField(UField node) { … else if (!node.hasModifierProperty("public")) { if (c[0] == 'm' && isUpperCase(c[1])) { report(node, name, context); } } }; } }e src/main/java/…/HungarianDetector.java
  17. classeHungarianDetector extends Detector implements UastScanner {e … private void report(UField

    node, String name, JavaContext context) { context.report(ISSUE_HUNGARIAN, context.getLocation(node), "Y u Hungarian?"); } }e src/main/java/…/HungarianDetector.java
  18. package com.example.lint; import com.android.tools.lint.client.api.IssueRegistry; import com.android.tools.lint.detector.api.Issue; import java.util.List; public class

    SampleRegistry extends IssueRegistry {e @Override public List<Issue> getIssues() { }f }e src/main/java/…/SampleRegistry.java
  19. package com.example.lint; import com.android.tools.lint.client.api.IssueRegistry; import com.android.tools.lint.detector.api.Issue; import java.util.Collections; import java.util.List;

    import static com.example.lint.HungarianDetector.ISSUE_HUNGARIAN; public class SampleRegistry extends IssueRegistry {e @Override public List<Issue> getIssues() { return Collections.singletonList(ISSUE_HUNGARIAN); }f }e src/main/java/…/SampleRegistry.java
  20. apply plugin: 'java-library' dependencies {e def toolsVersion = "26.0.0-beta6" compileOnly

    "com.android.tools.lint:lint-api:${toolsVersion}" compileOnly "com.android.tools.lint:lint-checks:${toolsVersion}" }e lint/build.gradle
  21. apply plugin: 'java-library' dependencies {e def toolsVersion = "26.0.0-beta6" compileOnly

    "com.android.tools.lint:lint-api:${toolsVersion}" compileOnly "com.android.tools.lint:lint-checks:${toolsVersion}" }e jar { manifest { attributes(‘Lint-Registry-v2': 'com.example.lint.SampleRegistry') } } lint/build.gradle
  22. import static com.android.tools.lint…TestLintTask.lint; public class HungarianDetectorTest {e @Test public void

    notHungarian() { lint() …; } }e src/test/java/…/HungarianDetectorTest.java
  23. import static com.android.tools.lint…TestFiles.java; import static com.android.tools.lint…TestLintTask.lint; public class HungarianDetectorTest {e

    @Test public void notHungarian() { lint() .files( java(“" + "package test;\n" + "class TestClass {\n" + " public int foo;\n" + "}\n") ) …; } }e src/test/java/…/HungarianDetectorTest.java
  24. public class HungarianDetectorTest {e @Test public void notHungarian() { lint()

    .files( java(“" + "package test;\n" + "class TestClass {\n" + " public int foo;\n" + "}\n") ) …; } }e src/test/java/…/HungarianDetectorTest.java
  25. public class HungarianDetectorTest {e @Test public void notHungarian() { lint()

    .files( java(“" + "package test;\n" + "class TestClass {\n" + " public int foo;\n" + "}\n") ) .issues(ISSUE_HUNGARIAN) .run() .expectClean(); } }e src/test/java/…/HungarianDetectorTest.java
  26. public class HungarianDetectorTest {e @Test public void staticMember() { lint()

    .files( java("" + "package test;\n" + "class TestClass {\n" + " static int sFoo = 5;\n" + “}\n") ) .issues(ISSUE_HUNGARIAN) .run() .expect(…); } }e src/test/java/…/HungarianDetectorTest.java
  27. public class HungarianDetectorTest {e @Test public void staticMember() { lint()

    .files( java("" + "package test;\n" + "class TestClass {\n" + " static int sFoo = 5;\n" + “}\n") ) .issues(ISSUE_HUNGARIAN) .run() .expect("src/test/TestClass.java:3: Error: Y u Hungarian? ” + “[HungarianNotationNotAllowed]\n" + " static int sFoo = 5;\n" + " ~~~~~~~~~~~~~~~~~~~~\n" + "1 errors, 0 warnings\n"); } src/test/java/…/HungarianDetectorTest.java
  28. apply plugin: 'com.android.application'
 apply plugin: 'com.kageiit.lintrules' 
 android {a
 lintOptions

    {e d…d }e
 }f
 
 dependencies { g…g 
 lintRules project(':lint')
 }h https://github.com/kageiit/gradle-lintrules-plugin app/build.gradle AGP 3.0-BETA5
  29. apply plugin: 'com.android.application'
 
 android {a
 lintOptions {e d…d }e


    }f
 
 dependencies { g…g 
 lintChecks project(':lint')
 }h app/build.gradle
  30. apply plugin: 'com.android.library'
 
 android {a
 lintOptions {e d…d }e


    }f
 
 dependencies { g…g 
 lintChecks project(':lint')
 }h library/build.gradle
  31. class SnapshotDetector extends Detector implements GradleScanner {e static final Issue

    ISSUE_SNAPSHOT = Issue.create("SnapshotVersionNotAllowed", …, Severity.ERROR, …); …e }e src/main/java/…/SnapshotDetector.java
  32. class SnapshotDetector extends Detector implements GradleScanner {e static final Issue

    ISSUE_SNAPSHOT = Issue.create("SnapshotVersionNotAllowed", …, Severity.ERROR, new Implementation(SnapshotDetector.class, Scope.GRADLE_SCOPE)); …e }e src/main/java/…/SnapshotDetector.java
  33. class SnapshotDetector extends Detector implements GradleScanner {e …e @Override public

    void afterCheckProject(Context context) { Project project = context.getProject(); Dependencies compileDependencies = getCompileDependencies(project); … }e }e src/main/java/…/SnapshotDetector.java
  34. class SnapshotDetector extends Detector implements GradleScanner {e …e @Override public

    void afterCheckProject(Context context) { Project project = context.getProject(); Dependencies compileDependencies = getCompileDependencies(project); Set<Library> allLibraries = Sets.newHashSet(); allLibraries.addAll(compileDependencies.getJavaLibraries()); allLibraries.addAll(compileDependencies.getLibraries()); … }e }e src/main/java/…/SnapshotDetector.java
  35. class SnapshotDetector extends Detector implements GradleScanner {e …e @Override public

    void afterCheckProject(Context context) { Project project = context.getProject(); Dependencies compileDependencies = getCompileDependencies(project); Set<Library> allLibraries = Sets.newHashSet(); allLibraries.addAll(compileDependencies.getJavaLibraries()); allLibraries.addAll(compileDependencies.getLibraries()); for (Library library : allLibraries) { MavenCoordinates coordinates = library.getResolvedCoordinates(); checkIfSnapshot(context, coordinates); } }e }e src/main/java/…/SnapshotDetector.java
  36. class SnapshotDetector extends Detector implements GradleScanner {e …e void checkIfSnapshot(Context

    context, MavenCoordinates coordinates) { Project project = context.getProject(); String version = coordinates.getVersion(); … } }e src/main/java/…/SnapshotDetector.java
  37. class SnapshotDetector extends Detector implements GradleScanner {e …e void checkIfSnapshot(Context

    context, MavenCoordinates coordinates) { Project project = context.getProject(); String version = coordinates.getVersion(); if (version.contains("SNAPSHOT")) { Location location = guessGradleLocation(context.getClient(), project.getDir(), version); context.report(ISSUE_SNAPSHOT, location, “Don’t use SNAPSHOT versions"); } } }e src/main/java/…/SnapshotDetector.java
  38. class SnapshotDetector extends Detector implements GradleScanner {e …e void checkIfSnapshot(Context

    context, MavenCoordinates coordinates) { … context.report(ISSUE_SNAPSHOT, location, “Don’t use SNAPSHOT versions"); } } }e src/main/java/…/SnapshotDetector.java
  39. class SnapshotDetector extends Detector implements GradleScanner {e …e Set<Position> alreadyReported

    = new LinkedHashSet<>(); void checkIfSnapshot(Context context, MavenCoordinates coordinates) { … if (!alreadyReported.contains(location)) { context.report(ISSUE_SNAPSHOT, location, “Don’t use SNAPSHOT versions"); alreadyReported.add(location); } } } }e src/main/java/…/SnapshotDetector.java
  40. import static com.example.lint.HungarianDetector.ISSUE_HUNGARIAN; public class SampleRegistry extends IssueRegistry {e @Override

    public List<Issue> getIssues() { return Collections.singletonList(ISSUE_HUNGARIAN); }f }e src/main/java/…/SampleRegistry.java
  41. import static com.example.lint.HungarianDetector.ISSUE_HUNGARIAN; public class SampleRegistry extends IssueRegistry {e @Override

    public List<Issue> getIssues() { return Arrays.asList(ISSUE_HUNGARIAN); }f }e src/main/java/…/SampleRegistry.java
  42. import static com.example.lint.HungarianDetector.ISSUE_HUNGARIAN; import static com.example.lint.SnapshotDetector.ISSUE_SNAPSHOT; public class SampleRegistry extends

    IssueRegistry {e @Override public List<Issue> getIssues() { return Arrays.asList(ISSUE_HUNGARIAN, ISSUE_SNAPSHOT); }f }e src/main/java/…/SampleRegistry.java
  43. /** * Lint client for command line usage. … *

    <p> * Minimal example: * <pre> * IssueRegistry registry = new BuiltinIssueRegistry(); * LintCliFlags flags = new LintCliFlags(); * LintCliClient client = new LintCliClient(flags); * int exitCode = client.run(registry, files); * </pre> */ @Beta public class LintCliClient extends LintClient { LintCliClient.java
  44. public class HungarianDetectorTest {e @Test public void notHungarian() { lint()

    .files( java(“" + "package test;\n" + "class TestClass {\n" + " public int foo;\n" + "}\n") ) .issues(ISSUE_HUNGARIAN) .run() .expectClean(); } }e src/test/java/…/HungarianDetectorTest.java
  45. public class HungarianDetectorTest {e @Test public void notHungarian() { lint()

    .files( java(“" + "package test;\n" + "class TestClass {\n" + " public int foo;\n" + "}\n") ) .issues(ISSUE_HUNGARIAN) .run() .expectClean(); } }e src/test/java/…/HungarianDetectorTest.java
  46. class HungarianDetector extends Detector … {e static final Issue ISSUE_HUNGARIAN

    = Issue.create(…, new Implementation(HungarianDetector.class, …)); …e }e
  47. class HungarianDetector extends Detector … {e static final Issue ISSUE_HUNGARIAN

    = Issue.create(…, new Implementation(HungarianDetector.class, …)); …e }e
  48. class HungarianDetector extends Detector … {e static final Issue ISSUE_HUNGARIAN

    = Issue.create(…, new Implementation(HungarianDetector.class, Scope.JAVA_FILE_SCOPE)); …e }e
  49. class HungarianDetector extends Detector … {e static final Issue ISSUE_HUNGARIAN

    = Issue.create(…, new Implementation(HungarianDetector.class, Scope.JAVA_FILE_SCOPE)); …e }e
  50. package com.example;
 
 public class Example { private void foo(int

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

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

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

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

    b) {
 if (b != 5) {
 b = 3;
 } else {
 throw new RuntimeException();
 }
 }
 } PackageStatement File Class ModifierList
  55. 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
  56. 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
  57. 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
  58. 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
  59. 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
  60. 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
  61. 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
  62. 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
  63. 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
  64. 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
  65. package com.example;
 
 public class Example { private void foo(int

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

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

    b) {
 if (b != 5) {
 b = 3;
 } else {
 throw new RuntimeException();
 }
 }
 } IfStatement BlockStatement BinaryExpression
  68. 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
  69. 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
  70. 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
  71. 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
  72. 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
  73. class WrongTimberUsageDetector … { 
 @Override public void visitMethod(JavaContext context,

    UCallExpression call, PsiMethod method) { String methodName = call.getMethodName(); JavaEvaluator evaluator = context.getEvaluator(); … } … }
  74. class WrongTimberUsageDetector … { 
 @Override public void visitMethod(JavaContext context,

    UCallExpression call, PsiMethod method) { String methodName = call.getMethodName(); JavaEvaluator evaluator = context.getEvaluator(); if ("tag".equals(methodName) && evaluator.isMemberInClass(method, "timber.log.Timber") && context.getMainProject().getMinSdk() <= 23) { checkTagLength(context, call); } } }
  75. class WrongTimberUsageDetector … {
 … void checkTagLength(JavaContext context, UCallExpression call)

    { List<UExpression> arguments = call.getValueArguments(); UExpression argument = arguments.get(0); } }
  76. class WrongTimberUsageDetector … {
 … void checkTagLength(JavaContext context, UCallExpression call)

    { List<UExpression> arguments = call.getValueArguments(); UExpression argument = arguments.get(0); String tag = evaluateString(context, argument, true); } }
  77. class WrongTimberUsageDetector … {
 … void checkTagLength(JavaContext context, UCallExpression call)

    { … if (tag.length() > 23) { Location location = context.getLocation(argument); context.report(ISSUE_TAG_LENGTH, argument, location, message); } } }
  78. class WrongTimberUsageDetector … {
 … void checkTagLength(JavaContext context, UCallExpression call)

    { … if (tag.length() > 23) { Location location = context.getLocation(argument); context.report(ISSUE_TAG_LENGTH, argument, location, message); } } }
  79. class WrongTimberUsageDetector … {
 … void checkTagLength(JavaContext context, UCallExpression call)

    { … LintFix fix = fix().replace().build(); context.report(ISSUE_TAG_LENGTH, argument, location, message, fix); … } }
  80. class WrongTimberUsageDetector … {
 … void checkTagLength(JavaContext context, UCallExpression call)

    { … LintFix fix = fix().replace() .text(node.asSourceString()) .with("\"" + tag.substring(0, 23) + "\"") .build(); context.report(ISSUE_TAG_LENGTH, argument, location, message, fix); … } }
  81. class WrongTimberUsageDetector … {
 … void checkTagLength(JavaContext context, UCallExpression call)

    { … int toTrim = tag.length() - 23; String s = toTrim == 1 ? "char" : toTrim + " chars”; LintFix fix = fix().replace() .text(node.asSourceString()) .with("\"" + tag.substring(0, 23) + "\"") .build(); context.report(ISSUE_TAG_LENGTH, argument, location, message, fix); … } }
  82. class WrongTimberUsageDetector … {
 … void checkTagLength(JavaContext context, UCallExpression call)

    { … int toTrim = tag.length() - 23; String s = toTrim == 1 ? "char" : toTrim + " chars”; LintFix fix = fix().replace() .name("Strip last " + s) .text(node.asSourceString()) .with("\"" + tag.substring(0, 23) + "\"") .build(); context.report(ISSUE_TAG_LENGTH, argument, location, message, fix); … } }
  83. @Test public void tagTooLong() { lint() .files(…) .issues(WrongTimberUsageDetector.ISSUE_TAG_LENGTH) .run() .expect("src/foo/Example.java:5:

    " + "Error: The logging tag can be at most 23 characters, " + "was 24 (abcdefghijklmnopqrstuvwx) [TimberTagLength]\n” + " Timber.tag(\"abcdefghijklmnopqrstuvwx\");\n" + " ~~~~~~~~~~~~~~~~~~~~~~~~~~\n” + "1 errors, 0 warnings\n”); }e
  84. org.junit.ComparisonFailure: <Click to see difference> at org.junit.Assert.assertEquals(Assert.java:115) at org.junit.Assert.assertEquals(Assert.java:144) at

    com.android…LintFixVerifier.expectFixDiffs(LintFixVerifier.java:124) at com.android…TestLintResult.expectFixDiffs(TestLintResult.java:482) at timber.lint.WrongTimberUsageDetectorTest.tagTooLong(WrongTimber…:487)
  85. @Test public void tagTooLong() { lint() .files(…) .issues(WrongTimberUsageDetector.ISSUE_TAG_LENGTH) .run() .expect(…)

    .expectFixDiffs("Fix for … line 4: Strip last char:\n" + "@@ -5 +5\n" + "- Timber.tag(\"abcdefghijklmnopqrstuvwx\");\n" + "+ Timber.tag(\"abcdefghijklmnopqrstuvw\");\n"); }e
  86. Resources • Lint Google Group: https://goo.gl/LSUYqq • Lint sources: https://goo.gl/o8GCcD

    • Google samples: https://git.io/vdJDb • Butterknife lint checks: https://git.io/vPrdV • Timber lint checks: https://git.io/vPr2t