From 7ca499c1a203237d4005fdc74f9b19cef3d556d6 Mon Sep 17 00:00:00 2001 From: snowingfox Date: Mon, 10 Aug 2026 12:12:39 +0000 Subject: [PATCH] Fix PackageList.java generation for dependencies with multiple package instances Summary: Dependencies that register more than one React package via react-native.config.js (e.g. react-native-appsflyer) supply a packageImportPath with multiple import statements and a packageInstance with multiple "new X()" expressions. The FQCN rewriting added in #54736 only considered the FIRST import, so the 2nd+ package instances were emitted as bare class names with no import and the generated PackageList.java failed to compile ("cannot find symbol"). Extract the FQCN from every import line and rewrite every short class name in the package instance string with its FQCN. Single-instance behaviour is unchanged. Changelog: [ANDROID] [FIXED] - Autolink: qualify every package instance in the generated PackageList.java when a dependency registers multiple packages Test Plan: Added a GeneratePackageListTaskTest case with a dependency that registers two packages (two imports, two instances). It failed before the fix (the 2nd instance was emitted as a bare class name) and passes after. Full react-native-gradle-plugin suite: 216 tests, 0 failures. Co-Authored-By: Claude --- .../react/tasks/GeneratePackageListTask.kt | 34 ++++++++++++------ .../tasks/GeneratePackageListTaskTest.kt | 36 +++++++++++++++++++ 2 files changed, 60 insertions(+), 10 deletions(-) diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GeneratePackageListTask.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GeneratePackageListTask.kt index 3bc2386901a..31e2130d87b 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GeneratePackageListTask.kt +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GeneratePackageListTask.kt @@ -64,10 +64,19 @@ abstract class GeneratePackageListTask : DefaultTask() { * Extracts the fully qualified class name from an import statement. E.g., "import * com.foo.bar.MyClass;" -> "com.foo.bar.MyClass" */ - internal fun extractFqcnFromImport(importStatement: String): String? { - val match = Regex("import\\s+([\\w.]+)\\s*;").find(importStatement) - return match?.groupValues?.get(1) - } + internal fun extractFqcnFromImport(importStatement: String): String? = + extractFqcsFromImports(importStatement).firstOrNull() + + /** + * Extracts the fully qualified class name from every import statement. E.g., "import + * com.foo.bar.MyClass;\nimport com.foo.baz.MyOtherClass;" -> ["com.foo.bar.MyClass", + * "com.foo.baz.MyOtherClass"] + */ + internal fun extractFqcsFromImports(importStatements: String): List = + Regex("import\\s+([\\w.]+)\\s*;") + .findAll(importStatements) + .map { it.groupValues[1] } + .toList() internal fun composePackageInstance( packageName: String, @@ -87,14 +96,19 @@ abstract class GeneratePackageListTask : DefaultTask() { val interpolated = interpolateDynamicValues(packageInstance, packageName) // Use FQCN to avoid class name collisions between different packages - val fqcn = extractFqcnFromImport(interpolateDynamicValues(packageImportPath, packageName)) + val interpolatedImportPath = interpolateDynamicValues(packageImportPath, packageName) + val fqcnByClassName = + extractFqcsFromImports(interpolatedImportPath) + .associateBy { it.substringAfterLast('.') } val fqcnInstance = - if (fqcn != null) { - val className = fqcn.substringAfterLast('.') - // Replace the short class name with FQCN in the instance - interpolated.replace(Regex("\\b${Regex.escape(className)}\\b")) { fqcn } - } else { + if (fqcnByClassName.isEmpty()) { interpolated + } else { + val classNamePattern = fqcnByClassName.keys.joinToString("|") { Regex.escape(it) } + // Replace every short class name with its FQCN in the instance + interpolated.replace(Regex("\\b(?:$classNamePattern)\\b")) { match -> + fqcnByClassName.getValue(match.value) + } } // Add comment with package name before each instance diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GeneratePackageListTaskTest.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GeneratePackageListTaskTest.kt index 789d7a75df2..ee3d4203443 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GeneratePackageListTaskTest.kt +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GeneratePackageListTaskTest.kt @@ -83,6 +83,42 @@ class GeneratePackageListTaskTest { ) } + @Test + fun composePackageInstance_withMultipleImportsInOneDependency_returnsFqcnForEachInstance() { + val task = createTestTask() + val packageName = "com.facebook.react" + + val result = + task.composePackageInstance( + packageName, + mapOf( + "@react-native/appsflyer" to + ModelAutolinkingDependenciesPlatformAndroidJson( + sourceDir = "./appsflyer/directory", + packageImportPath = + """ + import com.appsflyer.reactnative.RNAppsFlyerPackage; + import com.appsflyer.reactnative.PCAppsFlyerPackage; + """ + .trimIndent(), + packageInstance = + """ + new RNAppsFlyerPackage(), + new PCAppsFlyerPackage() + """ + .trimIndent(), + buildTypes = emptyList(), + ), + ), + ) + + assertThat(result) + .contains("new com.appsflyer.reactnative.RNAppsFlyerPackage()") + .contains("new com.appsflyer.reactnative.PCAppsFlyerPackage()") + .doesNotContain("new RNAppsFlyerPackage()") + .doesNotContain("new PCAppsFlyerPackage()") + } + @Test fun interpolateDynamicValues_withNoBuildConfigOrROccurrencies_doesNothing() { val packageName = "com.facebook.react"