정상에서 IT를 외치다

[Android, Custom Lint] 커스텀 린트 적용기 (구글 예제) 본문

안드로이드

[Android, Custom Lint] 커스텀 린트 적용기 (구글 예제)

Black-Jin 2022. 10. 11. 12:46
반응형

안녕하세요. 블랙진입니다.

 

정적 분석 도구인 린트에 대해서 알아보고 커스텀 린트 관련 구글 예제를 같이 살펴보고자 합니다. 

린트란?

린트는 정적 분석 도구 (static analysis tool) 라고 부릅니다. 정확성, 퍼포먼스, 안정성, 국제 규격, 사용성 등등의 오류나 버그를 컴파일 타임에서 알 수 있게 해 줍니다. 이러한 린트를 통해 컨벤션에 맞는 코드를 작성할 수 있게 도와주고 버그 또한 사전에 발견하여 프로그래머가 좀 더 코딩에 집중할 수 있게 도와줍니다. 이를 통해 코딩 중에 자주 하는 실수를 피함으로써 안전하고 더욱 멋진 코드를 만드는데 없어서는 안 될 존재가 바로 린트입니다.

 

좋은 코드를 제안하는 린트
버그를 알려주는 린트

커스텀 린트 적용하기

구글 예제를 통해 커스텀 린트를 적용해 보도록 해보겠습니다. 커스텀 린트를 구현하기 위해서는 2개의 모듈을 추가해 주어야 합니다. 

 

https://googlesamples.github.io/android-custom-lint-rules/api-guide.html#example:samplelintcheckgithubproject/projectlayout

 

 

1. 코틀린 라이브러리 모듈 추가 ( : checks )

checks는 순수 코틀린 또는 자바 모듈입니다. 실제 린트와 관련된 코드는 여기에 작성합니다.

 

환경 설정을 위해 아래와 같이 build.gradle 작성해 줍니다.

 

- com.android.lint 플러그인 추가

plugins {
    id 'java-library'
    id 'kotlin'
    id 'com.android.lint'
}

 

- lint 의존성 추가

 

린트 버전은 Android Gradle Plugin (AGP) Version에 +23 을 해줍니다. AGP Version을 따로 설정해 주지 않으신 분은 아래와 같이 Project Structure에서 확인할 수 있습니다. 만약 AGP 버전이 7.2.2이라면 린트 버전은 30.2.2 가 됩니다

 

Project Structure

 

이때 컴파일에서만 동작될 수 있게 compileOnly를 사용하여 추가해주어야 합니다.

dependencies {
    def lintVersion = '30.2.2'
    compileOnly "com.android.tools.lint:lint-api:$lintVersion"
    compileOnly "com.android.tools.lint:lint-checks:$lintVersion"

    def kotlinVersion = '1.7.10'
    compileOnly "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
}

 

- 자바 버전 설정을 위해 아래 값을 추가

sourceCompatibility = "1.8"
targetCompatibility = "1.8"

 

이렇게 설정해 주시면 checks 모듈 설정은 완료됩니다. 아래는 checks 모듈의 전체 코드입니다.

plugins {
    id 'java-library'
    id 'kotlin'
    id 'com.android.lint'
}

dependencies {
    def lintVersion = '30.2.2'
    compileOnly "com.android.tools.lint:lint-api:$lintVersion"
    compileOnly "com.android.tools.lint:lint-checks:$lintVersion"

    def kotlinVersion = '1.7.10'
    compileOnly "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
}

sourceCompatibility = "1.8"
targetCompatibility = "1.8"

 

2. 안드로이드 라이브러리 모듈을 추가 ( : library )

library는 안드로이드 모듈입니다. 여기에는 아무런 코드도 없습니다. 단지 build.gradle 설정만 아래와 같이 추가해줍니다.

 

- checks 의존성 추가

 

새로 생성해준 안드로이드 모듈에 위에서 생성한 checks 코틀린 모듈의 의존성을 lintPublish를 사용해 추가해 줍니다. 

dependencies {
    implementation project(':checks')
    lintPublish project(':checks')
}

Gradle plugin은 lintPublish 요소를 제공하며 이는 checks 모듈의 결과를 가져와 "lint.jar" 파일을 AAR 파일안에 넣습니다. 이것이 완료되면 다른 프로젝트에서는 lintlibrary를 의존함으로써 자동으로 checks에 구현되어 있는 린트를 적용할 수 있습니다.

 

3. App 모듈에 library Dependency 추가

마지막으로 app 모듈에 위에서 생성했던 library 모듈을 추가해 줍니다. 라이브러리 추가 시 compileOnly와 implemnt 둘 다 동작됩니다.

dependencies {
    implementation project(':library')
}

 

4. 커스텀 린트 등록

이제 커스텀 린트를 등록할 준비는 모두 마쳤습니다. 구글 예제에서는 lint라는 문구가 있는 경우 오류 메시지를 보여줍니다. 이를 위한 IssueRegistryDector 두 개의 클래스를 생성해 주어야 합니다. 실재 린트 검사를 위해 필요한 코드는 checks 모듈에서 생성해 줍니다.

 

- IssueRegistry 생성

 

IssueRegistry에 대한 자세한 설명은 링크를 참고해주세요.

구글 예제 SampleIssueRegistry 링크

더보기
/*
 * The list of issues that will be checked when running <code>lint</code>.
 */
@Suppress("UnstableApiUsage")
class SampleIssueRegistry : IssueRegistry() {
    override val issues = listOf(SampleCodeDetector.ISSUE)

    override val api: Int
        get() = CURRENT_API

    override val minApi: Int
        get() = 8 // works with Studio 4.1 or later; see com.android.tools.lint.detector.api.Api / ApiKt

    // Requires lint API 30.0+; if you're still building for something
    // older, just remove this property.
    override val vendor: Vendor = Vendor(
        vendorName = "Android Open Source Project",
        feedbackUrl = "https://github.com/googlesamples/android-custom-lint-rules/issues",
        contact = "https://github.com/googlesamples/android-custom-lint-rules"
    )
}

 

 

- Detector 생성

 

Detector에 대한 자세한 설명은 링크를 참고해주세요.

구글 예제 SampleCodeDector 링크

더보기
/**
 * Sample detector showing how to analyze Kotlin/Java code. This example
 * flags all string literals in the code that contain the word "lint".
 */
@Suppress("UnstableApiUsage")
class  SampleCodeDetector : Detector(), UastScanner {
    override fun getApplicableUastTypes(): List<Class<out UElement?>> {
        return listOf(ULiteralExpression::class.java)
    }

    override fun createUastHandler(context: JavaContext): UElementHandler {
        return object : UElementHandler() {
            override fun visitLiteralExpression(node: ULiteralExpression) {
                val string = node.evaluateString() ?: return
                if (string.contains("lint") && string.matches(Regex(".*\\blint\\b.*"))) {
                    context.report(
                        ISSUE, node, context.getLocation(node),
                        "This code mentions `lint`: **Congratulations**"
                    )
                }
            }
        }
    }

    companion object {
        /**
         * Issue describing the problem and pointing to the detector
         * implementation.
         */
        @JvmField
        val ISSUE: Issue = Issue.create(
            // ID: used in @SuppressLint warnings etc
            id = "SampleId",
            // Title -- shown in the IDE's preference dialog, as category headers in the
            // Analysis results window, etc
            briefDescription = "Lint Mentions",
            // Full explanation of the issue; you can use some markdown markup such as
            // `monospace`, *italic*, and **bold**.
            explanation = """
                    This check highlights string literals in code which mentions the word `lint`. \
                    Blah blah blah.

                    Another paragraph here.
                    """, // no need to .trimIndent(), lint does that automatically
            category = Category.CORRECTNESS,
            priority = 6,
            severity = Severity.WARNING,
            implementation = Implementation(
                SampleCodeDetector::class.java,
                Scope.JAVA_FILE_SCOPE
            )
        )
    }
}

 

5. META-INF 등록

checks 모듈에서 resources/META-INF/services 아래에 com.android.tools.lint.client.api.IssueRegistry 파일을 생성해 줍니다. 

 

issueRegistry 추가해준다.

위 파일 안에는 4번에서 생성한 IssueRegustry를 등록해 주어야 합니다. 현재 제 예제의 패키지 명은 com.example 이기 때문에 SampleIssueRegistry 파일을 아래와 같이 등록해 줍니다.

com.example.checks.SampleIssueRegistry

 

6. 실행하기

Sync Project with Gradle File을 해주시면 모든 변경 사항이 적용됩니다. 그다음 터미널을 실행시켜 app 모듈의 lint를 동작시켜 줍니다.

$ ./gradlew :app:lint

 

위 예제의 Detector에서는 아래와 같은 커스텀 린트 설정이 적용되어 있습니다.

override fun visitLiteralExpression(node: ULiteralExpression) {
    val string = node.evaluateString() ?: return
    if (string.contains("lint") && string.matches(Regex(".*\\blint\\b.*"))) {
        context.report(
            ISSUE, node, context.getLocation(node),
            "This code mentions `lint`: **Congratulations**"
        )
    }
}

lint 문구가 있으면 This code mentions `lint` : **Congratulations** 를 보여주라고 되어 있는데 실재 프로젝트에서 잘 적용되는 것을 확인할 수 있었습니다. 

 

lint 문구에 오류 린트가 적용 되었다.

 

마무리

구글 예제를 다운로드하여서 실행해 보면 위와 같이 잘 동작됨을 확인할 수 있습니다. 하지만 이를 다른 프로젝트에 설정하는 과정에서 생각한 대로 잘 동작되지 않았습니다. 이러한 경험을 바탕으로 step by step 형식으로 환경 설정하는 글을 작성해 보았는데요. 이제 환경 설정을 했으니 제 입맛대로 여러 커스텀 린트를 생성해 볼러고 합니다 :)

 

참고 링크

https://googlesamples.github.io/android-custom-lint-rules/api-guide.html

 

Android Lint API Guide

 

googlesamples.github.io

https://github.com/googlesamples/android-custom-lint-rules

 

GitHub - googlesamples/android-custom-lint-rules: This sample demonstrates how to create a custom lint checks and corresponding

This sample demonstrates how to create a custom lint checks and corresponding lint tests - GitHub - googlesamples/android-custom-lint-rules: This sample demonstrates how to create a custom lint che...

github.com

 

 

반응형
Comments