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

Building a Better Codebase with Lint - Kotlin Mumbai

Building a Better Codebase with Lint - Kotlin Mumbai

Lint is a powerful static analysis tool that can be used to enforce coding styles and also find common bugs across a codebase. Google provides a bunch of lint checks by default for Android. On popular demand, the Lint API was extended to support non-Android Kotlin projects too.

In this talk, Subhrajyoti discusses some examples of custom lint checks we use on the Tickertape app to enforce out design guidelines. He will go through the source code of some Android lint checks and then show how to write a custom lint rule. This talk also demonstrates how to write test cases for the rules. By the end of the talk, you will be able to understand how lint works and how you can add custom lint rules to your projects.

Subhrajyoti Sen

August 22, 2020
Tweet

More Decks by Subhrajyoti Sen

Other Decks in Technology

Transcript

  1. Building a Better Codebase
    with Lint
    Subhrajyoti Sen
    @iamsubhrajyoti

    View full-size slide

  2. What is Lint?

    View full-size slide

  3. What is Lint?
    ● Static analysis tool

    View full-size slide

  4. What is Lint?
    ● Static analysis tool
    ● Open Source

    View full-size slide

  5. What is Lint?
    ● Static analysis tool
    ● Open Source
    ● Works on .java, .kt, .gradle and a lot more

    View full-size slide

  6. What is Lint?
    ● Static analysis tool
    ● Open Source
    ● Works on .java, .kt, .gradle and a lot more
    ● Can be used outside Android as well

    View full-size slide

  7. What is Lint?
    ● Static analysis tool
    ● Open Source
    ● Works on .java, .kt, .gradle and a lot more
    ● Can be used outside Android as well
    ● Can be extended to write custom rules

    View full-size slide

  8. What is Lint?
    ● Static analysis tool
    ● Open Source
    ● Works on .java, .kt, .gradle and a lot more
    ● Can be used outside Android as well
    ● Can be extended to write custom rules
    ● Can be run from both Android Studio and CLI

    View full-size slide

  9. What does it look like?

    View full-size slide

  10. Internationalization

    View full-size slide

  11. Accessibility

    View full-size slide

  12. Why Consider Lint

    View full-size slide

  13. Why Consider Lint
    ● Opinionated codebase

    View full-size slide

  14. Why Consider Lint
    ● Opinionated codebase
    ● Simpler code reviews

    View full-size slide

  15. Why Consider Lint
    ● Opinionated codebase
    ● Simpler code reviews
    ● Adherence to style guide

    View full-size slide

  16. Setup
    dependencies {
    compileOnly "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    compileOnly 'com.android.tools.lint:lint-api:26.6.3'
    compileOnly 'com.android.tools.lint:lint-checks:26.6.3'
    }

    View full-size slide

  17. Setup
    class Registry : IssueRegistry() {
    override val api: Int = CURRENT_API
    @get:NotNull
    override val issues: List
    get() = listOf()
    }

    View full-size slide

  18. Setup
    jar {
    manifest {
    attributes 'Lint-Registry-V2': 'me.subhrajyoti.lint.Registry'
    }
    }

    View full-size slide

  19. Setup
    dependencies {
    lintChecks project(':lint')
    }

    View full-size slide

  20. The Basic Elements
    Scope
    The kind of files the lint rule will apply to
    ● JAVA_FILE_SCOPE (also applied to Kotlin)
    ● GRADLE_SCOPE
    ● MANIFEST_SCOPE
    ● PROGUARD_SCOPE
    ● RESOURCE_FILE_SCOPE

    View full-size slide

  21. The Basic Elements
    Severity
    How severe is the issue
    ● INFORMATIONAL
    ● WARNING
    ● ERROR
    ● FATAL

    View full-size slide

  22. The Basic Elements
    Category
    The category the issue falls in, from a pre-defined list of categories
    ● SECURITY
    ● PERFORMANCE
    ● I18N
    ● A11Y

    View full-size slide

  23. A Use case - TextView

    View full-size slide

  24. A Custom Rule Structure
    val ISSUE: Issue = Issue.create(
    "MissingStyleAttribute",
    "style attribute is missing",
    "We should use style to style a TextView " +
    "in order to provide consistent design",
    Category.CORRECTNESS,
    5,
    Severity.ERROR,
    Implementation(
    MissingStyleAttributeDetector::class.java,
    Scope.RESOURCE_FILE_SCOPE
    )
    )

    View full-size slide

  25. A Custom Rule Structure
    class MissingStyleAttributeDetector : ResourceXmlDetector() {
    }

    View full-size slide

  26. A Custom Rule Structure
    class MissingStyleAttributeDetector : ResourceXmlDetector() {
    @Nullable
    override fun getApplicableElements(): Collection? {
    return listOf(“TextView”)
    }
    }

    View full-size slide

  27. A Custom Rule Structure
    class MissingStyleAttributeDetector : ResourceXmlDetector() {
    @Nullable
    override fun getApplicableElements(): Collection? {
    return listOf(SdkConstants.TEXT_VIEW)
    }
    }

    View full-size slide

  28. A Custom Rule Structure
    class MissingStyleAttributeDetector : ResourceXmlDetector() {
    override fun appliesTo(folderType: ResourceFolderType): Boolean {
    return folderType == ResourceFolderType.LAYOUT
    }
    }

    View full-size slide

  29. class MissingStyleAttributeDetector : ResourceXmlDetector() {
    override fun visitElement(context: XmlContext, element: Element) {
    if (!element.hasAttribute(SdkConstants.ATTR_STYLE)) {
    context.report(
    ISSUE,
    element,
    context.getLocation(element),
    "Add style to TextView"
    )
    }
    }
    }

    View full-size slide

  30. A Custom Rule Structure
    class Registry : IssueRegistry() {
    @get:NotNull
    override val issues: List
    get() = listOf(MissingStyleAttributeDetector.ISSUE)
    }

    View full-size slide

  31. Remember!
    class MissingStyleAttributeDetector : ResourceXmlDetector() {
    @Nullable
    override fun getApplicableElements(): Collection? {
    return listOf(SdkConstants.TEXT_VIEW)
    }
    }

    View full-size slide

  32. A Use case - Snackbar

    View full-size slide

  33. A Custom Rule Structure
    val ISSUE = Issue.create(
    "CustomSnackbarIssue",
    DESCRIPTION,
    EXPLANATION,
    Category.CORRECTNESS,
    5,
    Severity.ERROR,
    Implementation(
    MaterialSnackbarUsageDetector::class.java,
    Scope.JAVA_FILE_SCOPE
    )
    )

    View full-size slide

  34. A Custom Rule Structure
    class MaterialSnackbarUsageDetector : Detector(), SourceCodeScanner {
    }

    View full-size slide

  35. A Custom Rule Structure
    class MaterialSnackbarUsageDetector : Detector(), SourceCodeScanner {
    override fun getApplicableMethodNames() = listOf("make")
    }

    View full-size slide

  36. A Custom Rule Structure
    class MaterialSnackbarUsageDetector : Detector(), SourceCodeScanner {
    override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
    if (context.evaluator.isMemberInClass(
    method,
    "com.google.android.material.snackbar.Snackbar"
    )) {
    // report usage
    }
    }
    }

    View full-size slide

  37. Testing a rule

    View full-size slide

  38. Testing a rule
    ● JUnit tests

    View full-size slide

  39. Testing a rule
    ● JUnit tests
    ● lint-tests library

    View full-size slide

  40. Testing a rule
    ● JUnit tests
    ● lint-tests library
    ● IDE syntax checker and code highlight

    View full-size slide

  41. IDE syntax check

    View full-size slide

  42. lint-test dependency
    dependencies {
    compileOnly 'com.android.tools.lint:lint-api:26.6.3'
    compileOnly 'com.android.tools.lint:lint-checks:26.6.3'
    testImplementation "com.android.tools.lint:lint-tests:26.6.3"
    }

    View full-size slide

  43. A successful case

    View full-size slide

  44. @Test
    fun `check no lint error`() {
    lint()
    }

    View full-size slide

  45. @Test
    fun `check no lint error`() {
    lint()
    .files()
    }

    View full-size slide

  46. @Test
    fun `check no lint error`() {
    lint()
    .files(
    xml(
    "res/layout/layout.xml",
    """
    android:layout_height="match_parent"
    android:id="@+id/textSpacerNoButtons"
    style="@style/TextAppearance.AppCompat"
    xmlns:android="http://schemas.android.com/apk/res/android" />
    """
    )
    )
    }

    View full-size slide

  47. @Test
    fun `check no lint error`() {
    lint()
    .files(
    xml(
    "res/layout/layout.xml",
    """
    android:layout_height="match_parent"
    android:id="@+id/textSpacerNoButtons"
    style="@style/TextAppearance.AppCompat"
    xmlns:android="http://schemas.android.com/apk/res/android" />
    """
    )
    )
    .run()
    }

    View full-size slide

  48. @Test
    fun `check no lint error`() {
    lint()
    .files(
    xml(
    "res/layout/layout.xml",
    """
    android:layout_height="match_parent"
    android:id="@+id/textSpacerNoButtons"
    style="@style/TextAppearance.AppCompat"
    xmlns:android="http://schemas.android.com/apk/res/android" />
    """
    )
    )
    .run()
    .expectClean()
    }

    View full-size slide

  49. @Test
    fun `check no lint error`() {
    lint()
    .files(
    xml(
    "res/layout/layout.xml",
    """
    android:layout_height="match_parent"
    android:id="@+id/textSpacerNoButtons"
    style="@style/TextAppearance.AppCompat"
    xmlns:android="http://schemas.android.com/apk/res/android" />
    """
    )
    )
    .run()
    .expectClean()
    }

    View full-size slide

  50. An error case

    View full-size slide

  51. @Test
    fun `check exactly 1 lint error`() {
    lint()
    .files(
    xml(
    "res/layout/layout.xml",
    """
    android:layout_height="match_parent"
    android:id="@+id/textSpacerNoButtons"
    xmlns:android="http://schemas.android.com/apk/res/android" />
    """
    )
    )
    .run()
    }

    View full-size slide

  52. @Test
    fun `check exactly 1 lint error`() {
    lint()
    .files(
    xml(
    "res/layout/layout.xml",
    """
    android:layout_height="match_parent"
    android:id="@+id/textSpacerNoButtons"
    xmlns:android="http://schemas.android.com/apk/res/android" />
    """
    )
    )
    .run()
    .expectErrorCount(1)
    }

    View full-size slide

  53. @Test
    fun `check exactly 1 lint error`() {
    lint()
    //
    .run()
    .expectErrorCount(1)
    .expect(
    """
    res/layout/layout.xml:2: Error: Add style to TextView in order to provide consistent design
    [MissingStyleAttribute]
    ^
    1 errors, 0 warnings
    """
    )
    }

    View full-size slide

  54. Integration
    ● Incremental fixes

    View full-size slide

  55. Integration
    ● Incremental fixes
    ● Use a baseline

    View full-size slide

  56. Baseline
    android {
    lintOptions {
    baseline file("lint-baseline.xml")
    }
    }

    View full-size slide

  57. Performance
    ● Single-pass:
    ○ Missing attribute
    ○ Fast
    ○ Enable in IDE

    View full-size slide

  58. Performance
    ● Single-pass:
    ○ Missing attribute
    ○ Fast
    ○ Enable in IDE
    ● Multiple-pass :
    ○ Unused resources
    ○ Slow
    ○ Disable in IDE

    View full-size slide

  59. Kotlin Considerations
    ● No explicit return statements

    View full-size slide

  60. Kotlin Considerations
    ● No explicit return statements
    ● No explicit type declarations

    View full-size slide

  61. Kotlin Considerations
    ● No explicit return statements
    ● No explicit type declarations
    ● Write one test case for Kotlin

    View full-size slide

  62. References
    ● https://groups.google.com/g/lint-dev
    ● https://github.com/SubhrajyotiSen/LintTalk/
    ● https://www.youtube.com/watch?v=p8yX5-lPS6o

    View full-size slide

  63. Thank You
    @iamsubhrajyoti

    View full-size slide