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

Better Code through Lint and Checkstyle

Better Code through Lint and Checkstyle

Presented at the droidcon NYC 2015:
http://droidcon.nyc/2015/dcnyc/25/

Nearly every Android developer has heard of the Lint and Checkstyle tools - however few use either to its full power, if at all. In addition to maintaining a consistent code style, we will see how to enforce architecture conventions and even prevent wrong usage of both internal and your own APIs.

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 session will show you how to configure Checkstyle and Lint to your liking, and how to use their APIs to create custom checks, as well as how to include both in your Gradle-based project.

Marc Prengemann

August 28, 2015
Tweet

More Decks by Marc Prengemann

Other Decks in Technology

Transcript

  1. About me Marc Prengemann Android Software Engineer Mail: [email protected] Wire:

    [email protected] Github: winterDroid Google+: Marc Prengemann Twitter: @winterDroid89
  2. When should I use it? • to ensure code quality

    • focus in reviews on real code • prevent people from misusing internal libraries … but what are the challenges? • Lint API is @Beta • getting familiar with the API • integrating within your Gradle Build • debugging / testing
  3. 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
  4. A real life example • Timber • logger by Jake

    Wharton • https://github.com/JakeWharton/ timber • want to create a detector that detects misuse of android.util.Log instead of Timber
  5. 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();
 String refName = ref.astIdentifier().astValue();
 if (!"Timber".equals(refName)) {
 context.report(ISSUE,
 node,
 context.getLocation(node),
 String.format("Use 'Timber' instead of '%s'",
 refName));
 }
 }
 } Detector • responsible for scanning through code and to find issues and report them
  6. 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();
 String refName = ref.astIdentifier().astValue();
 if (!"Timber".equals(refName)) {
 context.report(ISSUE,
 node,
 context.getLocation(node),
 String.format("Use 'Timber' instead of '%s'",
 refName));
 }
 }
 }
  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 • 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();
 String refName = ref.astIdentifier().astValue();
 if (!"Timber".equals(refName)) {
 context.report(ISSUE,
 node,
 context.getLocation(node),
 String.format("Use 'Timber' instead of '%s'",
 refName));
 }
 }
 }
  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 • 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();
 String refName = ref.astIdentifier().astValue();
 if (!"Timber".equals(refName)) {
 context.report(ISSUE,
 node,
 context.getLocation(node),
 String.format("Use 'Timber' instead of '%s'",
 refName));
 }
 }
 }
  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 • 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();
 String refName = ref.astIdentifier().astValue();
 if (!"Timber".equals(refName)) {
 context.report(ISSUE,
 node,
 context.getLocation(node),
 String.format("Use 'Timber' instead of '%s'",
 refName));
 }
 }
 }
  10. 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();
 String refName = ref.astIdentifier().astValue();
 if (!"Timber".equals(refName)) {
 context.report(ISSUE,
 node,
 context.getLocation(node),
 String.format("Use 'Timber' instead of '%s'",
 refName));
 }
 }
 }
  11. 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));
  12. 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));
  13. 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));
  14. 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));
  15. 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));
  16. 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));
  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 • 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));
  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 • 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));
  19. 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));
  20. 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);
 }
 }
  21. A real life example • Naming Conventions for variables •

    improve Readability public final static int FirstNumber = 0; public final static int SECOND_NUMBER = 0;
 private static int FirstNumber = 0;
 private static int sSecondNumber = 0; private int firstNumber = 0;
 private int mSecondNumber = 0; int FirstNumber = 0;
 int secondNumber = 0;
  22. A real life example • Naming Conventions for variables •

    improve Readability • different styles for • constant variables public final static int FirstNumber = 0; public final static int SECOND_NUMBER = 0;
 private static int FirstNumber = 0;
 private static int sSecondNumber = 0; private int firstNumber = 0;
 private int mSecondNumber = 0; int FirstNumber = 0;
 int secondNumber = 0;
  23. A real life example • Naming Conventions for variables •

    improve Readability • different styles for • constant variables • static variables public final static int FirstNumber = 0; public final static int SECOND_NUMBER = 0;
 private static int FirstNumber = 0;
 private static int sSecondNumber = 0; private int firstNumber = 0;
 private int mSecondNumber = 0; int FirstNumber = 0;
 int secondNumber = 0;
  24. A real life example • Naming Conventions for variables •

    improve Readability • different styles for • constant variables • static variables • member variables public final static int FirstNumber = 0; public final static int SECOND_NUMBER = 0; private static int FirstNumber = 0;
 private static int sSecondNumber = 0; private int firstNumber = 0;
 private int mSecondNumber = 0; int FirstNumber = 0;
 int secondNumber = 0;
  25. A real life example • Naming Conventions for variables •

    improve Readability • different styles for • constant variables • static variables • member variables • local variables public final static int FirstNumber = 0; public final static int SECOND_NUMBER = 0;
 private static int FirstNumber = 0;
 private static int sSecondNumber = 0; private int firstNumber = 0;
 private int mSecondNumber = 0; int FirstNumber = 0;
 int secondNumber = 0;
  26. Check • Visitor Pattern on the traversed Abstract Syntax Tree

    (AST) • Base class for checks • beginTree() • visitToken() • leaveToken() • finishTree() public class NamingConventionCheck extends Check {
 
 private static final String MSG_CONSTANT = "Constant '%s' " +
 "should be named in all uppercase with underscores.";
 
 @Override
 public int[] getDefaultTokens() {
 return new int[] {VARIABLE_DEF};
 }
 
 @Override
 public void visitToken(DetailAST ast) {
 final DetailAST modifier = ast.findFirstToken(MODIFIERS);
 final String name = ast.findFirstToken(IDENT)
 .getText();
 if (ScopeUtils.inInterfaceOrAnnotationBlock(ast) ||
 "serialVersionUID".equals(name)) {
 return;
 }
 
 if (modifier.branchContains(LITERAL_STATIC) &&
 modifier.branchContains(FINAL)) {
 if (NamingConventions.isWrongConstantNaming(name)) {
 log(ast.getLineNo(),
 ast.getColumnNo(),
 String.format(MSG_CONSTANT, name));
 }
 }
 }
 } public class NamingConventions {
 
 private static final Pattern WRONG_CONSTANT_NAME_PATTERN =
 Pattern.compile(".*[a-z].*");
 
 public static boolean isWrongConstantNaming(String variableName) {
 return WRONG_CONSTANT_NAME_PATTERN.matcher(variableName)
 .matches();
 }
 }
  27. Check • Visitor Pattern on the traversed Abstract Syntax Tree

    (AST) • Base class for checks • register for token types • see TokenTypes class • popular examples • CLASS_DEF • VARIABLE_DEF • METHOD_DEF public class NamingConventionCheck extends Check {
 
 private static final String MSG_CONSTANT = "Constant '%s' " +
 "should be named in all uppercase with underscores.";
 
 @Override
 public int[] getDefaultTokens() {
 return new int[] {VARIABLE_DEF};
 }
 
 @Override
 public void visitToken(DetailAST ast) {
 final DetailAST modifier = ast.findFirstToken(MODIFIERS);
 final String name = ast.findFirstToken(IDENT)
 .getText();
 if (ScopeUtils.inInterfaceOrAnnotationBlock(ast) ||
 "serialVersionUID".equals(name)) {
 return;
 }
 
 if (modifier.branchContains(LITERAL_STATIC) &&
 modifier.branchContains(FINAL)) {
 if (NamingConventions.isWrongConstantNaming(name)) {
 log(ast.getLineNo(),
 ast.getColumnNo(),
 String.format(MSG_CONSTANT, name));
 }
 }
 }
 } public class NamingConventions {
 
 private static final Pattern WRONG_CONSTANT_NAME_PATTERN =
 Pattern.compile(".*[a-z].*");
 
 public static boolean isWrongConstantNaming(String variableName) {
 return WRONG_CONSTANT_NAME_PATTERN.matcher(variableName)
 .matches();
 }
 }
  28. Check • Visitor Pattern on the traversed Abstract Syntax Tree

    (AST) • Base class for checks • register for token types • analyze the found tokens • DetailAST provides utility methods • possible to navigate around • don’t abuse that feature! Just look at the neighbours if necessary public class NamingConventionCheck extends Check {
 
 private static final String MSG_CONSTANT = "Constant '%s' " +
 "should be named in all uppercase with underscores.";
 
 @Override
 public int[] getDefaultTokens() {
 return new int[] {VARIABLE_DEF};
 }
 
 @Override
 public void visitToken(DetailAST ast) {
 final DetailAST modifier = ast.findFirstToken(MODIFIERS);
 final String name = ast.findFirstToken(IDENT)
 .getText();
 if (ScopeUtils.inInterfaceOrAnnotationBlock(ast) ||
 "serialVersionUID".equals(name)) {
 return;
 }
 
 if (modifier.branchContains(LITERAL_STATIC) &&
 modifier.branchContains(FINAL)) {
 if (NamingConventions.isWrongConstantNaming(name)) {
 log(ast.getLineNo(),
 ast.getColumnNo(),
 String.format(MSG_CONSTANT, name));
 }
 }
 }
 } public class NamingConventions {
 
 private static final Pattern WRONG_CONSTANT_NAME_PATTERN =
 Pattern.compile(".*[a-z].*");
 
 public static boolean isWrongConstantNaming(String variableName) {
 return WRONG_CONSTANT_NAME_PATTERN.matcher(variableName)
 .matches();
 }
 }
  29. Check • Visitor Pattern on the traversed Abstract Syntax Tree

    (AST) • Base class for checks • register for token types • analyze the found tokens • report the found issue • DetailAST provides location • supports internationalized error messages public class NamingConventionCheck extends Check {
 
 private static final String MSG_CONSTANT = "Constant '%s' " +
 "should be named in all uppercase with underscores.";
 
 @Override
 public int[] getDefaultTokens() {
 return new int[] {VARIABLE_DEF};
 }
 
 @Override
 public void visitToken(DetailAST ast) {
 final DetailAST modifier = ast.findFirstToken(MODIFIERS);
 final String name = ast.findFirstToken(IDENT)
 .getText();
 if (ScopeUtils.inInterfaceOrAnnotationBlock(ast) ||
 "serialVersionUID".equals(name)) {
 return;
 }
 
 if (modifier.branchContains(LITERAL_STATIC) &&
 modifier.branchContains(FINAL)) {
 if (NamingConventions.isWrongConstantNaming(name)) {
 log(ast.getLineNo(),
 ast.getColumnNo(),
 String.format(MSG_CONSTANT, name));
 }
 }
 }
 } public class NamingConventions {
 
 private static final Pattern WRONG_CONSTANT_NAME_PATTERN =
 Pattern.compile(".*[a-z].*");
 
 public static boolean isWrongConstantNaming(String variableName) {
 return WRONG_CONSTANT_NAME_PATTERN.matcher(variableName)
 .matches();
 }
 }
  30. Check • Visitor Pattern on the traversed Abstract Syntax Tree

    (AST) • Base class for checks • register for token types • analyze the found tokens • report the found issue • (optional) define check properties • uses JavaBean reflection • works for all primitive types • just use a setter public class NamingConventionCheck extends Check {
 
 private static final String MSG_CONSTANT = "Constant '%s' " +
 "should be named in all uppercase with underscores.";
 
 @Override
 public int[] getDefaultTokens() {
 return new int[] {VARIABLE_DEF};
 }
 
 @Override
 public void visitToken(DetailAST ast) {
 final DetailAST modifier = ast.findFirstToken(MODIFIERS);
 final String name = ast.findFirstToken(IDENT)
 .getText();
 if (ScopeUtils.inInterfaceOrAnnotationBlock(ast) ||
 "serialVersionUID".equals(name)) {
 return;
 }
 
 if (modifier.branchContains(LITERAL_STATIC) &&
 modifier.branchContains(FINAL)) {
 if (NamingConventions.isWrongConstantNaming(name)) {
 log(ast.getLineNo(),
 ast.getColumnNo(),
 String.format(MSG_CONSTANT, name));
 }
 }
 }
 } public class NamingConventions {
 
 private static final Pattern WRONG_CONSTANT_NAME_PATTERN =
 Pattern.compile(".*[a-z].*");
 
 public static boolean isWrongConstantNaming(String variableName) {
 return WRONG_CONSTANT_NAME_PATTERN.matcher(variableName)
 .matches();
 }
 }
  31. 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' } }
  32. 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'
  33. Project structure • lintrules • lintlib • app • has

    dependency to lintlib module • since lintlib is an Android library module, we can use the generated lint.jar • circumvent bug introduced in build tools version 1.2.0 https://goo.gl/1uCNSR $ ./gradlew projects :projects ------------------------------------------------------------ Root project ------------------------------------------------------------ Root project 'awsm_app' +--- Project ':app' +--- Project ':lintlib' \--- Project ':lintrules'
  34. Project structure • lintrules • lintlib • app • check

    your project using with ./gradlew :app:lintDebug • configure Lint as described here: http://goo.gl/xABHhy $ ./gradlew projects :projects ------------------------------------------------------------ Root project ------------------------------------------------------------ Root project 'awsm_app' +--- Project ':app' +--- Project ':lintlib' \--- Project ':lintrules'
  35. Project structure • custom-checkstyle • Java module • source of

    all custom checks • dependency to selected Checkstyle version $ ./gradlew projects :projects ------------------------------------------------------------ Root project ------------------------------------------------------------ Root project 'awsm_app' +--- Project ':app' \--- Project ':custom-checkstyle'
  36. Project structure • custom-checkstyle • app • uses Checkstyle plugin

    • holds configuration • module must be added with its fully qualified classname com.wire.NamingConventionCheck for example to the TreeWalker • has dependency to custom-checkstyle module in the Checkstyle configuration $ ./gradlew projects :projects ------------------------------------------------------------ Root project ------------------------------------------------------------ Root project 'awsm_app' +--- Project ':app' \--- Project ':custom-checkstyle'
  37. Project structure • custom-checkstyle • app • check your project

    using with ./gradlew :app:checkstyle • configure Checkstyle as described here: http://goo.gl/aDBeU9 $ ./gradlew projects :projects ------------------------------------------------------------ Root project ------------------------------------------------------------ Root project 'awsm_app' +--- Project ':app' \--- Project ':custom-checkstyle'
  38. Testing Lint Checks • part of lintrules module • register

    and execute tests as usual in build.gradle public class WrongTimberUsageTest extends LintDetectorTest {
 
 @Override
 protected Detector getDetector() {
 return new WrongTimberUsageDetector();
 }
 
 @Override
 protected List<Issue> getIssues() {
 return Arrays.asList(WrongTimberUsageDetector.ISSUE);
 }
 
 public void testLogTest() throws Exception {
 final String expected = ...;
 final TestFile testFile =
 java("src/test/pkg/WrongTimberTest.java",
 ...);
 assertEquals(expected, lintProject(testFile));
 }
 }
  39. • part of lintrules module • register and execute tests

    as usual in build.gradle • every test should extend LintDetectorTest • part of lint-tests dependency by com.android.tools.lint • tested with version 24.3.1 public class WrongTimberUsageTest extends LintDetectorTest {
 
 @Override
 protected Detector getDetector() {
 return new WrongTimberUsageDetector();
 }
 
 @Override
 protected List<Issue> getIssues() {
 return Arrays.asList(WrongTimberUsageDetector.ISSUE);
 }
 
 public void testLogTest() throws Exception {
 final String expected = ...;
 final TestFile testFile =
 java("src/test/pkg/WrongTimberTest.java",
 ...);
 assertEquals(expected, lintProject(testFile));
 }
 } Testing Lint Checks
  40. • part of lintrules module • register and execute tests

    as usual in build.gradle • every test should extend LintDetectorTest • need to define the to be used detector and the selected issue(s) public class WrongTimberUsageTest extends LintDetectorTest {
 
 @Override
 protected Detector getDetector() {
 return new WrongTimberUsageDetector();
 }
 
 @Override
 protected List<Issue> getIssues() {
 return Arrays.asList(WrongTimberUsageDetector.ISSUE);
 }
 
 public void testLogTest() throws Exception {
 final String expected = ...;
 final TestFile testFile =
 java("src/test/pkg/WrongTimberTest.java",
 ...);
 assertEquals(expected, lintProject(testFile));
 }
 } Testing Lint Checks
  41. • part of lintrules module • register and execute tests

    as usual in build.gradle • every test should extend LintDetectorTest • need to define the to be used detector and the selected issue(s) • perform usual unit tests with the helper methods including: • java - Create TestFile from source string • lintProject - Lint on given files when constructed as separate project public class WrongTimberUsageTest extends LintDetectorTest {
 
 @Override
 protected Detector getDetector() {
 return new WrongTimberUsageDetector();
 }
 
 @Override
 protected List<Issue> getIssues() {
 return Arrays.asList(WrongTimberUsageDetector.ISSUE);
 }
 
 public void testLogTest() throws Exception {
 final String expected = ...;
 final TestFile testFile =
 java("src/test/pkg/WrongTimberTest.java",
 ...);
 assertEquals(expected, lintProject(testFile));
 }
 } Testing Lint Checks
  42. Testing Lint Checks • part of custom-checkstyle module • register

    and execute tests as usual in build.gradle public class NamingConventionTest extends BaseCheckTestSupport {
 
 @Test
 public void testConstantNaming() throws Exception {
 final DefaultConfiguration checkConfig =
 createCheckConfig(NamingConventionCheck.class);
 final String[] expected = {
 "7:5: Constant test4 should be " +
 "named in all uppercase with underscores."
 };
 verify(checkConfig, getPath("ConstantName.java"), expected);
 }
 }
  43. Testing Lint Checks • part of custom-checkstyle module • register

    and execute tests as usual in build.gradle • every test should extend BaseCheckTestSupport • part of sevntu checks • contains some nice helper methods • createCheckConfig • getPath • getMessage public class NamingConventionTest extends BaseCheckTestSupport {
 
 @Test
 public void testConstantNaming() throws Exception {
 final DefaultConfiguration checkConfig =
 createCheckConfig(NamingConventionCheck.class);
 final String[] expected = {
 "7:5: Constant test4 should be " +
 "named in all uppercase with underscores."
 };
 verify(checkConfig, getPath("ConstantName.java"), expected);
 }
 }
  44. Testing Lint Checks • part of custom-checkstyle module • register

    and execute tests as usual in build.gradle • every test should extend BaseCheckTestSupport • need to create configuration for check public class NamingConventionTest extends BaseCheckTestSupport {
 
 @Test
 public void testConstantNaming() throws Exception {
 final DefaultConfiguration checkConfig =
 createCheckConfig(NamingConventionCheck.class);
 final String[] expected = {
 "7:5: Constant test4 should be " +
 "named in all uppercase with underscores."
 };
 verify(checkConfig, getPath("ConstantName.java"), expected);
 }
 }
  45. Testing Lint Checks • part of custom-checkstyle module • register

    and execute tests as usual in build.gradle • every test should extend BaseCheckTestSupport • need to create configuration for check • tests are performed on real Java files • part of test resources • should be in same package as test class public class NamingConventionTest extends BaseCheckTestSupport {
 
 @Test
 public void testConstantNaming() throws Exception {
 final DefaultConfiguration checkConfig =
 createCheckConfig(NamingConventionCheck.class);
 final String[] expected = {
 "7:5: Constant test4 should be " +
 "named in all uppercase with underscores."
 };
 verify(checkConfig, getPath("ConstantName.java"), expected);
 }
 }
  46. Testing Lint Checks • part of custom-checkstyle module • register

    and execute tests as usual in build.gradle • every test should extend BaseCheckTestSupport • need to create configuration for check • tests are performed on real Java files • verify for final test public class NamingConventionTest extends BaseCheckTestSupport {
 
 @Test
 public void testConstantNaming() throws Exception {
 final DefaultConfiguration checkConfig =
 createCheckConfig(NamingConventionCheck.class);
 final String[] expected = {
 "7:5: Constant test4 should be " +
 "named in all uppercase with underscores."
 };
 verify(checkConfig, getPath("ConstantName.java"), expected);
 }
 }
  47. Debugging • same for Checkstyle and Lint • prepare and

    run Gradle daemon ./gradlew --daemon $ ./gradle.properties ## Project-wide Gradle settings.
 #
 # For more details on how to configure your build environment visit
 # http://www.gradle.org/docs/current/userguide/build_environment.html
 #
 # Specifies the JVM arguments used for the daemon process.
 # The setting is particularly useful for tweaking memory settings.
 # Default value: -Xmx10248m -XX:MaxPermSize=256m
 # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX: +HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
 #
 # When configured, Gradle will run in incubating parallel mode.
 # This option should only be used with decoupled projects. More details, visit
 # http://www.gradle.org/docs/current/userguide/ multi_project_builds.html#sec:decoupled_projects
 # org.gradle.parallel=true
 org.gradle.jvmargs=-XX:MaxPermSize=4g \
 -XX:+HeapDumpOnOutOfMemoryError \
 -Xmx4g \
 -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000
  48. Debugging • same for Checkstyle and Lint • prepare and

    run Gradle daemon • create new debug configuration in IntelliJ
  49. Debugging • same for Checkstyle and Lint • prepare and

    run Gradle daemon • create new debug configuration in IntelliJ • debug newly created configuration
  50. Conclusion • Lint and Checkstyle do not have a well

    documented API • sample project: https://goo.gl/QBfyf8 • Checkstyle is easier to use and to integrate • Lint seems to be more flexible ➡ to improve code quality ➡ help new team members • integration of Lint Checks may become easier with https://goo.gl/d7U1qS