Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f223c0f6 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add CheckFlagRuleDetector to default Android linting" into main

parents edc0b13d 550d01aa
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import com.google.android.lint.aidl.EnforcePermissionDetector
import com.google.android.lint.aidl.PermissionAnnotationDetector
import com.google.android.lint.aidl.SimpleManualPermissionEnforcementDetector
import com.google.android.lint.aidl.SimpleRequiresNoPermissionDetector
import com.google.android.lint.flags.CheckFlagsRuleDetector
import com.google.auto.service.AutoService

@AutoService(IssueRegistry::class)
@@ -36,6 +37,7 @@ class AndroidGlobalIssueRegistry : IssueRegistry() {
            PermissionAnnotationDetector.ISSUE_MISSING_PERMISSION_ANNOTATION,
            SimpleManualPermissionEnforcementDetector.ISSUE_SIMPLE_MANUAL_PERMISSION_ENFORCEMENT,
            SimpleRequiresNoPermissionDetector.ISSUE_SIMPLE_REQUIRES_NO_PERMISSION,
            CheckFlagsRuleDetector.ISSUE_MISSING_CHECK_FLAGS_RULE,
    )

    override val api: Int
+119 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.android.lint.flags

import com.android.tools.lint.client.api.UElementHandler
import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Implementation
import com.android.tools.lint.detector.api.Issue
import com.android.tools.lint.detector.api.JavaContext
import com.android.tools.lint.detector.api.Scope
import com.android.tools.lint.detector.api.Severity
import com.android.tools.lint.detector.api.SourceCodeScanner
import java.util.EnumSet
import org.jetbrains.uast.UAnnotated
import org.jetbrains.uast.UClass
import org.jetbrains.uast.UElement

/**
 * Lint detector to ensure that any class that uses test flag annotations (@RequiresFlagsEnabled
 * or @RequiresFlagsDisabled) also includes a `CheckFlagsRule`.
 *
 * This check is necessary to ensure that the test flags are properly handled by the test runner.
 */
class CheckFlagsRuleDetector : Detector(), SourceCodeScanner {

    override fun getApplicableUastTypes(): List<Class<out UElement>> = listOf(UClass::class.java)

    override fun createUastHandler(context: JavaContext): UElementHandler =
        object : UElementHandler() {
            override fun visitClass(node: UClass) {
                if (!hasRequiresFlagsAnnotation(node)) {
                    return
                }

                val hasCheckFlagsRule =
                    node.fields.any { field ->
                        field.type.canonicalText ==
                            "android.platform.test.flag.junit.CheckFlagsRule"
                    }

                if (!hasCheckFlagsRule) {
                    context.report(
                        ISSUE_MISSING_CHECK_FLAGS_RULE,
                        node,
                        context.getNameLocation(node),
                        "Class `${node.name}` uses @RequiresFlagsEnabled or" +
                            " @RequiresFlagsDisabled and must have a CheckFlagsRule.",
                    )
                }
            }
        }

    private fun hasRequiresFlagsAnnotation(node: UClass): Boolean {
        if (node.hasRequiresFlagsAnnotation()) {
            return true
        }

        return node.methods.any { method -> method.hasRequiresFlagsAnnotation() }
    }

    private fun UAnnotated.hasRequiresFlagsAnnotation(): Boolean {
        return findAnnotation("android.platform.test.annotations.RequiresFlagsEnabled") != null ||
            findAnnotation("android.platform.test.annotations.RequiresFlagsDisabled") != null
    }

    companion object {
        private const val EXPLANATION =
            """
            The `@RequiresFlagsEnabled` and `@RequiresFlagsDisabled` annotations
            require a `CheckFlagsRule` to be present in the test class.

            This rule is responsible for checking the state of the specified feature flags and
            skipping the test if the conditions are not met. Without this rule,
            the annotations have no effect, and the test may run in an incorrect environment.

            To fix this, add a `CheckFlagsRule` field annotated with `@Rule` to your test class:
            ```java
            import android.platform.test.flag.junit.CheckFlagsRule;
            import org.junit.Rule;

            public class YourTestClass {
                @Rule
                public final CheckFlagsRule mCheckFlagsRule = new CheckFlagsRule();

                // ... your test methods
            }
            ```
            """
        val ISSUE_MISSING_CHECK_FLAGS_RULE =
            Issue.create(
                id = "MissingCheckFlagsRule",
                briefDescription = "Missing CheckFlagsRule",
                explanation = EXPLANATION,
                category = Category.CORRECTNESS,
                severity = Severity.ERROR,
                implementation =
                    Implementation(
                        CheckFlagsRuleDetector::class.java,
                        EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES),
                        Scope.JAVA_FILE_SCOPE,
                    ),
            )
    }
}
+362 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.android.lint.flags

import com.android.tools.lint.checks.infrastructure.LintDetectorTest
import com.android.tools.lint.checks.infrastructure.TestLintTask
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4

@RunWith(JUnit4::class)
class CheckFlagsRuleDetectorTest : LintDetectorTest() {
    override fun getDetector(): Detector = CheckFlagsRuleDetector()

    override fun getIssues(): List<Issue> =
        listOf(CheckFlagsRuleDetector.ISSUE_MISSING_CHECK_FLAGS_RULE)

    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)

    @Test
    fun testRequiresFlagsEnabled_methodAnnotation_noRule_throws() {
        lint()
            .files(
                java(
                        """
                    package com.example.app;
                    import android.platform.test.annotations.RequiresFlagsEnabled;

                    public class ExampleUnitTest {
                        @RequiresFlagsEnabled("my_flag")
                        public void testMethod() {}
                    }
                    """
                    )
                    .indented(),
                *stubs,
            )
            .issues(CheckFlagsRuleDetector.ISSUE_MISSING_CHECK_FLAGS_RULE)
            .run()
            .expect(
                """
                src/com/example/app/ExampleUnitTest.java:4: Error: Class ExampleUnitTest uses @RequiresFlagsEnabled or @RequiresFlagsDisabled and must have a CheckFlagsRule. [MissingCheckFlagsRule]
                public class ExampleUnitTest {
                             ~~~~~~~~~~~~~~~
                1 errors, 0 warnings
                """
                    .trimIndent()
            )
    }

    @Test
    fun testRequiresFlagsEnabled_classAnnotation_noRule_throws() {
        lint()
            .files(
                java(
                        """
                    package com.example.app;
                    import android.platform.test.annotations.RequiresFlagsEnabled;

                    @RequiresFlagsEnabled("my_flag")
                    public class ExampleUnitTest {

                    }
                    """
                    )
                    .indented(),
                *stubs,
            )
            .issues(CheckFlagsRuleDetector.ISSUE_MISSING_CHECK_FLAGS_RULE)
            .run()
            .expect(
                """
                src/com/example/app/ExampleUnitTest.java:5: Error: Class ExampleUnitTest uses @RequiresFlagsEnabled or @RequiresFlagsDisabled and must have a CheckFlagsRule. [MissingCheckFlagsRule]
                public class ExampleUnitTest {
                             ~~~~~~~~~~~~~~~
                1 errors, 0 warnings
                """
                    .trimIndent()
            )
    }

    @Test
    fun testRequiresFlagsDisabled_methodAnnotation_noRule_throws() {
        lint()
            .files(
                java(
                        """
                    package com.example.app;
                    import android.platform.test.annotations.RequiresFlagsDisabled;

                    public class ExampleUnitTest {
                        @RequiresFlagsDisabled("my_flag")
                        public void testMethod() {}
                    }
                    """
                    )
                    .indented(),
                *stubs,
            )
            .issues(CheckFlagsRuleDetector.ISSUE_MISSING_CHECK_FLAGS_RULE)
            .run()
            .expect(
                """
                src/com/example/app/ExampleUnitTest.java:4: Error: Class ExampleUnitTest uses @RequiresFlagsEnabled or @RequiresFlagsDisabled and must have a CheckFlagsRule. [MissingCheckFlagsRule]
                public class ExampleUnitTest {
                             ~~~~~~~~~~~~~~~
                1 errors, 0 warnings
                """
                    .trimIndent()
            )
    }

    @Test
    fun testRequiresFlagsDisabled_classAnnotation_noRule_throws() {
        lint()
            .files(
                java(
                        """
                    package com.example.app;
                    import android.platform.test.annotations.RequiresFlagsDisabled;

                    @RequiresFlagsDisabled("my_flag")
                    public class ExampleUnitTest {

                    }
                    """
                    )
                    .indented(),
                *stubs,
            )
            .issues(CheckFlagsRuleDetector.ISSUE_MISSING_CHECK_FLAGS_RULE)
            .run()
            .expect(
                """
                src/com/example/app/ExampleUnitTest.java:5: Error: Class ExampleUnitTest uses @RequiresFlagsEnabled or @RequiresFlagsDisabled and must have a CheckFlagsRule. [MissingCheckFlagsRule]
                public class ExampleUnitTest {
                             ~~~~~~~~~~~~~~~
                1 errors, 0 warnings
                """
                    .trimIndent()
            )
    }

    @Test
    fun testRequiresFlagsEnabled_methodAnnotation_withRule_passes() {
        lint()
            .files(
                java(
                        """
                    package com.example.app;
                    import android.platform.test.annotations.RequiresFlagsEnabled;
                    import android.platform.test.flag.junit.CheckFlagsRule;
                    import org.junit.Rule;

                    public class ExampleUnitTest {
                        @Rule
                        public final CheckFlagsRule mCheckFlagsRule = new CheckFlagsRule();

                        @RequiresFlagsEnabled("my_flag")
                        public void testMethod() {}
                    }
                    """
                    )
                    .indented(),
                *stubs,
            )
            .run()
            .expectClean()
    }

    @Test
    fun testRequiresFlagsEnabled_classAnnotation_withRule_passes() {
        lint()
            .files(
                java(
                        """
                    package com.example.app;
                    import android.platform.test.annotations.RequiresFlagsEnabled;
                    import android.platform.test.flag.junit.CheckFlagsRule;
                    import org.junit.Rule;

                    @RequiresFlagsEnabled("my_flag")
                    public class ExampleUnitTest {
                        @Rule
                        public final CheckFlagsRule mCheckFlagsRule = new CheckFlagsRule();

                    }
                    """
                    )
                    .indented(),
                *stubs,
            )
            .run()
            .expectClean()
    }

    @Test
    fun testRequiresFlagsDisabled_methodAnnotation_withRule_passes() {
        lint()
            .files(
                java(
                        """
                    package com.example.app;
                    import android.platform.test.annotations.RequiresFlagsDisabled;
                    import android.platform.test.flag.junit.CheckFlagsRule;
                    import org.junit.Rule;

                    public class ExampleUnitTest {
                        @Rule
                        public final CheckFlagsRule mCheckFlagsRule = new CheckFlagsRule();

                        @RequiresFlagsDisabled("my_flag")
                        public void testMethod() {}
                    }
                    """
                    )
                    .indented(),
                *stubs,
            )
            .run()
            .expectClean()
    }

    @Test
    fun testRequiresFlagsDisabled_classAnnotation_withRule_passes() {
        lint()
            .files(
                java(
                        """
                    package com.example.app;
                    import android.platform.test.annotations.RequiresFlagsDisabled;
                    import android.platform.test.flag.junit.CheckFlagsRule;
                    import org.junit.Rule;

                    @RequiresFlagsDisabled("my_flag")
                    public class ExampleUnitTest {
                        @Rule
                        public final CheckFlagsRule mCheckFlagsRule = new CheckFlagsRule();
                    }
                    """
                    )
                    .indented(),
                *stubs,
            )
            .run()
            .expectClean()
    }

    @Test
    fun testNoAnnotations_passes() {
        lint()
            .files(
                java(
                        """
                    package com.example.app;

                    public class ExampleUnitTest {}
                    """
                    )
                    .indented(),
                *stubs,
            )
            .run()
            .expectClean()
    }

    @Test
    fun testUnrelatedAnnotation_noRule_passes() {
        lint()
            .files(
                java(
                        """
                    package com.example.app;
                    import org.junit.Test;

                    public class ExampleUnitTest {
                        @Test
                        public void testMethod() {}
                    }
                    """
                    )
                    .indented(),
                *stubs,
            )
            .run()
            .expectClean()
    }

    private val requiresFlagsEnabledStub =
        java(
                """
            package android.platform.test.annotations;
            public @interface RequiresFlagsEnabled {
                String[] value();
            }
            """
            )
            .indented()

    private val requiresFlagsDisabledStub =
        java(
                """
            package android.platform.test.annotations;
            public @interface RequiresFlagsDisabled {
                String[] value();
            }
            """
            )
            .indented()

    private val checkFlagsRuleStub =
        java(
                """
            package android.platform.test.flag.junit;
            public class CheckFlagsRule {}
            """
            )
            .indented()

    private val ruleStub =
        java(
                """
            package org.junit;
            public @interface Rule {}
            """
            )
            .indented()

    private val testAnnotationStub =
        java(
                """
            package org.junit;
            public @interface Test {}
            """
            )
            .indented()

    private val stubs =
        arrayOf(
            requiresFlagsEnabledStub,
            requiresFlagsDisabledStub,
            checkFlagsRuleStub,
            ruleStub,
            testAnnotationStub,
        )
}