Coding in Style: Static Analysis with Custom Lint Rules ( from Dev Summit 19 ) |
* Initial project set-up
-
module 을 만들어서 java-library 로 지정하고, aar 로 패키징해서 배포한다.
-
apply plugin: 'java-library' apply plugin: 'kotlin' ext { lintVersion = "26.5.1" // android gradle version 에 23 을 더해준다. (걍 그렇게 되었다고 함) } dependencies { compileOnly "org.jetbrains.kotlin:kotlin-stdlib-jdk8" // Lint compileOnly "com.android.tools.lint:lint-api:$lintVersion" compileOnly "com.android.tools.lint:lint-checks:$lintVersion" // Lint Testing testImplementation "com.android.tools.lint:lint:$lintVersion" testImplementation "com.android.tools.lint:lint-tests$lintVersion" testImplementation "junit:junit:4.12" }
cf) 참고로 compileOnly 는 compile 시에만 사용하며, artifact 로 만들 때는 포함하지 않는다.
-
apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { ... } dependencies { lintPublish project(path: ':lint') // auto lint apply implementation 'androidx.appcompat:appcompat:1.1.0' testImplementation 'junit:junit:4.12' // ... }
-
... android { ... } dependencies { ... implementation project(':library') // aar // or lintChecks project(path: ':lint') // jar }
* IssueRegistry
-
IssueRegistry 는 Issue 들의 collection 이다.
Issue 는 detect 하고 싶은 것을 말하며, detect 하는 로직을 가진 issue detector 들이 구현되어야 한다.
IssueRegistry 의 위치는 "resources/META-INF/services/com.android.tools.intt.client.api.IssueRegistry" 가 되어야 한다.
-
class IssueRegistry : IssueRegistry() { override val issues: List<Issue> get() = listOf( NoisyIssue, BadConfigurationProviderIssue, RemoveWorkManagerInitializerIssue ) }
-
val NoisyIssue = Issue.create( id = "NoisyIssueId", briefDescription = "This is noisy issue", explanation = "This is noisy issue. It detects something noisy", category = Category.CORRECTNESS, severity = Severity.INFORMATIONAL, implementation = Implementation( NoisyDetector::class.java, Scope.MANIFEST_SCOPE // AndroidManifest.xml 을 검사할 것이다. )
-
Detector 는 callback style 로 관심있는 부분만 확인 할 수 있다.
// 이 구현은 모든 manifest 파일에 대해 무조건 complain 을 한다. class NoisyDetector : Detector(), XmlScanner{ override fun getApplicableElements() = listOf("manifest") override fun visitElement(context: XmlContext, element:Element){ ... } override fun afterCheckFile(context:Context){ context.report( NoisyIssue, Location.create(context.file), NoisyIssueDescription) } }
* Detector test
-
Lint 에 대한 test code 를 짜보자
@Test fun testNoisyDetector(){ lint() .files( manifest( """<manifest package="test">...</manifest>""" ).indented() ) .issues(NoisyIssue) .run() .expect( """Noisy issue alarm""".trimIndent() // copied from test result ) }
* Source code 에 대한 Lint
-
PSI : Program Structure Interface
UAST : Universal Abstract Syntax Tree
UAST 가 java 와 kotlin 를 다 support 하므로 java, kotlin 용 lint 를 각각 따로 만들지 않아도 된다.
최종적으로 lint check 를 하는 것은 PSI + UAST 를 상황에 맞게 조합해서 한다.
IntelliJ 의 Psi Viewer 를 통해 구조와 내용을 볼 수 있다.
출처 : Medium Article
-
val BadConfigurationProviderIssue = Issue.create( id = "BadConfigurationProviderId", briefDescription = "This is BadConfigurationProviderIssue", explanation = "This is BadConfigurationProviderIssue. You can detect this one", category = Category.CORRETNESS, severity = Severity.FATAL, // lint 에 걸리면 compile error implementation = Implmentation( BadConfigurationProviderDetector::class.java, Scope.JAVA_FILE_SCOPE // java 이지만 kotlin 도 cover ) )
-
class BadConfigurationProviderDetector : Detector(), SourceCodeScanner{ private var mCorrect = false override fun applicableSuperClasses() = listOf("androidx.work.Configuration.Provider") override fun visitClass(context:JavaContext, declaration:UClass){ // evaluator 는 helper class 로 기능들을 잘 알아두면 좋다. if (context.evaluator.extendsClass(declaration.javaPsi, "android.app.Application", false)){ mCorrect = true } } override fun afterCheckEachProject(context: Context){ if (mCorrect == false){ context.report( issue = IssueRegistry.BadConfigurationProviderIssue, location = Location.create(context.file), message = "You have to extends Application" ) } } }
* Source code lint test
-
// test 를 할 때 android.jar 를 가져오기 싫어 stub 을 만든다. object Stubs{ val WORK_MANAGER_CONFIGURATION_INTERFACE : TestFile = kotlin( "androidx/work/Configuration.kt", """ package androidx.work interface Configuration{ interface Provider{ fun getWorkManagerConfiguration() : Configuration } } """) .indented().within("src") val ANDROID_APPLICATION_CLASS: TestFile = kotlin( "android/app/Application.kt", """ package android.app open Class Application{ fun onCreate(){ } } """) .indented(),.within("src") }
-
@Test fun testBadConfigurationProviderDetector_Sccess(){ lint() .files( WORK_MANAGER_CONFIGURATION_INTERFACE, ANDROID_APPLICATION_CLASS, APP_IMPLMENTS_CONFIGURATION_PROVIDER ) .issues(BadConfigurationProviderIssue) .run() .expect("No warnings.") } @Test fun testBadConfigurationProviderDetector_Failure(){ lint() .files( WORK_MANAGER_CONFIGURATION_INTERFACE, ANDROID_APPLICATION_CLASS, OTHER_CLASS_IMPLMENTS_CONFIGURATION_PROVIDER ) .issues(BadConfigurationProviderIssue) .run() .expect("You have to extends Application.".trimIndent()) }
-
아래 명령을 통해서 lint 를 돌릴 수 있다.
./gradlew :app:lintDebug
이는 보기 결과에 대한 좋은 XML 과 HTML report 를 만들어준다. metadata 도 포함해서 말이다.
* method call 을 detect 해보자.
-
class LogWtfDetector : Detector(), SourceCodeScanner{ override fun getApplicableMethodNames() = listOf("wtf") override fun visitMethodCall(context:JavaContext, node:UCallExpression, method:PsiMethod){ if (context.evaluator.isMemberInClass(method, "android.util.Log"){ reportUsage(context, node, method) } } private fun reportUsage(context:Context, node:UCallExpression, method:PsiMethod){ val quickfixData = LintFix.create() .name("Use Log.e()") .replace() .text(method.name) .with("e") .robot(true) // can be applied automatically .independent(true) // does not conflict with other auto-fix .build() context.report( /*... */, quickfixData = quickfixData) } }
* Annotation 에 대한 lint 를 만들어보자
-
@Experimental @Retension(AnnotationReetension.BINARY) @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) annotation class TimeTravelExperiment @TimeTravelExperiment class TimeTravelProvider{ fun setTime(timestamp:Long){ // ... } } class UserTimeTravelExperimentFromJava{ @TimeTravelExperiment void setTimeToNow(){ new TimeTravelProvider().setTime(System.currentTimeInMillis()); } // UMethod, annotated 라면 UAnnotated @UseExperimental(markerClass = TimeTravelExperiment.class) void setTimeToEpoch(){ // TimeTravelProvider() -> UCallExpression // TimeTravelProvider().setTime(0); -> UCompositeQualifiedExpression new TimeTravelProvider().setTime(0); // UCodeBlockExpression } }
-
class ExperimentalDetector : Detector(), SourceCodeScanner{ override fun applicableAnnotations() = listOf("kotlin.Experimental") override fun visitAnnotationUsage(context:JavaContext, usage:UElement, annotation:UAnnotation, ... ){ if (isKotlin(usage.sourcePsi)) return // kotlin 은 lint 에서 자동 지원 if (isValidExperimentalUsage(context, usage, annotation) == false){ reportUsage(context, usage, annotations) } } private fun isValidExperimentalUsage(context:JavaContext, usage:UElement, annotation:UAnnotation):Boolean{ val featureName = (annotation.uastParent as? UClass) ?.qualifiedName ?: return false // find the nearest enclosing annotated element var element:UAnnoated? = if (usage is UAnnotated){ usage } else{ usage.getParentOfType(UAnnoated::class.java) } while (element != null){ val annotations = context.evaluator.getAllAnnotations(element, false) if (annotations.any { it.qualifiedName == featureName }){ return true } if (annotations .filter { annot -> annot.qualifiedName == "kotlin.UseExperimental" } .mapNotNull { annot -> annot.attributeValue.getOrNull(0) } .any { attr -> attr.getFullyQualifiedName(context) == featureName }){ return true } // next annotated parenting element element = element.getParentOfType(UAnnotated::class.java) } return false } }
* Rule 을 잘 적용하는 Tip
-
build-in lint 에 대해서는 아래를 참고하라.
https://tinyurl.com/aosp-lint-checks
-
확실치 않은 것들에 대해서는 test case 를 통해 debug point 찍고 잘 활용해라. (UAnnotated type 등?)
-
PSI Viewer 를 활용해라
* BaseLine
-
baseline 은 in-editor check 를 무시한다.
baseline 이 없다면, 적용되는 lint 에 대해 모든 소스코드를 한번에 고쳐야 한다.
basline 을 통해 lint error 를 확인하여 고치고, baseline 을 update 하는 과정을 반복하여, 점차적인 lint error 를 수정할 수 있다.
-
android{ lintOptions{ // ./gradlew lintDebug 로 생성된 file baseline file("lint-baseline.xml") // isAbortOnError = true // 이 녀석은 baseline 에 있는 녀석을 enable 시킨다. } }
'프로그래밍 놀이터 > 안드로이드, Java' 카테고리의 다른 글
[android] 성능에 대한 미신들을 때려잡자! ( from Dev Summit 2019 ) (0) | 2019.11.16 |
---|---|
[android] AndroidX 로의 migration : 그 때가 왔다! ( from Dev Summit 19 ) (0) | 2019.11.15 |
[android] Pie (android 9) 의 변경점 (0) | 2019.07.31 |
[android] Pie 에서 앱 잘 작동하는지 확인하기 (0) | 2019.07.30 |
[android] Pie (9) 의 Power management (0) | 2019.07.29 |
댓글