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 Slide

  2. What is Lint?

    View Slide

  3. What is Lint?
    ● Static analysis tool

    View Slide

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

    View Slide

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

    View 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 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 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 Slide

  9. What does it look like?

    View Slide

  10. Internationalization

    View Slide

  11. Security

    View Slide

  12. Performance

    View Slide

  13. Accessibility

    View Slide

  14. Why Consider Lint

    View Slide

  15. Why Consider Lint
    ● Opinionated codebase

    View Slide

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

    View Slide

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

    View Slide

  18. Setup

    View Slide

  19. 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

  23. 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 Slide

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

    View Slide

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

    View Slide

  26. A Use case - TextView

    View Slide

  27. 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  32. 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 Slide

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

    View Slide

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

    View Slide

  35. A small Bug

    View Slide

  36. A Use case - Snackbar

    View Slide

  37. 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 Slide

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

    View Slide

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

    View Slide

  40. 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 Slide

  41. Testing a rule

    View Slide

  42. Testing a rule
    ● JUnit tests

    View Slide

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

    View Slide

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

    View Slide

  45. IDE syntax check

    View Slide

  46. 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 Slide

  47. A successful case

    View Slide

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

    View Slide

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

    View Slide

  50. @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 Slide

  51. @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 Slide

  52. @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 Slide

  53. @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 Slide

  54. An error case

    View Slide

  55. @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 Slide

  56. @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 Slide

  57. @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 Slide

  58. Integration

    View Slide

  59. Integration
    ● Incremental fixes

    View Slide

  60. Integration
    ● Incremental fixes
    ● Use a baseline

    View Slide

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

    View Slide

  62. Baseline

    View Slide

  63. Baseline

    View Slide

  64. Baseline

    View Slide

  65. Performance

    View Slide

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

    View Slide

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

    View Slide

  68. Kotlin Considerations
    ● No explicit return statements

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  72. Thank You
    @iamsubhrajyoti

    View Slide