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 |
댓글