Share
Embed
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
Transcript
150 slides
-
public class MainActivity extends AppCompatActivity { private TextView mUserNameView; private
static Map<String, String> sMap; -
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) -
apply plugin:e'java-library'e dependenciese{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 -
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 -
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 -
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 -
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 -
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 = … …e }e src/main/java/…/HungarianDetector.java -
public interface UastScanner {e … List<String> getApplicableMethodNames(); List<String> getApplicableConstructorTypes(); List<String>
getApplicableReferenceNames(); boolean appliesToResourceRefs(); List<String> applicableSuperClasses(); }e Detector.UastScanner -
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 -
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 -
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 -
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… -
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 -
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 -
classeHungarianDetector extends Detector implements UastScanner {e … @Override public List<Class<?
extends UElement>> getApplicableUastTypes() { return Collections.singletonList(UField.class); } }e src/main/java/…/HungarianDetector.java -
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 -
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 -
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 -
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 -
package com.example.lint; import com.android.tools.lint.client.api.IssueRegistry; public class SampleRegistry extends IssueRegistry {e
}e src/main/java/…/SampleRegistry.java -
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 -
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 -
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 -
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 -
import static com.android.tools.lint…TestLintTask.lint; public class HungarianDetectorTest {e @Test public void
notHungarian() { lint() …; } }e src/test/java/…/HungarianDetectorTest.java -
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 -
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 -
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 -
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 -
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 -
apply plugin: 'com.android.application' android {a d…d }f dependencies
{ g…g }h app/build.gradle -
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 -
apply plugin: 'com.android.application' android {a lintOptions {e d…d }e
}f dependencies { g…g lintChecks project(':lint') }h app/build.gradle -
apply plugin: 'com.android.library' android {a lintOptions {e d…d }e
}f dependencies { g…g lintChecks project(':lint') }h library/build.gradle -
class SnapshotDetector extends Detector implements GradleScanner {e static final Issue
ISSUE_SNAPSHOT = Issue.create("SnapshotVersionNotAllowed", …, Severity.ERROR, …); …e }e src/main/java/…/SnapshotDetector.java -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
/** * 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 -
class SampleRegistry extends IssueRegistry {e public List<Issue> getIssues() { return
Arrays.asList(ISSUE_HUNGARIAN, …); }f }e -
class SampleRegistry extends IssueRegistry {e public List<Issue> getIssues() { return
Arrays.asList(ISSUE_HUNGARIAN, …); }f }e -
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 -
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 -
class SampleRegistry extends IssueRegistry {e public List<Issue> getIssues() { return
Arrays.asList(ISSUE_HUNGARIAN, …); }f }e -
class HungarianDetector extends Detector … {e static final Issue ISSUE_HUNGARIAN
= Issue.create(…, new Implementation(HungarianDetector.class, …)); …e }e -
class HungarianDetector extends Detector … {e static final Issue ISSUE_HUNGARIAN
= Issue.create(…, new Implementation(HungarianDetector.class, …)); …e }e -
class HungarianDetector extends Detector … {e static final Issue ISSUE_HUNGARIAN
= Issue.create(…, new Implementation(HungarianDetector.class, Scope.JAVA_FILE_SCOPE)); …e }e -
class HungarianDetector extends Detector … {e static final Issue ISSUE_HUNGARIAN
= Issue.create(…, new Implementation(HungarianDetector.class, Scope.JAVA_FILE_SCOPE)); …e }e -
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 -
class WrongTimberUsageDetector … { @Override public void visitMethod(JavaContext context,
UCallExpression call, PsiMethod method) { String methodName = call.getMethodName(); JavaEvaluator evaluator = context.getEvaluator(); … } … } -
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); } } } -
class WrongTimberUsageDetector … { … void checkTagLength(JavaContext context, UCallExpression call)
{ List<UExpression> arguments = call.getValueArguments(); UExpression argument = arguments.get(0); } } -
class WrongTimberUsageDetector … { … void checkTagLength(JavaContext context, UCallExpression call)
{ List<UExpression> arguments = call.getValueArguments(); UExpression argument = arguments.get(0); String tag = evaluateString(context, argument, true); } } -
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); } } } -
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); } } } -
class WrongTimberUsageDetector … { … void checkTagLength(JavaContext context, UCallExpression call)
{ … LintFix fix = fix().replace().build(); context.report(ISSUE_TAG_LENGTH, argument, location, message, fix); … } } -
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); … } } -
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); … } } -
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); … } } -
@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 -
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) -
@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 -
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