Skip to content

Commit 71e4936

Browse files
committed
Fix autolinking for libraries with multiple ReactPackage classes
`extractFqcnFromImport` used `Regex.find()` which only returned the first FQCN from `packageImportPath`. Libraries registering multiple ReactPackage classes (e.g. react-native-appsflyer with both RNAppsFlyerPackage and PCAppsFlyerPackage) would have only the first class qualified, leaving others as bare class names causing `cannot find symbol` compilation errors in the generated PackageList.java. Additionally, the replacement regex `\bClassName\b` could match inside already-qualified names (since `.` is not a word character), causing double-qualification if packageInstance already contained FQCNs. Changes: - Add `extractAllFqcnsFromImport` that uses `Regex.findAll()` to extract all FQCNs from import statements - Replace bare class names sequentially with a `(?<!\.)` negative lookbehind to prevent double-qualification - Add regression tests for multi-package libraries and pre-qualified packageInstance entries Made-with: Cursor
1 parent 3483dfa commit 71e4936

File tree

2 files changed

+106
-7
lines changed

2 files changed

+106
-7
lines changed

packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GeneratePackageListTask.kt

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,13 @@ abstract class GeneratePackageListTask : DefaultTask() {
6969
return match?.groupValues?.get(1)
7070
}
7171

72+
/**
73+
* Extracts all fully qualified class names from one or more import statements. E.g., "import
74+
* com.foo.bar.A;\nimport com.foo.bar.B;" -> ["com.foo.bar.A", "com.foo.bar.B"]
75+
*/
76+
internal fun extractAllFqcnsFromImport(importStatements: String): List<String> =
77+
Regex("import\\s+([\\w.]+)\\s*;").findAll(importStatements).map { it.groupValues[1] }.toList()
78+
7279
internal fun composePackageInstance(
7380
packageName: String,
7481
packages: Map<String, ModelAutolinkingDependenciesPlatformAndroidJson>,
@@ -86,15 +93,17 @@ abstract class GeneratePackageListTask : DefaultTask() {
8693
val packageImportPath = dep.packageImportPath
8794
val interpolated = interpolateDynamicValues(packageInstance, packageName)
8895

89-
// Use FQCN to avoid class name collisions between different packages
90-
val fqcn = extractFqcnFromImport(interpolateDynamicValues(packageImportPath, packageName))
96+
// Use FQCNs to avoid class name collisions between different packages.
97+
// A library may register multiple ReactPackage classes (e.g. react-native-appsflyer),
98+
// so we extract all FQCNs and replace each bare class name individually.
99+
val fqcns =
100+
extractAllFqcnsFromImport(interpolateDynamicValues(packageImportPath, packageName))
91101
val fqcnInstance =
92-
if (fqcn != null) {
102+
fqcns.fold(interpolated) { acc, fqcn ->
93103
val className = fqcn.substringAfterLast('.')
94-
// Replace the short class name with FQCN in the instance
95-
interpolated.replace(Regex("\\b${Regex.escape(className)}\\b")) { fqcn }
96-
} else {
97-
interpolated
104+
// Negative lookbehind (?<!\.) ensures we only replace bare class names,
105+
// not ones already part of a fully qualified name (e.g. com.foo.ClassName).
106+
acc.replace(Regex("(?<!\\.)\\b${Regex.escape(className)}\\b")) { fqcn }
98107
}
99108

100109
// Add comment with package name before each instance

packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GeneratePackageListTaskTest.kt

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,34 @@ class GeneratePackageListTaskTest {
5656
assertThat(result).isNull()
5757
}
5858

59+
@Test
60+
fun extractAllFqcnsFromImport_withMultipleImports_returnsAll() {
61+
val task = createTestTask<GeneratePackageListTask>()
62+
val result =
63+
task.extractAllFqcnsFromImport(
64+
"import com.appsflyer.reactnative.RNAppsFlyerPackage;\nimport com.appsflyer.reactnative.PCAppsFlyerPackage;"
65+
)
66+
assertThat(result)
67+
.containsExactly(
68+
"com.appsflyer.reactnative.RNAppsFlyerPackage",
69+
"com.appsflyer.reactnative.PCAppsFlyerPackage"
70+
)
71+
}
72+
73+
@Test
74+
fun extractAllFqcnsFromImport_withSingleImport_returnsSingleElement() {
75+
val task = createTestTask<GeneratePackageListTask>()
76+
val result = task.extractAllFqcnsFromImport("import com.facebook.react.APackage;")
77+
assertThat(result).containsExactly("com.facebook.react.APackage")
78+
}
79+
80+
@Test
81+
fun extractAllFqcnsFromImport_withNoValidImport_returnsEmpty() {
82+
val task = createTestTask<GeneratePackageListTask>()
83+
val result = task.extractAllFqcnsFromImport("not an import statement")
84+
assertThat(result).isEmpty()
85+
}
86+
5987
@Test
6088
fun composePackageInstance_withNoPackages_returnsEmpty() {
6189
val task = createTestTask<GeneratePackageListTask>()
@@ -83,6 +111,68 @@ class GeneratePackageListTaskTest {
83111
)
84112
}
85113

114+
@Test
115+
fun composePackageInstance_withMultiplePackagesPerLibrary_qualifiesAll() {
116+
val task = createTestTask<GeneratePackageListTask>()
117+
val packageName = "com.example.app"
118+
119+
val multiPackageDeps =
120+
mapOf(
121+
"react-native-appsflyer" to
122+
ModelAutolinkingDependenciesPlatformAndroidJson(
123+
sourceDir = "./node_modules/react-native-appsflyer/android",
124+
packageImportPath =
125+
"import com.appsflyer.reactnative.RNAppsFlyerPackage;\nimport com.appsflyer.reactnative.PCAppsFlyerPackage;",
126+
packageInstance =
127+
"new RNAppsFlyerPackage(),\nnew PCAppsFlyerPackage()",
128+
buildTypes = emptyList(),
129+
),
130+
)
131+
132+
val result = task.composePackageInstance(packageName, multiPackageDeps)
133+
assertThat(result)
134+
.isEqualTo(
135+
"""
136+
,
137+
// react-native-appsflyer
138+
new com.appsflyer.reactnative.RNAppsFlyerPackage(),
139+
new com.appsflyer.reactnative.PCAppsFlyerPackage()
140+
"""
141+
.trimIndent()
142+
)
143+
}
144+
145+
@Test
146+
fun composePackageInstance_withPreQualifiedInstances_doesNotDoubleQualify() {
147+
val task = createTestTask<GeneratePackageListTask>()
148+
val packageName = "com.example.app"
149+
150+
val preQualifiedDeps =
151+
mapOf(
152+
"react-native-appsflyer" to
153+
ModelAutolinkingDependenciesPlatformAndroidJson(
154+
sourceDir = "./node_modules/react-native-appsflyer/android",
155+
packageImportPath =
156+
"import com.appsflyer.reactnative.RNAppsFlyerPackage;\nimport com.appsflyer.reactnative.PCAppsFlyerPackage;",
157+
packageInstance =
158+
"new com.appsflyer.reactnative.RNAppsFlyerPackage(),\nnew com.appsflyer.reactnative.PCAppsFlyerPackage()",
159+
buildTypes = emptyList(),
160+
),
161+
)
162+
163+
val result = task.composePackageInstance(packageName, preQualifiedDeps)
164+
assertThat(result)
165+
.isEqualTo(
166+
"""
167+
,
168+
// react-native-appsflyer
169+
new com.appsflyer.reactnative.RNAppsFlyerPackage(),
170+
new com.appsflyer.reactnative.PCAppsFlyerPackage()
171+
"""
172+
.trimIndent()
173+
)
174+
}
175+
86176
@Test
87177
fun interpolateDynamicValues_withNoBuildConfigOrROccurrencies_doesNothing() {
88178
val packageName = "com.facebook.react"

0 commit comments

Comments
 (0)