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

The Power of Custom Lint Checks

The Power of Custom Lint Checks

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.

Marc Prengemann

June 05, 2015
Tweet

More Decks by Marc Prengemann

Other Decks in Programming

Transcript

  1. 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
  2. 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
  3. 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
  4. 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'");
 }
 }
 }
  5. 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'");
 }
 }
 }
  6. 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'");
 }
 }
 }
  7. 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'");
 }
 }
 }
  8. 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'");
 }
 }
 }
  9. 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'");
 }
 }
 }
  10. 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));
  11. 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));
  12. 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));
  13. 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));
  14. 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));
  15. 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));
  16. 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));
  17. 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));
  18. 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));
  19. 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);
 }
 }
  20. Project structure • lintrules • Java module • source of

    all custom detectors and our IssueRegistry • specify reference in build.gradle as attribute Lint-Registry • will export a lint.jar $ ./gradlew projects :projects ------------------------------------------------------------ Root project ------------------------------------------------------------ Root project ‘awsm_app' +--- Project ':app' +--- Project ':lintlib' \--- Project ':lintrules' jar { manifest { attributes 'Manifest-Version': 1.0 attributes 'Lint-Registry': 'com.checks.CustomIssueRegistry' } }
  21. Project structure • lintrules • lintlib • Android library module

    • has a dependency to lintrules $ ./gradlew projects :projects ------------------------------------------------------------ Root project ------------------------------------------------------------ Root project ‘awsm_app' +--- Project ':app' +--- Project ':lintlib' \--- Project ':lintrules'
  22. Project structure • lintrules • lintlib • app • has

    dependency to lintlib module • since lintlib is an Android library module, we can use the generated lint.jar $ ./gradlew projects :projects ------------------------------------------------------------ Root project ------------------------------------------------------------ Root project ‘awsm_app' +--- Project ':app' +--- Project ':lintlib' \--- Project ':lintrules'
  23. Project structure • lintrules • lintlib • app • check

    your project using with ./gradlew lint • configure Lint as described here: http://goo.gl/xABHhy $ ./gradlew projects :projects ------------------------------------------------------------ Root project ------------------------------------------------------------ Root project ‘awsm_app' +--- Project ':app' +--- Project ':lintlib' \--- Project ':lintrules'
  24. 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);
 }
 }
  25. 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);
 }
 }
  26. 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);
 }
 }
  27. 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);
 }
 }
  28. 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");
 }
 }
  29. Debugging • not possible on real sources without building lint

    on your own • workaround is to debug on your tests
  30. Conclusion • not well documented API • sample project: https://goo.gl/TQt1jV

    • to improve code quality • help new team members