The Power of Custom Lint Checks - droidcon Berlin 2015
-
Upload
marc-prengemann -
Category
Engineering
-
view
226 -
download
1
Transcript of The Power of Custom Lint Checks - droidcon Berlin 2015
The Power of Custom Lint Checksdroidcon Berlin | 05.06.2015 | Marc Prengemann
About me
Marc Prengemann Working student
Mail: [email protected] Wire: [email protected] Github: winterDroid Google+: Marc Prengemann
Lint? What?
What is Lint?
http://developer.android.com/tools/help/lint.html
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
Getting started with own Checks
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
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
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'"); } }}
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'"); } }}
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'"); } }}
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'"); } }}
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'"); } }}
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'"); } }}
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));
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));
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));
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));
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));
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));
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));
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));
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));
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); } }
Include within your Project
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' } }
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'
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'
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'
Further information
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); }}
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); }}
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); }}
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); } }
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"); } }
Debugging
• not possible on real sources without building lint on your own
• workaround is to debug on your tests
Conclusion
• not well documented API
• sample project:
https://goo.gl/TQt1jV
• to improve code quality
• help new team members
Questions?
Thank you!