Presented at the droidcon Berlin 2015:
http://droidcon.de/session/power-custom-lint-checks
You can find the recorded talk here:
https://www.youtube.com/watch?v=td7RzzBhBfk
Nearly every Android developer knows about the Lint toolset which comes bundled with the Android SDK - however not many use it to its full power, if at all. Lint can help prevent wrong usage of the SDK APIs, and enforce not only code style, but also internal architecture conventions. For example, you have a fancy BaseFragment which should be extended by all your Fragments, or you have a custom logger which should be used instead of android.util.Log. Both of these are perfect use cases for custom Lint checks.
This talk will show you how to use the APIs to create custom Lint checks, and how to include them in your Gradle-based project.
5. When should I use Lint?
• To ensure code quality
• Focus in reviews on real code
• Prevent people from misusing internal
libraries
… but what are the challenges?
• @Beta
• Getting familiar with the Lint API
• Integrating within your Gradle Build
• Debugging / Testing
7. Test ideas
• Fragments and Activities should extend your
BaseClass
• Use ViewUtils instead of finding and casting a
View
• Don’t check floats for equality - use
Float.equals instead
• Find leaking resources
• Enforce Naming conventions
• Find hardcoded values in XMLs
8. A real example
• Timber
• logger by Jake Wharton
• https://github.com/JakeWharton/
timber
• want to create a detector that finds
misuse of android.util.Log instead of
Timber
9. Detector
• responsible for scanning through code
and to find issues and report them
public class WrongTimberUsageDetector extends Detector
implements Detector.JavaScanner {
public static final Issue ISSUE = ...;
@Override
public List<String> getApplicableMethodNames() {
return Arrays.asList("v", "d", "i", "w", "e", "wtf");
}
@Override
public void visitMethod(@NonNull JavaContext context,
AstVisitor visitor,
@NonNull MethodInvocation node) {
if (!(node.astOperand() instanceof VariableReference)) {
return;
}
VariableReference ref = (VariableReference) node.astOperand();
if ("Log".equals(ref.astIdentifier().astValue())) {
context.report(ISSUE,
node,
context.getLocation(node),
"Using 'Log' instead of 'Timber'");
}
}
}
10. Detector
• responsible for scanning through code
and to find issues and report them
• most detectors implement one or more
scanner interfaces that depend on the
specified scope
• Detector.XmlScanner
• Detector.JavaScanner
• Detector.ClassScanner
public class WrongTimberUsageDetector extends Detector
implements Detector.JavaScanner {
public static final Issue ISSUE = ...;
@Override
public List<String> getApplicableMethodNames() {
return Arrays.asList("v", "d", "i", "w", "e", "wtf");
}
@Override
public void visitMethod(@NonNull JavaContext context,
AstVisitor visitor,
@NonNull MethodInvocation node) {
if (!(node.astOperand() instanceof VariableReference)) {
return;
}
VariableReference ref = (VariableReference) node.astOperand();
if ("Log".equals(ref.astIdentifier().astValue())) {
context.report(ISSUE,
node,
context.getLocation(node),
"Using 'Log' instead of 'Timber'");
}
}
}
11. Detector
• responsible for scanning through code
and finding issue instances and reporting
them
• most detectors implement one or more
scanner interfaces
• can detect multiple different issues
• allows you to have different severities for
different types of issues
public class WrongTimberUsageDetector extends Detector
implements Detector.JavaScanner {
public static final Issue ISSUE = ...;
@Override
public List<String> getApplicableMethodNames() {
return Arrays.asList("v", "d", "i", "w", "e", "wtf");
}
@Override
public void visitMethod(@NonNull JavaContext context,
AstVisitor visitor,
@NonNull MethodInvocation node) {
if (!(node.astOperand() instanceof VariableReference)) {
return;
}
VariableReference ref = (VariableReference) node.astOperand();
if ("Log".equals(ref.astIdentifier().astValue())) {
context.report(ISSUE,
node,
context.getLocation(node),
"Using 'Log' instead of 'Timber'");
}
}
}
12. Detector
• responsible for scanning through code
and finding issue instances and reporting
them
• most detectors implement one or more
scanner interfaces
• can detect multiple different issues
• define the calls that should be analyzed
• overwritten method depends on
implemented scanner interface
• depends on the goal of the detector
public class WrongTimberUsageDetector extends Detector
implements Detector.JavaScanner {
public static final Issue ISSUE = ...;
@Override
public List<String> getApplicableMethodNames() {
return Arrays.asList("v", "d", "i", "w", "e", "wtf");
}
@Override
public void visitMethod(@NonNull JavaContext context,
AstVisitor visitor,
@NonNull MethodInvocation node) {
if (!(node.astOperand() instanceof VariableReference)) {
return;
}
VariableReference ref = (VariableReference) node.astOperand();
if ("Log".equals(ref.astIdentifier().astValue())) {
context.report(ISSUE,
node,
context.getLocation(node),
"Using 'Log' instead of 'Timber'");
}
}
}
13. Detector
• responsible for scanning through code
and finding issue instances and reporting
them
• most detectors implement one or more
scanner interfaces
• can detect multiple different issues
• define the calls that should be analyzed
• analyze the found calls
• overwritten method depends on
implemented scanner interface
• depends on the goal of the detector
public class WrongTimberUsageDetector extends Detector
implements Detector.JavaScanner {
public static final Issue ISSUE = ...;
@Override
public List<String> getApplicableMethodNames() {
return Arrays.asList("v", "d", "i", "w", "e", "wtf");
}
@Override
public void visitMethod(@NonNull JavaContext context,
AstVisitor visitor,
@NonNull MethodInvocation node) {
if (!(node.astOperand() instanceof VariableReference)) {
return;
}
VariableReference ref = (VariableReference) node.astOperand();
if ("Log".equals(ref.astIdentifier().astValue())) {
context.report(ISSUE,
node,
context.getLocation(node),
"Using 'Log' instead of 'Timber'");
}
}
}
14. Detector
• responsible for scanning through code
and finding issue instances and reporting
them
• most detectors implement one or more
scanner interfaces
• can detect multiple different issues
• define the calls that should be analyzed
• analyze the found calls
• report the found issue
• specify the location
• report() will handle to suppress warnings
• add a message for the warning
public class WrongTimberUsageDetector extends Detector
implements Detector.JavaScanner {
public static final Issue ISSUE = ...;
@Override
public List<String> getApplicableMethodNames() {
return Arrays.asList("v", "d", "i", "w", "e", "wtf");
}
@Override
public void visitMethod(@NonNull JavaContext context,
AstVisitor visitor,
@NonNull MethodInvocation node) {
if (!(node.astOperand() instanceof VariableReference)) {
return;
}
VariableReference ref = (VariableReference) node.astOperand();
if ("Log".equals(ref.astIdentifier().astValue())) {
context.report(ISSUE,
node,
context.getLocation(node),
"Using 'Log' instead of 'Timber'");
}
}
}
15. Issue
• potential bug in an Android application
• is discovered by a Detector
• are exposed to the user
public static final Issue ISSUE =
Issue.create("LogNotTimber",
"Used Log instead of Timber",
"Since Timber is included in the project, "
+ "it is likely that calls to Log should "
+ "instead be going to Timber.",
Category.MESSAGES,
5,
Severity.WARNING,
new Implementation(WrongTimberUsageDetector.class,
Scope.JAVA_FILE_SCOPE));
16. Issue
• the id of the issue
• should be unique
• recommended to add the package name as
a prefix like com.wire.LogNotTimber
public static final Issue ISSUE =
Issue.create(“LogNotTimber",
"Used Log instead of Timber",
"Since Timber is included in the project, “
+ "it is likely that calls to Log should "
+ "instead be going to Timber.",
Category.MESSAGES,
5,
Severity.WARNING,
new Implementation(WrongTimberUsageDetector.class,
Scope.JAVA_FILE_SCOPE));
17. Issue
• the id of the issue
• short summary
• typically 5-6 words or less
• describe the problem rather than the fix public static final Issue ISSUE =
Issue.create(“LogNotTimber",
"Used Log instead of Timber",
"Since Timber is included in the project, “
+ "it is likely that calls to Log should "
+ "instead be going to Timber.",
Category.MESSAGES,
5,
Severity.WARNING,
new Implementation(WrongTimberUsageDetector.class,
Scope.JAVA_FILE_SCOPE));
18. Issue
• the id of the issue
• short summary
• full explanation of the issue
• should include a suggestion how to fix it public static final Issue ISSUE =
Issue.create(“LogNotTimber",
"Used Log instead of Timber",
"Since Timber is included in the project, “
+ "it is likely that calls to Log should "
+ "instead be going to Timber.",
Category.MESSAGES,
5,
Severity.WARNING,
new Implementation(WrongTimberUsageDetector.class,
Scope.JAVA_FILE_SCOPE));
19. Issue
• the id of the issue
• short summary
• full explanation of the issue
• the associated category, if any
• Lint
• Correctness (incl. Messages)
• Security
• Performance
• Usability (incl. Icons, Typography)
• Accessibility
• Internationalization
• Bi-directional text
public static final Issue ISSUE =
Issue.create(“LogNotTimber",
"Used Log instead of Timber",
"Since Timber is included in the project, “
+ "it is likely that calls to Log should "
+ "instead be going to Timber.",
Category.MESSAGES,
5,
Severity.WARNING,
new Implementation(WrongTimberUsageDetector.class,
Scope.JAVA_FILE_SCOPE));
20. Issue
• the id of the issue
• short summary
• full explanation of the issue
• the associated category, if any
• the priority
• a number from 1 to 10
• 10 being most important/severe
public static final Issue ISSUE =
Issue.create(“LogNotTimber",
"Used Log instead of Timber",
"Since Timber is included in the project, “
+ "it is likely that calls to Log should "
+ "instead be going to Timber.",
Category.MESSAGES,
5,
Severity.WARNING,
new Implementation(WrongTimberUsageDetector.class,
Scope.JAVA_FILE_SCOPE));
21. Issue
• the id of the issue
• short summary
• full explanation of the issue
• the associated category, if any
• the priority
• the default severity of the issue
• Fatal
• Error
• Warning
• Informational
• Ignore
public static final Issue ISSUE =
Issue.create(“LogNotTimber",
"Used Log instead of Timber",
"Since Timber is included in the project, “
+ "it is likely that calls to Log should "
+ "instead be going to Timber.",
Category.MESSAGES,
5,
Severity.WARNING,
new Implementation(WrongTimberUsageDetector.class,
Scope.JAVA_FILE_SCOPE));
22. Issue
• the id of the issue
• short summary
• full explanation of the issue
• the associated category, if any
• the priority
• the default severity of the issue
• the default implementation for this issue
• maps to the Detector class
• specifies the scope of the implementation
public static final Issue ISSUE =
Issue.create(“LogNotTimber",
"Used Log instead of Timber",
"Since Timber is included in the project, “
+ "it is likely that calls to Log should "
+ "instead be going to Timber.",
Category.MESSAGES,
5,
Severity.WARNING,
new Implementation(WrongTimberUsageDetector.class,
Scope.JAVA_FILE_SCOPE));
23. Issue
• the id of the issue
• short summary
• full explanation of the issue
• the associated category, if any
• the priority
• the default severity of the issue
• the default implementation for this issue
• the scope of the implementation
• describes set of files a detector must
consider when performing its analysis
• Include:
• Resource files / folder
• Java files
• Class files
public static final Issue ISSUE =
Issue.create(“LogNotTimber",
"Used Log instead of Timber",
"Since Timber is included in the project, “
+ "it is likely that calls to Log should "
+ "instead be going to Timber.",
Category.MESSAGES,
5,
Severity.WARNING,
new Implementation(WrongTimberUsageDetector.class,
Scope.JAVA_FILE_SCOPE));
24. Issue Registry
• provide list of checks to be performed
• return a list of Issues in getIssues()
public class CustomIssueRegistry extends IssueRegistry {
@Override
public List<Issue> getIssues() {
return Arrays.asList(WrongTimberUsageDetector.ISSUE);
}
}
31. Testing
• part of lintrules module
• tested with JUnit 4.12 and Easymock 3.3
• register and execute tests as usual in
build.gradle
public class WrongTimberUsageTest extends LintCheckTest {
@Override
protected Detector getDetector() {
return new WrongTimberUsageDetector();
}
@Test
public void testLog() throws Exception {
String expected = ...;
String lintResult = lintFiles(
"WrongTimberTestActivity.java.txt=>" +
"src/test/WrongTimberTestActivity.java");
assertEquals(expectedResult, lintResult);
}
}
32. Testing
• part of lintrules module
• tested with JUnit 4.12 and Easymock 3.3
• register and execute tests as usual in
build.gradle
• every test should extend LintCheckTest
• custom version of AbstractCheckTest
public class WrongTimberUsageTest extends LintCheckTest {
@Override
protected Detector getDetector() {
return new WrongTimberUsageDetector();
}
@Test
public void testLog() throws Exception {
String expected = ...;
String lintResult = lintFiles(
"WrongTimberTestActivity.java.txt=>" +
"src/test/WrongTimberTestActivity.java");
assertEquals(expectedResult, lintResult);
}
}
33. Testing
• part of lintrules module
• tested with JUnit 4.12 and Easymock 3.3
• register and execute tests as usual in
build.gradle
• every test should extend LintCheckTest
• need to define the to be used detector
and the selected issues
public class WrongTimberUsageTest extends LintCheckTest {
@Override
protected Detector getDetector() {
return new WrongTimberUsageDetector();
}
@Test
public void testLog() throws Exception {
String expected = ...;
String lintResult = lintFiles(
"WrongTimberTestActivity.java.txt=>" +
"src/test/WrongTimberTestActivity.java");
assertEquals(expectedResult, lintResult);
}
}
34. Testing
• part of lintrules module
• tested with JUnit 4.12 and Easymock 3.3
• register and execute tests as usual in
build.gradle
• every test should extend LintCheckTest
• need to define the to be used detector
and the selected issues
• perform usual unit tests with the helper
methods including:
• lintFiles
• lintProject
public class WrongTimberUsageTest extends LintCheckTest {
@Override
protected Detector getDetector() {
return new WrongTimberUsageDetector();
}
@Test
public void testLog() throws Exception {
String expected = ...;
String lintResult = lintFiles(
"WrongTimberTestActivity.java.txt=>" +
"src/test/WrongTimberTestActivity.java");
assertEquals(expectedResult, lintResult);
}
}
35. Testing
• need to specify the files that should be
tested
• put into data/src package in test
resources
• append to all classes suffix .txt
• more examples:
https://goo.gl/Z3gk5U
public class WrongTimberTestActivity extends FragmentActivity {
private static final String TAG = "WrongTimberTestActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
Log.d(TAG, "Test android logging");
Timber.d("Test timber logging");
}
}
36. Debugging
• not possible on real sources without building
lint on your own
• workaround is to debug on your tests
37. Conclusion
• not well documented API
• sample project:
https://goo.gl/TQt1jV
• to improve code quality
• help new team members