The Power of Custom Lint Checks - droidcon Berlin 2015

39
The Power of Custom Lint Checks droidcon Berlin | 05.06.2015 | Marc Prengemann

Transcript of The Power of Custom Lint Checks - droidcon Berlin 2015

Page 1: The Power of Custom Lint Checks - droidcon Berlin 2015

The Power of Custom Lint Checksdroidcon Berlin | 05.06.2015 | Marc Prengemann

Page 2: The Power of Custom Lint Checks - droidcon Berlin 2015

About me

Marc Prengemann Working student

Mail: [email protected] Wire: [email protected] Github: winterDroid Google+: Marc Prengemann

Page 3: The Power of Custom Lint Checks - droidcon Berlin 2015

Lint? What?

Page 4: The Power of Custom Lint Checks - droidcon Berlin 2015

What is Lint?

http://developer.android.com/tools/help/lint.html

Page 5: The Power of Custom Lint Checks - droidcon Berlin 2015

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

Page 6: The Power of Custom Lint Checks - droidcon Berlin 2015

Getting started with own Checks

Page 7: The Power of Custom Lint Checks - droidcon Berlin 2015

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

Page 8: The Power of Custom Lint Checks - droidcon Berlin 2015

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

Page 9: The Power of Custom Lint Checks - droidcon Berlin 2015

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'"); } }}

Page 10: The Power of Custom Lint Checks - droidcon Berlin 2015

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'"); } }}

Page 11: The Power of Custom Lint Checks - droidcon Berlin 2015

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'"); } }}

Page 12: The Power of Custom Lint Checks - droidcon Berlin 2015

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'"); } }}

Page 13: The Power of Custom Lint Checks - droidcon Berlin 2015

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'"); } }}

Page 14: The Power of Custom Lint Checks - droidcon Berlin 2015

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'"); } }}

Page 15: The Power of Custom Lint Checks - droidcon Berlin 2015

Issue

• potential bug in an Android application

• is discovered by a Detector

• are exposed to the userpublic 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));

Page 16: The Power of Custom Lint Checks - droidcon Berlin 2015

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));

Page 17: The Power of Custom Lint Checks - droidcon Berlin 2015

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));

Page 18: The Power of Custom Lint Checks - droidcon Berlin 2015

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));

Page 19: The Power of Custom Lint Checks - droidcon Berlin 2015

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));

Page 20: The Power of Custom Lint Checks - droidcon Berlin 2015

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));

Page 21: The Power of Custom Lint Checks - droidcon Berlin 2015

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));

Page 22: The Power of Custom Lint Checks - droidcon Berlin 2015

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));

Page 23: The Power of Custom Lint Checks - droidcon Berlin 2015

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));

Page 24: The Power of Custom Lint Checks - droidcon Berlin 2015

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); } }

Page 25: The Power of Custom Lint Checks - droidcon Berlin 2015

Include within your Project

Page 26: The Power of Custom Lint Checks - droidcon Berlin 2015

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' } }

Page 27: The Power of Custom Lint Checks - droidcon Berlin 2015

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'

Page 28: The Power of Custom Lint Checks - droidcon Berlin 2015

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'

Page 29: The Power of Custom Lint Checks - droidcon Berlin 2015

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'

Page 30: The Power of Custom Lint Checks - droidcon Berlin 2015

Further information

Page 31: The Power of Custom Lint Checks - droidcon Berlin 2015

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); }}

Page 32: The Power of Custom Lint Checks - droidcon Berlin 2015

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); }}

Page 33: The Power of Custom Lint Checks - droidcon Berlin 2015

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); }}

Page 34: The Power of Custom Lint Checks - droidcon Berlin 2015

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); } }

Page 35: The Power of Custom Lint Checks - droidcon Berlin 2015

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"); } }

Page 36: The Power of Custom Lint Checks - droidcon Berlin 2015

Debugging

• not possible on real sources without building lint on your own

• workaround is to debug on your tests

Page 37: The Power of Custom Lint Checks - droidcon Berlin 2015

Conclusion

• not well documented API

• sample project:

https://goo.gl/TQt1jV

• to improve code quality

• help new team members

Page 38: The Power of Custom Lint Checks - droidcon Berlin 2015

Questions?

Page 39: The Power of Custom Lint Checks - droidcon Berlin 2015

Thank you!