Custom Lint - Checking for gradient tag in vector fails

66 views Asked by At

I'm writing a custom lint rule to catch if android:src namespace is being used for displaying vector drawables which have gradient tag in them instead of app:scrCompat namespace.

Android Studio is able to detect the errors after writing this rule.

Example

When the below vector drawable is used with android namespace on API level < 22 will lead to crash!

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:aapt="http://schemas.android.com/aapt"
    android:width="9dp"
    android:height="17dp"
    android:viewportWidth="9"
    android:viewportHeight="17">
    <path
        android:pathData="M7.1921,17L8.0072,17L8.0072,0L0,0L0,9.8079C-0,11.8049 0.2818,12.9046 0.8109,13.894C1.3401,14.8834 2.1166,15.6599 3.106,16.1891C4.0954,16.7182 5.1951,17 7.1921,17Z"
        android:strokeWidth="1"
        android:fillType="evenOdd"
        android:strokeColor="#00000000">
        <aapt:attr name="android:fillColor">
            <gradient
                android:startY="17"
                android:startX="3.3198788"
                android:endY="17"
                android:endX="5.201214"
                android:type="linear">
                <item android:offset="0" android:color="#000000"/>
                <item android:offset="1" android:color="#FFFFFF"/>
            </gradient>
        </aapt:attr>
    </path>
</vector>

------------
Incorrect use of the above drawable
<ImageView
     android:id="@+id/imageView"
     ❌ android:src="@drawable/hero_background"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"/>

Correct usage
<ImageView
     android:id="@+id/imageView"
     ✅ app:srcCompat="@drawable/hero_background"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"/>
------------

This is the rule

class VectorUsageDetector: ResourceXmlDetector() {

    companion object {

        val VECTOR_ISSUE = Issue.create(
            id = "VectorGradientIssue",
            briefDescription = "Warns usage of vectors with `android` namespace",
            explanation = "Vectors with `android` are not supported in API level below 21",
            category = Category.CORRECTNESS,
            severity = Severity.FATAL,
            implementation = Implementation(
                VectorUsageDetector::class.java,
                Scope.RESOURCE_FILE_SCOPE
            )
        )
    }

    override fun appliesTo(folderType: ResourceFolderType): Boolean {
        return folderType == ResourceFolderType.LAYOUT
    }

    override fun getApplicableAttributes(): Collection<String>? {
        return listOf(SdkConstants.ATTR_SRC)
    }

    override fun visitAttribute(context: XmlContext, attribute: Attr) {
        val name = attribute.name
        val value = attribute.value
        if (name.contains("android:") and value.contains("drawable")) {
            val isVectorDrawable = isVectorDrawable(value.replace("@drawable/", ""), context)
            if (isVectorDrawable) {
                context.report(
                    VECTOR_ISSUE,
                    attribute,
                    context.getValueLocation(attribute),
                    "Vector is used without `app` namespace. This can cause a crash on API levels below 21.")
            } else {
                return
            }
        } else {
            return
        }
    }

    private fun isVectorDrawable(name: String, context: XmlContext): Boolean {
        var hasVectorTag = false
        var hasGradientTag = false
        context.mainProject.resourceFolders.forEach { folder ->
            val path = folder.path
            val drawableFolder = "$path/drawable/"
            val drawableFile = File("$drawableFolder/$name.xml")
            if (drawableFile.exists()) {
                drawableFile.forEachLine { lineString ->
                    if (lineString.contains(TAG_VECTOR)) {
                        hasVectorTag = true
                    }
                    if (lineString.contains(TAG_GRADIENT)) {
                        hasGradientTag = true
                    }
                }
            }
        }
        return hasVectorTag && hasGradientTag
    }
}

When I run ./gradlew app:lint if there are any violations, they are shown as errors.

But when all errors are fixed and run the same command (./gradlew app:lint) this time I see the following error pointing to some random xml file.

❌

The lint detector
com.xyz.rules.VectorUsageDetector
called context.getMainProject() during module analysis.

This does not work correctly when running in AGP (8.1.0).

In particular, there may be false positives or false negatives because
the lint check may be using the minSdkVersion or manifest information
from the library instead of any consuming app module.

"VectorGradientIssue"

Issue Vendors:
Identifier: com.xyz.rules

Call stack: Context.getMainProject(Context.kt:102)
←VectorUsageDetector.isVectorDrawable(VectorUsageDetector.kt:64)
←VectorUsageDetector.visitAttribute(VectorUsageDetector.kt:46)
←ResourceVisitor.visitElement(ResourceVisitor.java:161)
←ResourceVisitor.visitElement(ResourceVisitor.java:172)
←ResourceVisitor.visitElement(ResourceVisitor.java:172)
←ResourceVisitor.visitElement(ResourceVisitor.java:172)
←ResourceVisitor.visitElement(ResourceVisitor.java:172)
←ResourceVisitor.visitFile(ResourceVisitor.java:120)
←LintDriver$checkResourceFolder$1.run(LintDriver.kt:2400)
←LintClient.runReadAction(LintClient.kt:1700)
←LintDriver$LintClientWrapper.runReadAction(LintDriver.kt:2871)
←LintDriver.checkResourceFolder(LintDriver.kt:2396)
←LintDriver.checkResFolder(LintDriver.kt:2349)
←LintDriver.runFileDetectors(LintDriver.kt:1362)
←LintDriver.checkProject(LintDriver.kt:1148)
←LintDriver.checkProjectRoot(LintDriver.kt:619)
←LintDriver.access$checkProjectRoot(LintDriver.kt:170)
←LintDriver$analyzeOnly$1.invoke(LintDriver.kt:444)
←LintDriver$analyzeOnly$1.invoke(LintDriver.kt:441)

Config

Min SDK - 22

Target SDK - 33

AGP - 8.1.0

Gradle Wrapper - gradle-8.0

Kotlin Version - 1.9

Build tools - 34.0.0

Gradle for the rules module

apply plugin: 'java-library'
apply plugin: 'kotlin'

jar {
    manifest{
        attributes 'Lint-Registry-V2': 'com.xyz.rules.IssueRegistry'
    }
}

dependencies {
    implementation "com.android.tools.lint:lint-api:31.1.0"
    implementation "com.android.tools.lint:lint-checks:31.1.0"
}

What maybe the problem here? Any alternatives to write the same lint check?

Expecting it to not error out if all errors are fixed!

1

There are 1 answers

0
jokuskay On

Seems it's not allowed to use Context#getMainProject() in partial runs: https://googlesamples.github.io/android-custom-lint-rules/api-guide.html

8.4.1 Catching Mistakes: Blocking Access to Main Project

...

When lint is running in partial analysis, a number of calls, such as looking up the main project, or consulting the merged manifest, is not allowed during the analysis phase.

Can't you always use app:srcCompat? Reading files to check existense of vector and gradient tags seems like an expensive operation for me. Also, you're checking only one resource directory while an android project may have multiple.