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