1 /*
2  * Copyright 2025 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.build.lint
18 
19 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
20 import com.android.tools.lint.checks.infrastructure.ProjectDescription
21 import com.android.tools.lint.checks.infrastructure.TestLintTask
22 import com.android.tools.lint.detector.api.Detector
23 import com.android.tools.lint.detector.api.Issue
24 
25 class FlaggedApiDetectorTest : LintDetectorTest() {
getIssuesnull26     override fun getIssues(): List<Issue> = listOf(FlaggedApiDetector.ISSUE)
27 
28     override fun getDetector(): Detector {
29         return FlaggedApiDetector()
30     }
31 
lintnull32     override fun lint(): TestLintTask {
33         return super.lint().allowMissingSdk()
34     }
35 
testBasicnull36     fun testBasic() {
37         // Test case from b/303434307#comment2
38         lint()
39             .files(
40                 java(
41                     """
42           package test.pkg;
43 
44           import androidx.annotation.ChecksAconfigFlag;
45 
46           public final class Flags {
47               @ChecksAconfigFlag("test.pkg.myFlag")
48               public static boolean myFlag() { return true; }
49           }
50           """
51                 ),
52                 java(
53                         """
54             package test.pkg;
55 
56             import android.annotation.FlaggedApi;
57 
58             public class JavaTest {
59                 @FlaggedApi("test.pkg.myFlag")
60                 class Foo {
61                     public void someMethod() { }
62                 }
63 
64                 public void testValid1() {
65                     if (Flags.myFlag()) {
66                         Foo f = new Foo(); // OK 1
67                         f.someMethod();    // OK 2
68                     }
69                 }
70             }
71             """
72                     )
73                     .indented(),
74                 Stubs.FlaggedApi,
75                 Stubs.ChecksAconfigFlag,
76             )
77             .run()
78             .expectClean()
79     }
80 
testApiGatingnull81     fun testApiGating() {
82         // Test case from b/303434307#comment2
83         lint()
84             .files(
85                 java(
86                     """
87           package test.pkg;
88 
89           import androidx.annotation.ChecksAconfigFlag;
90 
91           public final class Flags {
92               @ChecksAconfigFlag("test.pkg.myFlag")
93               public static boolean myFlag() { return true; }
94           }
95           """
96                 ),
97                 java(
98                         """
99             package test.pkg;
100 
101             import android.annotation.FlaggedApi;
102 
103             public class JavaTest {
104                 interface MyInterface {
105                     void bar();
106                 }
107 
108                 static class OldImpl implements MyInterface {
109                     @Override
110                     public void bar() {
111                     }
112                 }
113 
114                 @FlaggedApi("test.pkg.myFlag")
115                 static class NewImpl implements MyInterface {
116                     @Override
117                     public void bar() {
118                     }
119                  }
120 
121                  void test(MyInterface f) {
122                      MyInterface obj = null;
123                      if (Flags.myFlag()) {
124                          obj = new NewImpl();
125                      } else {
126                          obj = new OldImpl();
127                      }
128                      f.bar();
129                  }
130             }
131             """
132                     )
133                     .indented(),
134                 Stubs.FlaggedApi,
135                 Stubs.ChecksAconfigFlag,
136             )
137             .run()
138             .expectClean()
139     }
140 
testChecksAconfigFlagGating_javaIfCheck_isCleannull141     fun testChecksAconfigFlagGating_javaIfCheck_isClean() {
142         lint()
143             .files(
144                 kotlin(
145                     """
146           package test.pkg
147 
148           import androidx.annotation.ChecksAconfigFlag
149 
150           object FlagsCompat {
151               @ChecksAconfigFlag("test.pkg.myFlag")
152               fun myFlag() { return true; }
153           }
154           """
155                 ),
156                 java(
157                         """
158             package test.pkg;
159 
160             import android.annotation.FlaggedApi;
161 
162             public class JavaTest {
163                 static class FlaggedApiContainer {
164                     @FlaggedApi("test.pkg.myFlag")
165                     public static void flaggedApi() {
166                     }
167                 }
168 
169                 void callFlaggedApi(MyInterface f) {
170                    if (FlagsCompat.myFlag()) {
171                        FlaggedApiContainer.flaggedApi();
172                    }
173                 }
174             }
175             """
176                     )
177                     .indented(),
178                 Stubs.FlaggedApi,
179                 Stubs.ChecksAconfigFlag,
180             )
181             .run()
182             .expectClean()
183     }
184 
testChecksAconfigFlagGating_kotlinIfCheck_isCleannull185     fun testChecksAconfigFlagGating_kotlinIfCheck_isClean() {
186         lint()
187             .files(
188                 kotlin(
189                     """
190           package test.pkg
191 
192           import androidx.annotation.ChecksAconfigFlag
193 
194           object FlagsCompat {
195               @ChecksAconfigFlag("test.pkg.myFlag")
196               fun myFlag() { return true; }
197           }
198           """
199                 ),
200                 kotlin(
201                         """
202                     package test.pkg
203 
204                     import android.annotation.FlaggedApi
205 
206                     class KotlinTest {
207                         object FlaggedApiContainer {
208                             @FlaggedApi("test.pkg.myFlag")
209                             fun flaggedApi() {
210                             }
211                         }
212 
213                         fun callFlaggedApi() =
214                             if (FlagsCompat.myFlag()) {
215                                 FlaggedApiContainer.flaggedApi()
216                             }
217                     }
218                     """
219                     )
220                     .indented(),
221                 Stubs.FlaggedApi,
222                 Stubs.ChecksAconfigFlag,
223             )
224             .run()
225             .expectClean()
226     }
227 
testChecksAconfigFlagGating_kotlinIncorrectWhenCheck_raisesErrornull228     fun testChecksAconfigFlagGating_kotlinIncorrectWhenCheck_raisesError() {
229         lint()
230             .files(
231                 kotlin(
232                     """
233           package test.pkg
234 
235           import androidx.annotation.ChecksAconfigFlag
236 
237           object FlagsCompat {
238               @ChecksAconfigFlag("test.pkg.myFlag")
239               fun myFlag() { return true; }
240           }
241           """
242                 ),
243                 kotlin(
244                         """
245                     package test.pkg
246 
247                     import android.annotation.FlaggedApi
248 
249                     class KotlinTest {
250                         object FlaggedApiContainer {
251                             @FlaggedApi("test.pkg.myFlag")
252                             fun flaggedApi() {
253                             }
254                         }
255 
256                         fun callFlaggedApi() =
257                             when {
258                                 FlagsCompat.myFlag() ->
259                                     println("")
260                                 else ->
261                                     FlaggedApiContainer.flaggedApi()
262                             }
263                     }
264                     """
265                     )
266                     .indented(),
267                 Stubs.FlaggedApi,
268                 Stubs.ChecksAconfigFlag,
269             )
270             .run()
271             .expect(
272                 """
273         src/test/pkg/KotlinTest.kt:17: Error: Method flaggedApi() is a flagged API and must be inside a flag check for "test.pkg.myFlag" [AndroidXFlaggedApi]
274                         FlaggedApiContainer.flaggedApi()
275                         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
276         1 error
277         """
278             )
279     }
280 
testChecksAconfigFlagGating_javaCheckForWrongFlag_raisesErrornull281     fun testChecksAconfigFlagGating_javaCheckForWrongFlag_raisesError() {
282         lint()
283             .files(
284                 kotlin(
285                     """
286           package test.pkg
287 
288           import androidx.annotation.ChecksAconfigFlag
289 
290           object FlagsCompat {
291               @ChecksAconfigFlag("test.pkg.myOtherFlag")
292               fun myFlag() { return true; }
293           }
294           """
295                 ),
296                 java(
297                         """
298             package test.pkg;
299 
300             import android.annotation.FlaggedApi;
301 
302             public class JavaTest {
303                 static class FlaggedApiContainer {
304                     @FlaggedApi("test.pkg.myFlag")
305                     public static void flaggedApi() {
306                     }
307                 }
308 
309                 void callFlaggedApi(MyInterface f) {
310                     if (FlagsCompat.INSTANCE.flaggedApi()) {
311                        FlaggedApiContainer.flaggedApi();
312                    }
313                 }
314             }
315             """
316                     )
317                     .indented(),
318                 Stubs.FlaggedApi,
319                 Stubs.ChecksAconfigFlag,
320             )
321             .run()
322             .expect(
323                 """
324         src/test/pkg/JavaTest.java:14: Error: Method flaggedApi() is a flagged API and must be inside a flag check for "test.pkg.myFlag" [AndroidXFlaggedApi]
325                    FlaggedApiContainer.flaggedApi();
326                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
327         1 error
328         """
329             )
330     }
331 
testChecksAconfigFlagGating_kotlinCheckForWrongFlag_raisesErrornull332     fun testChecksAconfigFlagGating_kotlinCheckForWrongFlag_raisesError() {
333         lint()
334             .files(
335                 kotlin(
336                     """
337           package test.pkg
338 
339           import androidx.annotation.ChecksAconfigFlag
340 
341           object FlagsCompat {
342               @ChecksAconfigFlag("test.pkg.myOtherFlag")
343               fun myFlag() { return true; }
344           }
345           """
346                 ),
347                 kotlin(
348                         """
349                     package test.pkg
350 
351                     import android.annotation.FlaggedApi
352 
353                     class KotlinTest {
354                         object FlaggedApiContainer {
355                             @FlaggedApi("test.pkg.myFlag")
356                             fun flaggedApi() {
357                             }
358                         }
359 
360                         fun callFlaggedApi() =
361                             if (FlagsCompat.myFlag()) {
362                                 FlaggedApiContainer.flaggedApi()
363                             }
364                     }
365                     """
366                     )
367                     .indented(),
368                 Stubs.FlaggedApi,
369                 Stubs.ChecksAconfigFlag,
370             )
371             .run()
372             .expect(
373                 """
374         src/test/pkg/KotlinTest.kt:14: Error: Method flaggedApi() is a flagged API and must be inside a flag check for "test.pkg.myFlag" [AndroidXFlaggedApi]
375                     FlaggedApiContainer.flaggedApi()
376                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
377         1 error
378         """
379             )
380     }
381 
testChecksAconfigFlagGating_javaWithFlaggedDeprecation_isCleannull382     fun testChecksAconfigFlagGating_javaWithFlaggedDeprecation_isClean() {
383         lint()
384             .files(
385                 java(
386                         """
387             package test.pkg;
388 
389             import android.annotation.FlaggedApi;
390 
391             public class JavaTest {
392                 static class FlaggedApiContainer {
393                     @FlaggedApi("test.pkg.myFlag")
394                     @Deprecated
395                     public static void flaggedApi() {
396                     }
397                  }
398 
399                  void callFlaggedApi(MyInterface f) {
400                      FlaggedApiContainer.flaggedApi();
401                  }
402             }
403             """
404                     )
405                     .indented(),
406                 Stubs.FlaggedApi,
407             )
408             .run()
409             .expectClean()
410     }
411 
testChecksAconfigFlagGating_kotlinWithFlaggedDeprecation_isCleannull412     fun testChecksAconfigFlagGating_kotlinWithFlaggedDeprecation_isClean() {
413         lint()
414             .files(
415                 kotlin(
416                         """
417                     package test.pkg
418 
419                     import android.annotation.FlaggedApi
420 
421                     class KotlinTest {
422                         object FlaggedApiContainer {
423                             @FlaggedApi("test.pkg.myFlag")
424                             @Deprecated
425                             fun flaggedApi() {
426                             }
427                         }
428 
429                         fun callFlaggedApi() =
430                             FlaggedApiContainer.flaggedApi()
431                     }
432                     """
433                     )
434                     .indented(),
435                 Stubs.FlaggedApi,
436             )
437             .run()
438             .expectClean()
439     }
440 
testFinalFieldsnull441     fun testFinalFields() {
442         // Test case from b/303434307#comment2
443         lint()
444             .files(
445                 java(
446                     """
447           package test.pkg;
448 
449           import androidx.annotation.ChecksAconfigFlag;
450 
451           public final class Flags {
452               @ChecksAconfigFlag("test.pkg.myFlag")
453               public static boolean myFlag() { return true; }
454           }
455           """
456                 ),
457                 java(
458                         """
459             package test.pkg;
460 
461             import android.annotation.FlaggedApi;
462 
463             public class JavaTest {
464                 static class Bar {
465                     @FlaggedApi("test.pkg.myFlag")
466                     public void bar() { }
467                 }
468                 static class Foo {
469                     private static final boolean useNewStuff = Flags.myFlag();
470                     private final Bar mBar = new Bar();
471 
472                     void someMethod() {
473                         if (useNewStuff) {
474                             // OK because flags can't change value without a reboot, though this might change in
475                             // the future and in that case caching the flag value would be an error. We can restart
476                             // apps due to a server push of new flag values but restarting the framework would be
477                             // too disruptive
478                             mBar.bar(); // OK
479                         }
480                     }
481                 }
482             }
483             """
484                     )
485                     .indented(),
486                 Stubs.FlaggedApi,
487                 Stubs.ChecksAconfigFlag,
488             )
489             .run()
490             .expectClean()
491     }
492 
testInverseLogicnull493     fun testInverseLogic() {
494         lint()
495             .files(
496                 java(
497                     """
498           package test.pkg;
499 
500           import androidx.annotation.ChecksAconfigFlag;
501 
502           public final class Flags {
503               @ChecksAconfigFlag("test.pkg.myFlag")
504               public static boolean myFlag() { return true; }
505           }
506           """
507                 ),
508                 java(
509                         """
510             package test.pkg;
511 
512             import android.annotation.FlaggedApi;
513 
514             public class JavaTest {
515                 @FlaggedApi("test.pkg.myFlag")
516                 class Foo {
517                     public void someMethod() { }
518                 }
519 
520                 public void testInverse() {
521                     if (!Flags.myFlag()) {
522                         // ...
523                     } else {
524                         Foo f = new Foo(); // OK 1
525                         f.someMethod();    // OK 2
526                     }
527                 }
528             }
529             """
530                     )
531                     .indented(),
532                 Stubs.FlaggedApi,
533                 Stubs.ChecksAconfigFlag,
534             )
535             .run()
536             .expectClean()
537     }
538 
testAndednull539     fun testAnded() {
540         lint()
541             .files(
542                 java(
543                     """
544           package test.pkg;
545 
546           import androidx.annotation.ChecksAconfigFlag;
547 
548           public final class Flags {
549               @ChecksAconfigFlag("test.pkg.myFlag")
550               public static boolean myFlag() { return true; }
551           }
552           """
553                 ),
554                 java(
555                         """
556             package test.pkg;
557 
558             import android.annotation.FlaggedApi;
559             import static test.pkg.Flags.myFlag;
560 
561             /** @noinspection InstantiationOfUtilityClass, AccessStaticViaInstance , ResultOfMethodCallIgnored , StatementWithEmptyBody */
562             public class JavaTest {
563                 @FlaggedApi("test.pkg.myFlag")
564                 public static class Foo {
565                     public static boolean someMethod() { return true; }
566                 }
567 
568                 public void testValid1(boolean something) {
569                     if (true && something && Flags.myFlag()) {
570                         Foo f = new Foo(); // OK 1
571                         f.someMethod();    // OK 2
572                     }
573                 }
574 
575                 public void testValid2(boolean something) {
576                     if (something || !Flags.myFlag()) {
577                     } else {
578                         Foo f = new Foo(); // OK 3
579                         f.someMethod();    // OK 4
580                     }
581                 }
582 
583                 public void testValid3(Foo f, boolean something) {
584                     // b/b/383061307
585                     if (Flags.myFlag() && f.someMethod()) { // OK 5
586                     }
587                     if (myFlag() && f.someMethod()) { // OK 6
588                     }
589                     if (Flags.myFlag() && something && f.someMethod()) { // OK 7
590                     }
591                 }
592             }
593             """
594                     )
595                     .indented(),
596                 Stubs.FlaggedApi,
597                 Stubs.ChecksAconfigFlag,
598             )
599             .run()
600             .expectClean()
601     }
602 
testChecksAconfigFlagGating_notInAllowlist_raisesErrornull603     fun testChecksAconfigFlagGating_notInAllowlist_raisesError() {
604         val project =
605             project()
606                 .name("notallowedArtifactId")
607                 .type(ProjectDescription.Type.LIBRARY)
608                 .report(false)
609                 .files(
610                     gradle(
611                             """
612                     apply plugin: 'com.android.library'
613                     group=notallowedGroupId
614                     version=1.0.0-alpha01
615                     """
616                         )
617                         .indented(),
618                     kotlin(
619                         """
620                   package test.pkg
621 
622                   import androidx.annotation.ChecksAconfigFlag
623 
624                   object FlagsCompat {
625                       @ChecksAconfigFlag("test.pkg.myFlag")
626                       fun myFlag() { return true; }
627                   }
628                   """
629                     ),
630                     kotlin(
631                             """
632                     package test.pkg
633 
634                     import android.annotation.FlaggedApi
635 
636                     class KotlinTest {
637                         object FlaggedApiContainer {
638                             @FlaggedApi("test.pkg.myFlag")
639                             fun flaggedApi() {
640                             }
641                         }
642 
643                         fun callFlaggedApi() =
644                             if (FlagsCompat.myFlag()) {
645                                 FlaggedApiContainer.flaggedApi()
646                             }
647                     }
648                     """
649                         )
650                         .indented(),
651                     Stubs.FlaggedApi,
652                     Stubs.ChecksAconfigFlag,
653                 )
654 
655         lint()
656             .projects(project)
657             .run()
658             .expect(
659                 """
660         src/main/kotlin/test/pkg/KotlinTest.kt:14: Error: Flagged APIs are subject to additional policies and may only be called by libraries that have been allowlisted by Jetpack Working Group [AndroidXFlaggedApi]
661                     FlaggedApiContainer.flaggedApi()
662                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
663         1 error
664         """
665             )
666     }
667 
testChecksAconfigFlagGating_withBetaVersion_raisesErrornull668     fun testChecksAconfigFlagGating_withBetaVersion_raisesError() {
669         val project =
670             project()
671                 .name("allowedArtifactId")
672                 .type(ProjectDescription.Type.LIBRARY)
673                 .report(false)
674                 .files(
675                     gradle(
676                             """
677                     apply plugin: 'com.android.library'
678                     group=test
679                     version=1.0.0-beta01
680                     """
681                         )
682                         .indented(),
683                     kotlin(
684                         """
685                   package test.pkg
686 
687                   import androidx.annotation.ChecksAconfigFlag
688 
689                   object FlagsCompat {
690                       @ChecksAconfigFlag("test.pkg.myFlag")
691                       fun myFlag() { return true; }
692                   }
693                   """
694                     ),
695                     kotlin(
696                             """
697                     package test.pkg
698 
699                     import android.annotation.FlaggedApi
700 
701                     class KotlinTest {
702                         object FlaggedApiContainer {
703                             @FlaggedApi("test.pkg.myFlag")
704                             fun flaggedApi() {
705                             }
706                         }
707 
708                         fun callFlaggedApi() =
709                             if (FlagsCompat.myFlag()) {
710                                 FlaggedApiContainer.flaggedApi()
711                             }
712                     }
713                     """
714                         )
715                         .indented(),
716                     Stubs.FlaggedApi,
717                     Stubs.ChecksAconfigFlag,
718                 )
719 
720         lint()
721             .projects(project)
722             .run()
723             .expect(
724                 """
725         src/main/kotlin/test/pkg/KotlinTest.kt:14: Error: Flagged APIs may only be called during alpha and must be removed before moving to beta [AndroidXFlaggedApi]
726                     FlaggedApiContainer.flaggedApi()
727                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
728         1 error
729         """
730             )
731     }
732 }
733