본문 바로가기

Android/Lint

[Lint] Lint에 Custom rule을 추가해보자. - 2. XML Rule

728x90

본 게시글은 이전 게시글에 이어서 작성된 부분입니다.

2022.04.13 - [Android/Lint] - [Lint] Lint에 Custom rule을 추가해보자. -1. 기본 설정 및 적용

 

지난번 게시글에서는 기본적인 설정과 Method에 대한 Lint Rule을 추가했었는데,

이번에는 XML 파일에 대한 Lint Rule을 추가해보고자 한다.

 

Item 전체에 대한 lint, 속성 값에 대한 lint, 이 두 가지를 합친 경우. 3가지에 대하여 작성할 예정이다.


우선,

XML도 마찬가지로 Issue와 Detector가 필요하다.

 

하지만, Issue의 경우 사용에 따라, 사용자에 따라 마음대로 커스텀하는 부분이기 때문에 Method에 대한 Issue와 한 가지를 제외하고는 동일하게 사용하면 된다.

변경이 필요한 한 가지는, Implementation에 대한 항목이다.

 

Implementation는 Detector가 수행될 범위에 대해 설정을 해주는 부분인데, 기존에 작성했던 implementation은 다음과 같다.

 

private val IMPLEMENTATION = Implementation(
    LintContentViewDetector::class.java,
    Scope.JAVA_FILE_SCOPE
)

 

Scope가 Java File로 한정되어 있기 때문에 지금 하고자 하는 xml 탐지를 할 수 없다.

 

따라서, 이 부분을 

 

private val IMPLEMENTATION = Implementation(
    YourXMLDetectorClass::class.java,
    Scope.RESOURCE_FILE_SCOPE
)

 

Resourece file scope로 변경해주어 사용해야 한다.

 

해당 스코프로 설정한 이유는 Scope Class에서 확인할 수 있다.

 

 

RESOURCE_FILE_SCOPE는 다음과 같이 나와있는데, 여기에 사용되는 RESOURCE_FILE에 대해 확인해 보면

 

 

이처럼 xml 파일을 검색한다고 나와있다.

 

이것 외에도 적용할 수 있는 Scope는 많으니 필요에 따라 확인하고 적용하면 될 것으로 보인다.


다음으로,

Detector 부분을 확인해보자.

 

지난번에는 Detector를 그대로 상속받아서 사용했던 반면, 이번에는 ResourceXmlDetector를 상속받아서 사용한다.

 

class LintXMLHardCodingDetector : ResourceXmlDetector()

 

ResourceXmlDetector를 확인해보면, Detector를 상속받은 클래스로 보여서 Detector만 상속받고 적용을 해보았을 때 XML에서 Lint가 정상적으로 적용되지 않으므로 ResourceXmlDetector를 사용하도록 한다.

 

처음으로 XML의 Item을 탐색하는 Detector를 작성해 보자.

 

companion object {
    private const val SCHEMA = "http://schemas.android.com/apk/res/android"
    private const val TEXTVIEW = "TextView"
    private const val CHECK_ATTRIBUTE = "textAppearance"   
}

override fun getApplicableElements(): Collection<String>? {
    return listOf(TEXTVIEW)
}

override fun visitElement(context: XmlContext, element: Element) {
    if (!element.hasAttributeNS(SCHEMA, CHECK_ATTRIBUTE)) {
        context.report(
            issue = ISSUE,
            location = context.getLocation(element),
            message = EXPLANATION
        )
    }
}

 

사용할 변수들은 Issue를 선언할 때 사용한 companion object 안에서 선언해 사용하도록 하였다.

 

override 된 함수를 살펴보면,

 

getApplicableElements()는 앞서 작업했던 getApplicableMethodNames() 동일한 역할을 한다고 생각하면 된다.

반환되는 List에 TextView를 넣어 텍스트 뷰만 감지할 수 있도록 한다.

 

Return type인 Collection<String>에 맞춰서 사용하긴 했지만,

 

return listOf(TEXTVIEW)

 

와 같이 listOf를 사용해도 상관 없다.

 

다음으로, visitElement() 또한 동일하게 getApplicableElements에서 반환된 이름의 element를 감지하면 호출되는 부분이다.

 

해당 함수에서 사용되는 param 값에 대해서 알아보면,

 

  • XmlContext : 감지된 XML 코드에 대한 정보
  • Element : 감지된 Element에 대한 정보

와 같은 값을 사용하게 된다.

 

element.hasAttributeNS(SCHEMA, CHECK_ATTRIBUTE)

 

조건문을 확인해보면 hasAttributeNS를 사용하고 있는데,

이것은 현재 요소지정된 속성이 있는지 확인하는 함수다.

SCHEMA를 사용하는 현재 요소에서 CHECK_ATTRIBUTE 속성이 있는지 확인한다는 것이 된다.

 

여기서, SCHEMA를 보면 http://schemas.android.com/apk/res/android 라는 값을 사용하고 있다.

해당 값을 사용하는 이유는 XML 코드를 보면 한 번에 이해가 가능하다.

 

<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    ...
    >
    <TextView
        android:id="@+id/sample_text2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/teal_700"
        android:text="sample"
        android:textAppearance="?android:attr/textAppearanceLarge"
        app:layout_constraintTop_toBottomOf="@id/sample_text1"/>
    
    ....
    
</androidx.constraintlayout.widget.ConstraintLayout>

 

필자가 샘플로 사용한 xml의 일부분이다.

우리가 사용하는 android:~ 에서 android를 사용하기 위해서

 

xmlns:android="http://schemas.android.com/apk/res/android"

 

해당 라인이 필요하게 된다.

 

다시 해당 함수로 넘어가서,

위에 선언한 함수를 해석하자면 SCHEMA:CHECK_ATTRIBUTE로 선언된 값이 있는가?라는 것이다.

 

 

SCHEMA는 android로 사용이 되고 있으며,

CHECK_ATTRIBUTE는 현재 textApperance이기 때문에,

android:textApperance라는 Attribute가 TextView에 있는지 여부를 판단하게 된다.

 

if (!element.hasAttributeNS(SCHEMA, CHECK_ATTRIBUTE))

 

따라서, 해당 조건문은 android:textApperance 속성이 없는 경우 true 값이 되게 되어 context.report를 호출하게 되는데,

visitElement가 호출되기 위해서는 TextView가 감지됐을 경우이기 때문에,

TextView에 android:textApperance 속성이 없는 경우 해당 Item에 Lint를 발생한다.라는 조건이 완성되게 된다.

 


이번에는 Item 전체가 아닌 attribute에만 lint를 적용시켜보도록 하자.

 

이번 Lint는 text를 설정할 때 하드 코딩되는 경우에 Error를 표시하도록 조건을 추가하였다.

 

companion object {
    private const val CHECK_ATTRIBUTE = "text"
    private const val PREFIX = "@string/"
}

override fun getApplicableAttributes(): Collection<String> {
    return listOf(CHECK_ATTRIBUTE)
}

override fun visitAttribute(context: XmlContext, attribute: Attr) {
    val content = attribute.value

    if (!content.startsWith(PREFIX)) {
        context.report(
            issue = ISSUE,
            location = context.getLocation(attribute),
            message = EXPLANATION
        )
    }
}

 

이번에는 Element 대신 Attributes를 사용한 함수를 override 하였다.

 

위의 Item 전체에 Lint를 적용시키는 예제와 다른 점은,

  1. Element를 Attributes로 변경한 것
  2. visit Method의 param 값이 Element에서 Attr로 변경된 것

2가지밖에 없으며,

Attr에는 감지된 attribute에 대한 정보가 들어가 있다.

 

즉, XML 전체 코드에서 CHECK_ATTRIBUTE가 감지되는 경우 visitAttribute가 호출되게 된다.

위의 예제에서 CHECK_ATTRIBUTE는 text이기 때문에 xml에서 text를 설정하는 부분이 있다면 모두 감지가 된다.

 

여기서, visit 내부의 조건에 따라 Lint가 표시가 되는데 attribute는 감지된 Attribute에 대한 정보가 들어가 있으니 attribute.value를 사용하여 text에 설정한 값을 확인할 수 있다.

 

if (!content.startsWith(PREFIX))

 

위의 조건문을 확인해 보면, text로 설정한 값에 PREFIX가 앞에 없으면 해당 조건문이 true가 된다.

즉, PREFIX는 @string/ 이므로 text에 Hardcoding 된 값을 사용하고 있으면 Lint가 발생하게 되는 것이다.

 

 

이처럼 Item 종류에 상관없이 text 속성을 사용할 때, 하드코딩이 되는 경우 Error가 발생하게 된다.

중간에 Warning은 위의 textAppearance 예제를 제외하지 않고 테스트했기 때문에 발생한 것이니 무시해도 된다.


마지막으로, 지금까지 작업했던 2가지의 내용을 합친 Lint를 적용해보자.

지정된 Item에서, 지정된 속성 값에 따라 해당 속성 값에만 Lint를 호출하기 위하여 TextView에 선언된 text 값에서 하드 코딩된 부분을 Error처리해보도록 하자.

 

그렇기 위해서 필자는 위의 2가지 예제를 사용하여 많은 테스트를 해보았다.

visit 함수에서 사용하는 파라미터들이 정확히 어떠한 정보까지 제공해주는지 명확하게 알지 못하였기 때문에, 제공하는 값들을 모두 확인해가면서 적용했다.

 

첫 번째 예제를 사용하기 위해서는 Item 전체를 탐지하고 Lint를 적용시켜야 했고,

Item과 Attribute를 찾아도 해당 속성 값에만 Lint를 적용시키는 것에 문제가 있었다.

 

따라서, Attribute를 찾는 두 번째 예제에서 해당 Attribute가 선언된 item을 찾아서 분기 처리해주는 방식으로 구현할 수 있었다.

 

override fun visitAttribute(context: XmlContext, attribute: Attr) {
    val content = attribute.value
    val parentItem = attribute.ownerElement.nodeName

    if (!content.startsWith(PREFIX) && parentItem == CHECK_ITEM) {
        context.report(
            issue = ISSUE,
            location = context.getLocation(attribute),
            message = EXPLANATION
        )
    }
}

 

감지된 Attribute에 대한 정보가 들어있는 Attr을 사용하였다.

ownerElement를 사용하게 되면, 해당 속성이 선언되어 있는 Item을 찾을 수 있으며, nodeName을 통해 해당 Item의 이름을 가져올 수 있다.

해당 예제로 치자면, ownerElement는 TextView라는 객체, ownerElement.nodeName은 TextView라는 String 값이 된다.

 

즉, 

 

parentItem == CHECK_ITEM

 

해당 조건은 CHECK_ITEM이 TextView이기 때문에, 감지된 속성 값이 선언된 item이 TextView일 경우에 조건이 true가 된다.

 

해당 조건을 사용함으로써 원하고자 하는 Lint Rule을 추가할 수 있다.

 

여기서, Item이 이름이 아니라 다른 속성 값들에 대한 조건도 추가하고 싶은 경우

 

attribute.ownerElement.textContent

 

를 사용하여 속성 값을 찾아내서 조건으로 추가하면 될 것으로 생각한다.

해당 값을 추가하면 

 

 

다음과 같이 해당 속성이 들어간 Item 전체를 보여주기 때문에 쉽게 값을 찾아서 넣을 수 있을 것으로 보인다.


다른 추가적인 Lint 조건보다 비교적 적용하기 쉬웠던 XML에 대한 Lint를 적용해 보았다.

 

text의 하드코딩 같은 경우 이미 안드로이드 스튜디오에 적용된 lint를 통해 warning이 발생하고 있긴 하지만,

필자가 이전 하드 코딩된 값을 제대로 제거하지 않고 테스트를 하다가 시간을 날린 경험이 있으므로 error로 표시하도록 적용해 보았다.

 

기획상 Text가 표현되는 곳에서는 모두 style이 적용되어 있어야 한다거나, 이와 같이 반드시 체크가 되어야 하는 부분이 있다면 Lint를 추가해 error나 warning을 보여준다면 놓치지 않고 작업을 할 수 있을 것으로 보인다.

 

다음에는 Lint에 대한 것들을 조금 더 공부해보고, 적용할만한 조건이 있으면 글을 작성해 볼 예정이다.

 

해당 게시글에 사용한 예제는 다음 Github에 올려두었다.

https://github.com/HeeGyeong/CleanArchitectureSample

 

GitHub - HeeGyeong/CleanArchitectureSample

Contribute to HeeGyeong/CleanArchitectureSample development by creating an account on GitHub.

github.com

728x90