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

Building a Better Codebase with Lint - Droidcon APAC

Building a Better Codebase with Lint - Droidcon APAC

53b72671be580e70c9795c7eaf35ac12?s=128

Subhrajyoti Sen

December 14, 2020
Tweet

Transcript

  1. KeepTruckin Subhrajyoti Sen Droidcon APAC 2020 Building a Better Codebase

    with Lint
  2. What is Lint? < / >

  3. What is Lint? • Static analysis tool

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

  5. What is Lint? • Static analysis tool • Open Source

    • Works on .java, .kt, .gradle and a lot more
  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
  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
  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
  9. What does Lint look like? < / >

  10. Internationalization

  11. Security

  12. Performance

  13. Accessibility

  14. Why consider Lint? < / >

  15. Why consider Lint? • Opinionated codebase

  16. Why consider Lint? • Opinionated codebase • Simpler code reviews

  17. Why consider Lint? • Opinionated codebase • Simpler code reviews

    • Adherence to style guide
  18. Setup < / >

  19. Create a new module

  20. Dependencies dependencies { compileOnly "org.jetbrains.kotlin:kotlin stdlib:$kotlin_version" compileOnly 'com.android.tools.lint:lint api:27.1.1' compileOnly

    'com.android.tools.lint:lint checks:27.1.1' testImplementation "com.android.tools.lint:lint tests:27.1.1" } lint/build.gradle
  21. Issue Registry class Registry : IssueRegistry() { override val api:

    Int = CURRENT_API @get:NotNull override val issues: List<Issue> get() = listOf() }
  22. Register Issue Registry jar { manifest { attributes 'Lint-Registry-V2': 'me.subhrajyoti.lint.Registry'

    } } lint/build.gradle
  23. Add dependency on lint module dependencies { lintChecks project(':lint') }

    app/build.gradle
  24. The Basic Elements Scope The kind of files the lint

    rule applies to • JAVA_FILE_SCOPE • GRADLE_SCOPE • MANIFEST_SCOPE • PROGUARD_SCOPE • RESOURCE_FILE_SCOPE
  25. The Basic Elements Severity How severe is the issue •

    INFORMATIONAL • WARNING • ERROR • FATAL
  26. The Basic Elements Category The category the issue falls in,

    from a pre-defined list of categories • SECURITY • PERFORMANCE • L10N • A11Y
  27. Use case - Style Migration V1.TextView.Header1 V2.TextView.Header1 →

  28. Custom Rule Structure val ISSUE: Issue = Issue.create( "V1StyleUsageDetector", "Replace

    V1 styles with V2", "V1 styles have been deprecated and " + "you should use V2 styles for all TextView going forward", Category.CORRECTNESS, 5, Severity.ERROR, Implementation( V1StyleUsageDetector::class.java, Scope.RESOURCE_FILE_SCOPE ) )
  29. Custom Rule Structure class V1StyleUsageDetector : ResourceXmlDetector() { }

  30. Custom Rule Structure class V1StyleUsageDetector : ResourceXmlDetector() { override fun

    getApplicableElements(): Collection<String> { return listOf("TextView") } }
  31. Custom Rule Structure class V1StyleUsageDetector : ResourceXmlDetector() { override fun

    getApplicableElements(): Collection<String> { return listOf(SdkConstants.TEXT_VIEW) } }
  32. Custom Rule Structure class V1StyleUsageDetector : ResourceXmlDetector() { //.. override

    fun appliesTo(folderType: ResourceFolderType): Boolean { return folderType == ResourceFolderType.LAYOUT } }
  33. Custom Rule Structure class V1StyleUsageDetector : ResourceXmlDetector() { //.. override

    fun visitElement(context: XmlContext, element: Element) { if (element.hasAttribute(SdkConstants.ATTR_STYLE)) { val attributeValue = element.getAttribute(SdkConstants.ATTR_STYLE) if (attributeValue.startsWith("@style/V1")) { context.report( ISSUE, element, context.getLocation(element), "Replace V1 style with V2") } } }
  34. Issue Registry class Registry : IssueRegistry() { override val api:

    Int = CURRENT_API @get:NotNull override val issues: List<Issue> get() = listOf(V1StyleUsageDetector.ISSUE) }
  35. Lint x val newValue = element.getAttribute(SdkConstants.ATTR_STYLE).replace("V1", "V2") val compositeFix =

    LintFix.create() .name("Replace V1 with V2") .composite( LintFix.create() .unset(null, SdkConstants.ATTR_STYLE) .build(), LintFix.create() .set(null, SdkConstants.ATTR_STYLE, newValue) .build(), )
  36. Lint x context.report( ISSUE, element, context.getLocation(element), "Replace V1 style with

    V2", compositeFix )
  37. Auto x val newValue = element.getAttribute(SdkConstants.ATTR_STYLE).replace("V1", "V2") val compositeFix =

    LintFix.create() .name("Replace V1 with V2") .composite( LintFix.create() .unset(null, SdkConstants.ATTR_STYLE) .build(), LintFix.create() .set(null, SdkConstants.ATTR_STYLE, newValue) .build(), ) .autofix()
  38. Lint x ./gradlew app:lintFix

  39. Testing

  40. Testing @RunWith(JUnit4 class) class V1StyleUsageDetectorTest : LintDetectorTest() { override fun

    getIssues(): MutableList<Issue> = mutableListOf(V1StyleUsageDetector.ISSUE) override fun getDetector(): Detector = V1StyleUsageDetector() }
  41. @RunWith(JUnit4 class) class V1StyleUsageDetectorTest : LintDetectorTest() { @Test fun `check

    no lint error`() { lint() .files( xml( "res/layout/layout.xml", """ <TextView android:layout_width="match_parent" android:layout_height="match_parent" android:id="@ id/textSpacerNoButtons" style="@style/V2.TextAppearance.AppCompat" xmlns:android="http: schemas.android.com/apk/res/android" """ ) ) .run() .expectClean() }
  42. @Test fun `check exactly 1 lint error`() { lint() .files(

    xml( "res/layout/layout.xml", """ <TextView android:layout_width="match_parent" android:layout_height="match_parent" android:id="@ id/textSpacerNoButtons" style="@style/V1.TextAppearance.AppCompat" xmlns:android="http: schemas.android.com/apk/res/android" """ ).indented() ) }
  43. @Test fun `check exactly 1 lint error`() { lint() .files(

    xml( ).indented() ) .run() .expectErrorCount(1) .expect( """ res/layout/layout.xml:1 Error: Replace V1 style with V2 [V1StyleUsageDetector] <TextView android:layout_width="match_parent" ^ 1 errors, 0 warnings """ ) }
  44. Integration

  45. Integration • Incremental fixes

  46. Integration • Incremental fixes • Using a baseline

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

  48. Baseline - Pass 1

  49. Baseline - Pass 2

  50. lint-baseline.xml <issues format="5" by="lint 3.6.3" client="gradle" variant="debug" version="3.6.3"> <issue id="MissingStyleAttribute"

    message="Add style to TextView in order to provide consistent design" errorLine1=" &lt;TextView" errorLine2=" ^"> <location file="src/main/res/layout/activity_main.xml" line="10" column="5"/> </issue> </issues>
  51. Performance

  52. Performance • Single pass • Missing attribute • Fast •

    Enable in IDE
  53. Performance • Single pass • Missing attribute • Fast •

    Enable in IDE • Multiple pass • Unused resources • Slow • Disable in IDE
  54. Kotlin Considerations?

  55. Kotlin Considerations? • No explicit return statements

  56. Kotlin Considerations? • No explicit return statements • No explicit

    type declarations
  57. Kotlin Considerations? • No explicit return statements • No explicit

    type declarations • Write one test case for Kotlin usage
  58. References • https://groups.google.com/g/lint-dev • https://github.com/SubhrajyotiSen/LintTalk • https://github.com/vanniktech/lint-rules • https://www.youtube.com/watch?v=p8yX5-lPS6o •

    https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio- master-dev:lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/
  59. @iamsubhrajyoti