Skip to content

Commit 3070c36

Browse files
committed
fix(react-native): Enhance fragment detection for indirect references
1 parent 469d0e5 commit 3070c36

File tree

3 files changed

+837
-30
lines changed

3 files changed

+837
-30
lines changed

packages/babel-plugin-component-annotate/src/index.ts

Lines changed: 140 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ type AnnotationPlugin = PluginObj<AnnotationPluginPass>;
6161
export default function componentNameAnnotatePlugin({ types: t }: typeof Babel): AnnotationPlugin {
6262
return {
6363
visitor: {
64+
Program: {
65+
enter(path, state) {
66+
const fragmentContext = collectFragmentContext(path);
67+
state['sentryFragmentContext'] = fragmentContext;
68+
}
69+
},
6470
FunctionDeclaration(path, state) {
6571
if (!path.node.id || !path.node.id.name) {
6672
return;
@@ -69,14 +75,17 @@ export default function componentNameAnnotatePlugin({ types: t }: typeof Babel):
6975
return;
7076
}
7177

78+
const fragmentContext = state['sentryFragmentContext'] as FragmentContext | undefined;
79+
7280
functionBodyPushAttributes(
7381
state.opts["annotate-fragments"] === true,
7482
t,
7583
path,
7684
path.node.id.name,
7785
sourceFileNameFromState(state),
7886
attributeNamesFromState(state),
79-
state.opts.ignoredComponents ?? []
87+
state.opts.ignoredComponents ?? [],
88+
fragmentContext
8089
);
8190
},
8291
ArrowFunctionExpression(path, state) {
@@ -97,14 +106,17 @@ export default function componentNameAnnotatePlugin({ types: t }: typeof Babel):
97106
return;
98107
}
99108

109+
const fragmentContext = state['sentryFragmentContext'] as FragmentContext | undefined;
110+
100111
functionBodyPushAttributes(
101112
state.opts["annotate-fragments"] === true,
102113
t,
103114
path,
104115
parent.id.name,
105116
sourceFileNameFromState(state),
106117
attributeNamesFromState(state),
107-
state.opts.ignoredComponents ?? []
118+
state.opts.ignoredComponents ?? [],
119+
fragmentContext
108120
);
109121
},
110122
ClassDeclaration(path, state) {
@@ -120,6 +132,8 @@ export default function componentNameAnnotatePlugin({ types: t }: typeof Babel):
120132

121133
const ignoredComponents = state.opts.ignoredComponents ?? [];
122134

135+
const fragmentContext = state['sentryFragmentContext'] as FragmentContext | undefined;
136+
123137
render.traverse({
124138
ReturnStatement(returnStatement) {
125139
const arg = returnStatement.get("argument");
@@ -135,7 +149,8 @@ export default function componentNameAnnotatePlugin({ types: t }: typeof Babel):
135149
name.node && name.node.name,
136150
sourceFileNameFromState(state),
137151
attributeNamesFromState(state),
138-
ignoredComponents
152+
ignoredComponents,
153+
fragmentContext
139154
);
140155
},
141156
});
@@ -151,7 +166,8 @@ function functionBodyPushAttributes(
151166
componentName: string,
152167
sourceFileName: string | undefined,
153168
attributeNames: string[],
154-
ignoredComponents: string[]
169+
ignoredComponents: string[],
170+
fragmentContext?: FragmentContext
155171
): void {
156172
let jsxNode: Babel.NodePath;
157173

@@ -200,7 +216,8 @@ function functionBodyPushAttributes(
200216
componentName,
201217
sourceFileName,
202218
attributeNames,
203-
ignoredComponents
219+
ignoredComponents,
220+
fragmentContext
204221
);
205222
}
206223
const alternate = arg.get("alternate");
@@ -212,7 +229,8 @@ function functionBodyPushAttributes(
212229
componentName,
213230
sourceFileName,
214231
attributeNames,
215-
ignoredComponents
232+
ignoredComponents,
233+
fragmentContext
216234
);
217235
}
218236
return;
@@ -236,7 +254,8 @@ function functionBodyPushAttributes(
236254
componentName,
237255
sourceFileName,
238256
attributeNames,
239-
ignoredComponents
257+
ignoredComponents,
258+
fragmentContext
240259
);
241260
}
242261

@@ -247,7 +266,8 @@ function processJSX(
247266
componentName: string | null,
248267
sourceFileName: string | undefined,
249268
attributeNames: string[],
250-
ignoredComponents: string[]
269+
ignoredComponents: string[],
270+
fragmentContext?: FragmentContext
251271
): void {
252272
if (!jsxNode) {
253273
return;
@@ -264,7 +284,8 @@ function processJSX(
264284
componentName,
265285
sourceFileName,
266286
attributeNames,
267-
ignoredComponents
287+
ignoredComponents,
288+
fragmentContext
268289
);
269290
});
270291

@@ -300,7 +321,8 @@ function processJSX(
300321
componentName,
301322
sourceFileName,
302323
attributeNames,
303-
ignoredComponents
324+
ignoredComponents,
325+
fragmentContext
304326
);
305327
} else {
306328
processJSX(
@@ -310,7 +332,8 @@ function processJSX(
310332
null,
311333
sourceFileName,
312334
attributeNames,
313-
ignoredComponents
335+
ignoredComponents,
336+
fragmentContext
314337
);
315338
}
316339
});
@@ -322,11 +345,12 @@ function applyAttributes(
322345
componentName: string | null,
323346
sourceFileName: string | undefined,
324347
attributeNames: string[],
325-
ignoredComponents: string[]
348+
ignoredComponents: string[],
349+
fragmentContext?: FragmentContext
326350
): void {
327351
const [componentAttributeName, elementAttributeName, sourceFileAttributeName] = attributeNames;
328352

329-
if (isReactFragment(t, openingElement)) {
353+
if (isReactFragment(t, openingElement, fragmentContext)) {
330354
return;
331355
}
332356
// e.g., Raw JSX text like the `A` in `<h1>a</h1>`
@@ -443,18 +467,106 @@ function attributeNamesFromState(state: AnnotationPluginPass): [string, string,
443467
return [webComponentName, webElementName, webSourceFileName];
444468
}
445469

446-
function isReactFragment(t: typeof Babel.types, openingElement: Babel.NodePath): boolean {
470+
interface FragmentContext {
471+
fragmentAliases: Set<string>;
472+
reactNamespaceAliases: Set<string>;
473+
}
474+
475+
function collectFragmentContext(programPath: Babel.NodePath): FragmentContext {
476+
const fragmentAliases = new Set<string>();
477+
const reactNamespaceAliases = new Set<string>(['React']); // Default React namespace
478+
479+
programPath.traverse({
480+
ImportDeclaration(importPath) {
481+
const source = importPath.node.source.value;
482+
483+
// Handle React imports
484+
if (source === 'react' || source === 'React') {
485+
importPath.node.specifiers.forEach(spec => {
486+
if (spec.type === 'ImportSpecifier' && spec.imported.type === 'Identifier') {
487+
// import { Fragment } from 'react' -> Fragment
488+
// import { Fragment as F } from 'react' -> F
489+
if (spec.imported.name === 'Fragment') {
490+
fragmentAliases.add(spec.local.name);
491+
}
492+
} else if (spec.type === 'ImportDefaultSpecifier') {
493+
// import React from 'react' -> React
494+
reactNamespaceAliases.add(spec.local.name);
495+
} else if (spec.type === 'ImportNamespaceSpecifier') {
496+
// import * as React from 'react' -> React
497+
reactNamespaceAliases.add(spec.local.name);
498+
}
499+
});
500+
}
501+
},
502+
503+
// Handle simple variable assignments only (avoid complex cases)
504+
VariableDeclarator(varPath) {
505+
if (varPath.node.init) {
506+
const init = varPath.node.init;
507+
508+
// Handle identifier assignments: const MyFragment = Fragment
509+
if (varPath.node.id.type === 'Identifier') {
510+
// Handle: const MyFragment = Fragment (only if Fragment is a known alias)
511+
if (init.type === 'Identifier' && fragmentAliases.has(init.name)) {
512+
fragmentAliases.add(varPath.node.id.name);
513+
}
514+
515+
// Handle: const MyFragment = React.Fragment (only for known React namespaces)
516+
if (init.type === 'MemberExpression' &&
517+
init.object.type === 'Identifier' &&
518+
init.property.type === 'Identifier' &&
519+
reactNamespaceAliases.has(init.object.name) &&
520+
init.property.name === 'Fragment') {
521+
fragmentAliases.add(varPath.node.id.name);
522+
}
523+
}
524+
525+
// Handle destructuring assignments: const { Fragment } = React
526+
if (varPath.node.id.type === 'ObjectPattern') {
527+
if (init.type === 'Identifier' && reactNamespaceAliases.has(init.name)) {
528+
(varPath.node.id as any).properties.forEach((prop: any) => {
529+
if (prop.type === 'ObjectProperty' &&
530+
prop.key?.type === 'Identifier' &&
531+
prop.value?.type === 'Identifier' &&
532+
prop.key.name === 'Fragment') {
533+
fragmentAliases.add(prop.value.name);
534+
}
535+
});
536+
}
537+
}
538+
}
539+
}
540+
});
541+
542+
return { fragmentAliases, reactNamespaceAliases };
543+
}
544+
545+
function isReactFragment(
546+
t: typeof Babel.types,
547+
openingElement: Babel.NodePath,
548+
context?: FragmentContext // Add this optional parameter
549+
): boolean {
550+
// Handle JSX fragments (<>)
447551
if (openingElement.isJSXFragment()) {
448552
return true;
449553
}
450554

451555
const elementName = getPathName(t, openingElement);
452556

557+
// Direct fragment references
453558
if (elementName === "Fragment" || elementName === "React.Fragment") {
454559
return true;
455560
}
456561

457562
// TODO: All these objects are typed as unknown, maybe an oversight in Babel types?
563+
564+
// Check if the element name is a known fragment alias
565+
if (context && elementName && context.fragmentAliases.has(elementName)) {
566+
return true;
567+
}
568+
569+
// Handle JSXMemberExpression
458570
if (
459571
openingElement.node &&
460572
"name" in openingElement.node &&
@@ -463,10 +575,6 @@ function isReactFragment(t: typeof Babel.types, openingElement: Babel.NodePath):
463575
"type" in openingElement.node.name &&
464576
openingElement.node.name.type === "JSXMemberExpression"
465577
) {
466-
if (!("name" in openingElement.node)) {
467-
return false;
468-
}
469-
470578
const nodeName = openingElement.node.name;
471579
if (typeof nodeName !== "object" || !nodeName) {
472580
return false;
@@ -487,9 +595,23 @@ function isReactFragment(t: typeof Babel.types, openingElement: Babel.NodePath):
487595
const objectName = "name" in nodeNameObject && nodeNameObject.name;
488596
const propertyName = "name" in nodeNameProperty && nodeNameProperty.name;
489597

598+
// React.Fragment check
490599
if (objectName === "React" && propertyName === "Fragment") {
491600
return true;
492601
}
602+
603+
// Enhanced checks using context
604+
if (context) {
605+
// Check React.Fragment pattern with known React namespaces
606+
if (context.reactNamespaceAliases.has(objectName as string) && propertyName === "Fragment") {
607+
return true;
608+
}
609+
610+
// Check MyFragment.Fragment pattern
611+
if (context.fragmentAliases.has(objectName as string) && propertyName === "Fragment") {
612+
return true;
613+
}
614+
}
493615
}
494616
}
495617

0 commit comments

Comments
 (0)