@lariocpt/flawless-react 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/.agents/skills/flawless-a11y/SKILL.md +113 -0
  2. package/.agents/skills/flawless-api/SKILL.md +159 -0
  3. package/.agents/skills/flawless-api-fixtures/SKILL.md +144 -0
  4. package/.agents/skills/flawless-e2e/SKILL.md +155 -0
  5. package/.agents/skills/flawless-expo/SKILL.md +283 -0
  6. package/.agents/skills/flawless-findings/SKILL.md +116 -0
  7. package/.agents/skills/flawless-load/SKILL.md +112 -0
  8. package/.agents/skills/flawless-nextjs/SKILL.md +198 -0
  9. package/.agents/skills/flawless-perf-harness/SKILL.md +158 -0
  10. package/.agents/skills/flawless-perf-static/SKILL.md +131 -0
  11. package/.agents/skills/flawless-react/SKILL.md +403 -0
  12. package/.agents/skills/flawless-react-rules/SKILL.md +96 -0
  13. package/.agents/skills/flawless-regen/SKILL.md +135 -0
  14. package/.agents/skills/flawless-unit-browser/SKILL.md +178 -0
  15. package/LICENSE +21 -0
  16. package/README.md +608 -2
  17. package/bin/flawless-react +4 -0
  18. package/package.json +67 -4
  19. package/scripts/postinstall.d.mts +46 -0
  20. package/scripts/postinstall.mjs +127 -0
  21. package/src/analyse/authShims.ts +220 -0
  22. package/src/analyse/bootability.ts +257 -0
  23. package/src/analyse/childBridge.ts +302 -0
  24. package/src/analyse/childrenIntoSlot.ts +66 -0
  25. package/src/analyse/compoundFamily.ts +100 -0
  26. package/src/analyse/dataExports.ts +200 -0
  27. package/src/analyse/extractChildAttrs.ts +112 -0
  28. package/src/analyse/extractDataDeps.ts +857 -0
  29. package/src/analyse/extractDisclosures.ts +186 -0
  30. package/src/analyse/extractFetchCalls.ts +81 -0
  31. package/src/analyse/extractFlows.ts +239 -0
  32. package/src/analyse/extractGuards.ts +270 -0
  33. package/src/analyse/extractHooks.ts +237 -0
  34. package/src/analyse/extractInteractions.ts +1421 -0
  35. package/src/analyse/extractLogging.ts +45 -0
  36. package/src/analyse/extractNativeA11y.ts +138 -0
  37. package/src/analyse/extractPredicates.ts +784 -0
  38. package/src/analyse/extractPropUsage.ts +162 -0
  39. package/src/analyse/extractProps.ts +1212 -0
  40. package/src/analyse/extractRenderMode.ts +165 -0
  41. package/src/analyse/extractRootTag.ts +117 -0
  42. package/src/analyse/extractState.ts +361 -0
  43. package/src/analyse/extractStaticText.ts +212 -0
  44. package/src/analyse/extractVariants.ts +186 -0
  45. package/src/analyse/harvestApiEndpoints.ts +284 -0
  46. package/src/analyse/hostProps.ts +185 -0
  47. package/src/analyse/hostTag.ts +72 -0
  48. package/src/analyse/htmlContentModel.ts +94 -0
  49. package/src/analyse/index.ts +744 -0
  50. package/src/analyse/mountPathCalls.ts +442 -0
  51. package/src/analyse/nativeContentModel.ts +129 -0
  52. package/src/analyse/nativeDialect.ts +83 -0
  53. package/src/analyse/nativeProps.ts +339 -0
  54. package/src/analyse/ownReturns.ts +31 -0
  55. package/src/analyse/parseFile.ts +919 -0
  56. package/src/analyse/pos.ts +8 -0
  57. package/src/analyse/providerWraps.ts +1977 -0
  58. package/src/analyse/pureModule.ts +88 -0
  59. package/src/analyse/reexportTable.ts +148 -0
  60. package/src/analyse/refs.ts +195 -0
  61. package/src/analyse/renderPath.ts +441 -0
  62. package/src/analyse/resolveDepReturns.ts +370 -0
  63. package/src/analyse/resolveIndexKeys.ts +98 -0
  64. package/src/analyse/resolveModule.ts +431 -0
  65. package/src/analyse/resolveTagRootTags.ts +103 -0
  66. package/src/analyse/serverActionReturns.ts +213 -0
  67. package/src/analyse/sourceCache.ts +263 -0
  68. package/src/analyse/spreadSiblings.ts +98 -0
  69. package/src/analyse/walkChildren.ts +98 -0
  70. package/src/analyse/zodSchema.ts +424 -0
  71. package/src/bin.ts +23 -0
  72. package/src/changed.ts +221 -0
  73. package/src/cli.ts +560 -0
  74. package/src/cliFlags.ts +281 -0
  75. package/src/core.ts +1229 -0
  76. package/src/docLine.ts +122 -0
  77. package/src/emitPaths.ts +51 -0
  78. package/src/expo/detect.ts +184 -0
  79. package/src/expo/refusal.ts +62 -0
  80. package/src/expo/routes.ts +145 -0
  81. package/src/index.ts +95 -0
  82. package/src/mcp/analysisTools.ts +720 -0
  83. package/src/mcp/infoTools.ts +135 -0
  84. package/src/mcp/protocol.ts +130 -0
  85. package/src/mcp/run.ts +269 -0
  86. package/src/mcp/runTools.ts +362 -0
  87. package/src/mcp/server.ts +279 -0
  88. package/src/mcp/spec.ts +86 -0
  89. package/src/mcp/support.ts +268 -0
  90. package/src/mcp/tools.ts +75 -0
  91. package/src/nextjs/apiRoutes.ts +116 -0
  92. package/src/nextjs/detect.ts +397 -0
  93. package/src/nextjs/emit.ts +106 -0
  94. package/src/nextjs/liveSpec.ts +79 -0
  95. package/src/nextjs/mswHandlers.ts +55 -0
  96. package/src/nextjs/providerWrapper.ts +603 -0
  97. package/src/nextjs/routes.ts +86 -0
  98. package/src/nextjs/swagger.ts +32 -0
  99. package/src/packageJson.ts +414 -0
  100. package/src/platform.ts +18 -0
  101. package/src/prompt.ts +132 -0
  102. package/src/reactCompiler.ts +165 -0
  103. package/src/reporters/findings.ts +156 -0
  104. package/src/reporters/report.ts +463 -0
  105. package/src/runner/runTests.ts +402 -0
  106. package/src/scaffold/adoptStyle.ts +181 -0
  107. package/src/scaffold/index.ts +253 -0
  108. package/src/scaffold/openapi.ts +183 -0
  109. package/src/scaffold/templates.ts +2379 -0
  110. package/src/scaffold-tests/index.ts +463 -0
  111. package/src/scaffold-tests/mockValues.ts +208 -0
  112. package/src/scaffold-tests/mocks.ts +794 -0
  113. package/src/scaffold-tests/nativeIndex.ts +52 -0
  114. package/src/scaffold-tests/nativeTemplates.ts +1528 -0
  115. package/src/scaffold-tests/postDispatch.ts +152 -0
  116. package/src/scaffold-tests/registry.ts +443 -0
  117. package/src/scaffold-tests/selectors.ts +493 -0
  118. package/src/scaffold-tests/selectorsNative.ts +273 -0
  119. package/src/scaffold-tests/stamp.ts +79 -0
  120. package/src/scaffold-tests/templates.ts +3578 -0
  121. package/src/skills/cli.ts +248 -0
  122. package/src/skills/compat.ts +166 -0
  123. package/src/skills/data.ts +192 -0
  124. package/src/skills/md.d.ts +7 -0
  125. package/src/skills/menu.ts +55 -0
  126. package/src/skills/spec.ts +227 -0
  127. package/src/suiteScripts.ts +110 -0
  128. package/src/tasks/a11yNative.ts +281 -0
  129. package/src/tasks/a11yScan.ts +67 -0
  130. package/src/tasks/biomeRunner.ts +301 -0
  131. package/src/tasks/biomeSweep.ts +166 -0
  132. package/src/tasks/e2eExpo.ts +436 -0
  133. package/src/tasks/e2eRoutes.ts +756 -0
  134. package/src/tasks/eslintReactRules.ts +67 -0
  135. package/src/tasks/index.ts +24 -0
  136. package/src/tasks/loadExpo.ts +273 -0
  137. package/src/tasks/loadPipes.ts +750 -0
  138. package/src/tasks/perfHarness.ts +145 -0
  139. package/src/tasks/staticPerfAnalysis.ts +510 -0
  140. package/src/tasks/types.ts +1 -0
  141. package/src/tasks/unitAndBrowser.ts +462 -0
  142. package/src/types.ts +1419 -0
  143. package/src/version.ts +12 -0
@@ -0,0 +1,186 @@
1
+ import ts from "typescript";
2
+ import type { LabelSource } from "../types.ts";
3
+
4
+ /**
5
+ * Elements that only exist in the DOM after something is clicked.
6
+ *
7
+ * A menu, dialog or popover mounts its content lazily, so a spec that queries an
8
+ * item straight after render finds nothing — and the failure looks like a wrong
9
+ * selector rather than a missing step. The shape is the same across
10
+ * base-ui/radix/shadcn and is recognisable by structure alone:
11
+ *
12
+ * <DropdownMenu> <- disclosure root
13
+ * <DropdownMenuTrigger>…</…> <- the thing to click
14
+ * <DropdownMenuContent> <- gated subtree
15
+ * <DropdownMenuItem>Light</…> <- only mounted once open
16
+ *
17
+ * A `*Trigger` with a sibling subtree rooted at `*Content` / `*Popup` / `*Panel`
18
+ * / `*Portal` / `*Popover` — that is the whole rule. No package allowlist, so it
19
+ * also covers a project's own wrappers.
20
+ */
21
+
22
+ const TRIGGER_TAG = /Trigger$/;
23
+ const GATED_ROOT_TAG = /(Content|Popup|Panel|Portal|Overlay|Popover)$/;
24
+
25
+ export interface Disclosure {
26
+ /** How a spec finds the element to click first. */
27
+ trigger: LabelSource;
28
+ /** Every node that only mounts after that click. */
29
+ gated: Set<ts.Node>;
30
+ }
31
+
32
+ export function findDisclosures(root: ts.Node): Disclosure[] {
33
+ const out: Disclosure[] = [];
34
+
35
+ const visit = (n: ts.Node): void => {
36
+ const tag = openingTagName(n);
37
+ if (tag && TRIGGER_TAG.test(tag)) {
38
+ const found = disclosureFor(n, root);
39
+ if (found) out.push(found);
40
+ }
41
+ ts.forEachChild(n, visit);
42
+ };
43
+ visit(root);
44
+
45
+ return out;
46
+ }
47
+
48
+ /** The gated subtrees that sit beside `trigger`, and how to click it. */
49
+ function disclosureFor(trigger: ts.Node, root: ts.Node): Disclosure | undefined {
50
+ const handle = triggerHandle(trigger);
51
+ if (handle.kind === "none") return undefined;
52
+
53
+ for (let a = trigger.parent; a && a !== root.parent; a = a.parent) {
54
+ if (!ts.isJsxElement(a) && !ts.isJsxFragment(a)) continue;
55
+ const gatedRoots = a.children.filter((child) => {
56
+ if (contains(child, trigger)) return false;
57
+ const tag = openingTagName(child);
58
+ return tag !== undefined && GATED_ROOT_TAG.test(tag);
59
+ });
60
+ if (gatedRoots.length === 0) continue;
61
+ // An enclosing root written `open` / `open={true}` / `defaultOpen` mounts
62
+ // its content right away — nothing to open first, so nothing is gated.
63
+ // (An expression like `open={isOpen}` stays gated: its initial value
64
+ // belongs to render-state this structural pass cannot see.)
65
+ if (ts.isJsxElement(a) && isAlwaysOpen(a.openingElement)) return undefined;
66
+
67
+ const gated = new Set<ts.Node>();
68
+ for (const subtree of gatedRoots) collectAll(subtree, gated);
69
+ return { trigger: handle, gated };
70
+ }
71
+ return undefined;
72
+ }
73
+
74
+ /** `open`/`defaultOpen` written as a bare attribute or a literal `true`. */
75
+ function isAlwaysOpen(opening: ts.JsxOpeningElement): boolean {
76
+ for (const p of opening.attributes.properties) {
77
+ if (!ts.isJsxAttribute(p) || !ts.isIdentifier(p.name)) continue;
78
+ if (p.name.text !== "open" && p.name.text !== "defaultOpen") continue;
79
+ const init = p.initializer;
80
+ if (init === undefined) return true;
81
+ if (ts.isJsxExpression(init) && init.expression?.kind === ts.SyntaxKind.TrueKeyword) {
82
+ return true;
83
+ }
84
+ }
85
+ return false;
86
+ }
87
+
88
+ /**
89
+ * Something a spec can use to click the trigger.
90
+ *
91
+ * Literal text is taken from anywhere in the trigger's subtree, INCLUDING the
92
+ * JSX inside attributes: base-ui's `render={<Button/>}` slot and shadcn's
93
+ * `<span className="sr-only">` both put the only human-readable label somewhere
94
+ * a direct-children scan never looks. Clicking that span bubbles to the button,
95
+ * so it is a valid handle.
96
+ */
97
+ function triggerHandle(trigger: ts.Node): LabelSource {
98
+ const attrs = jsxAttributes(trigger);
99
+ const testid = stringAttr(attrs, "data-testid");
100
+ if (testid) return { kind: "testid", value: testid };
101
+ const aria = stringAttr(attrs, "aria-label");
102
+ if (aria) return { kind: "aria", value: aria };
103
+
104
+ const texts: string[] = [];
105
+ const visit = (n: ts.Node): void => {
106
+ if (ts.isJsxText(n)) {
107
+ const t = n.text.replace(/\s+/g, " ").trim();
108
+ if (t) texts.push(t);
109
+ }
110
+ ts.forEachChild(n, visit);
111
+ };
112
+ visit(trigger);
113
+
114
+ // More than one candidate and we cannot tell which identifies the trigger.
115
+ const unique = [...new Set(texts)];
116
+ const only = unique.length === 1 ? unique[0] : undefined;
117
+ if (only && only.length >= 2) return { kind: "text", value: only };
118
+ // Nothing names the trigger because the APP CONSUMER is its content —
119
+ // `<AlertDialogTrigger asChild>{children}</AlertDialogTrigger>`. A unit
120
+ // spec passes CHILD_TEXT as children, so that text is the click handle.
121
+ if (childrenExprInSubtree(trigger)) return { kind: "children" };
122
+ return { kind: "none" };
123
+ }
124
+
125
+ /** True when the subtree renders the `{children}` identifier expression. */
126
+ function childrenExprInSubtree(n: ts.Node): boolean {
127
+ if (
128
+ ts.isJsxExpression(n) &&
129
+ n.expression !== undefined &&
130
+ ts.isIdentifier(n.expression) &&
131
+ n.expression.text === "children"
132
+ ) {
133
+ return true;
134
+ }
135
+ let found = false;
136
+ ts.forEachChild(n, (c) => {
137
+ if (!found && childrenExprInSubtree(c)) found = true;
138
+ });
139
+ return found;
140
+ }
141
+
142
+ function contains(haystack: ts.Node, needle: ts.Node): boolean {
143
+ for (let n: ts.Node | undefined = needle; n; n = n.parent) {
144
+ if (n === haystack) return true;
145
+ }
146
+ return false;
147
+ }
148
+
149
+ function collectAll(n: ts.Node, into: Set<ts.Node>): void {
150
+ into.add(n);
151
+ ts.forEachChild(n, (c) => collectAll(c, into));
152
+ }
153
+
154
+ function openingTagName(n: ts.Node): string | undefined {
155
+ const tagName = ts.isJsxElement(n)
156
+ ? n.openingElement.tagName
157
+ : ts.isJsxSelfClosingElement(n)
158
+ ? n.tagName
159
+ : undefined;
160
+ return tagName && ts.isIdentifier(tagName) ? tagName.text : undefined;
161
+ }
162
+
163
+ function jsxAttributes(n: ts.Node): ts.JsxAttributes | undefined {
164
+ if (ts.isJsxElement(n)) return n.openingElement.attributes;
165
+ if (ts.isJsxSelfClosingElement(n)) return n.attributes;
166
+ return undefined;
167
+ }
168
+
169
+ function stringAttr(attrs: ts.JsxAttributes | undefined, name: string): string | undefined {
170
+ if (!attrs) return undefined;
171
+ for (const a of attrs.properties) {
172
+ if (!ts.isJsxAttribute(a)) continue;
173
+ if (!ts.isIdentifier(a.name) || a.name.text !== name) continue;
174
+ const init = a.initializer;
175
+ if (init && ts.isStringLiteral(init)) return init.text;
176
+ if (
177
+ init &&
178
+ ts.isJsxExpression(init) &&
179
+ init.expression &&
180
+ ts.isStringLiteral(init.expression)
181
+ ) {
182
+ return init.expression.text;
183
+ }
184
+ }
185
+ return undefined;
186
+ }
@@ -0,0 +1,81 @@
1
+ import ts from "typescript";
2
+ import type { FetchCallInfo } from "../types.ts";
3
+ import { posOf } from "./pos.ts";
4
+ import { hookCalleeName } from "./refs.ts";
5
+
6
+ const EFFECT_HOOKS = new Set(["useEffect", "useLayoutEffect"]);
7
+
8
+ /**
9
+ * Collect fetch() call sites with their URL shape. Interpolations become
10
+ * :holes named after the interpolated identifier (or the property name of a
11
+ * member access); anything unnameable becomes :param. Non-literal URLs
12
+ * (variables, function results) are skipped — better no spec than a wrong one.
13
+ */
14
+ export function extractFetchCalls(bodyNode: ts.Node, sourceFile: ts.SourceFile): FetchCallInfo[] {
15
+ const out: FetchCallInfo[] = [];
16
+
17
+ const visit = (n: ts.Node): void => {
18
+ if (ts.isCallExpression(n) && ts.isIdentifier(n.expression) && n.expression.text === "fetch") {
19
+ const urlArg = n.arguments[0];
20
+ const parsed = urlArg ? parseUrlArg(urlArg) : undefined;
21
+ if (parsed) {
22
+ out.push({
23
+ urlTemplate: parsed.template,
24
+ holes: parsed.holes,
25
+ method: readMethod(n.arguments[1]),
26
+ pos: posOf(n, sourceFile),
27
+ inEffect: isInsideEffect(n, bodyNode),
28
+ });
29
+ }
30
+ }
31
+ ts.forEachChild(n, visit);
32
+ };
33
+ ts.forEachChild(bodyNode, visit);
34
+ return out;
35
+ }
36
+
37
+ export function parseUrlArg(arg: ts.Expression): { template: string; holes: string[] } | undefined {
38
+ if (ts.isStringLiteral(arg) || ts.isNoSubstitutionTemplateLiteral(arg)) {
39
+ return { template: arg.text, holes: [] };
40
+ }
41
+ if (ts.isTemplateExpression(arg)) {
42
+ let template = arg.head.text;
43
+ const holes: string[] = [];
44
+ for (const span of arg.templateSpans) {
45
+ const hole = holeName(span.expression, holes.length);
46
+ holes.push(hole);
47
+ template += `:${hole}${span.literal.text}`;
48
+ }
49
+ return { template, holes };
50
+ }
51
+ return undefined;
52
+ }
53
+
54
+ function holeName(expr: ts.Expression, index: number): string {
55
+ if (ts.isIdentifier(expr)) return expr.text;
56
+ if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.name)) return expr.name.text;
57
+ return `param${index}`;
58
+ }
59
+
60
+ export function readMethod(init: ts.Expression | undefined): string {
61
+ if (!init || !ts.isObjectLiteralExpression(init)) return "GET";
62
+ for (const p of init.properties) {
63
+ if (!ts.isPropertyAssignment(p)) continue;
64
+ const name = ts.isIdentifier(p.name) || ts.isStringLiteral(p.name) ? p.name.text : undefined;
65
+ if (name !== "method") continue;
66
+ if (ts.isStringLiteral(p.initializer)) return p.initializer.text.toUpperCase();
67
+ }
68
+ return "GET";
69
+ }
70
+
71
+ function isInsideEffect(call: ts.Node, bodyNode: ts.Node): boolean {
72
+ let cur: ts.Node | undefined = call.parent;
73
+ while (cur && cur !== bodyNode) {
74
+ if (ts.isCallExpression(cur)) {
75
+ const name = hookCalleeName(cur.expression);
76
+ if (name && EFFECT_HOOKS.has(name)) return true;
77
+ }
78
+ cur = cur.parent;
79
+ }
80
+ return false;
81
+ }
@@ -0,0 +1,239 @@
1
+ import ts from "typescript";
2
+ import { VOID_ELEMENTS } from "./hostProps.ts";
3
+ import { domOnlyResolver, type HostTagResolver } from "./hostTag.ts";
4
+ import { nativeAcceptsChildrenByName } from "./nativeProps.ts";
5
+ import { initialRenderExpression, type RenderEnv, reachability } from "./renderPath.ts";
6
+
7
+ /**
8
+ * Whether `children` and `className` actually reach the DOM.
9
+ *
10
+ * Both are the basis for assertions on components that render no literal copy of
11
+ * their own — the whole shadcn/ui layer, which was 40% of the generated specs and
12
+ * every one of them assertion-free. Accepting a prop is not the same as
13
+ * rendering it, so each rule below insists on seeing the value flow somewhere it
14
+ * can be observed, and gives up otherwise.
15
+ */
16
+
17
+ export interface RenderFlows {
18
+ /** A sentinel child passed in will appear in the markup. */
19
+ children: boolean;
20
+ /** A className passed in will land on some element. */
21
+ className: boolean;
22
+ }
23
+
24
+ export function extractFlows(
25
+ bodyNode: ts.Node,
26
+ env: RenderEnv,
27
+ hostTag: HostTagResolver = domOnlyResolver,
28
+ ): RenderFlows {
29
+ const initial = initialRenderExpression(bodyNode, env);
30
+ if (!initial) return { children: false, className: false };
31
+ const { expr: root, scope } = initial;
32
+ const reached = (n: ts.Node): boolean => reachability(n, root, scope) === "yes";
33
+
34
+ return {
35
+ children: childrenFlow(root, reached, hostTag),
36
+ // NativeWind compiles className into a `style` at BUILD time, through babel.
37
+ // The string does not survive to the rendered tree under jest-expo, and
38
+ // react-native-web consumes it without forwarding anything either (measured:
39
+ // docs/archive/2026-08-expo-probes.md, P1). So "a className passed in will
40
+ // land on some element" is both unfalsifiable AND unsatisfiable on native,
41
+ // and asserting it would be a test that can never pass.
42
+ //
43
+ // childrenFlow is unaffected: a sentinel child under a <View> genuinely renders.
44
+ className:
45
+ hostTag("View")?.dialect === "native" ? false : classNameFlow(root, reached, hostTag),
46
+ };
47
+ }
48
+
49
+ /**
50
+ * `{children}` rendered directly, or a `{...props}` spread onto a host element
51
+ * that can hold children.
52
+ *
53
+ * The spread case is how the entire shadcn layer works — `<div {...props} />`
54
+ * never mentions children by name — so leaving it out would miss most of the
55
+ * components this rule exists for. Void elements are excluded: <input {...props}/>
56
+ * accepts the prop and renders nothing.
57
+ */
58
+ function childrenFlow(
59
+ root: ts.Node,
60
+ reached: (n: ts.Node) => boolean,
61
+ hostTag: HostTagResolver,
62
+ ): boolean {
63
+ let found = false;
64
+ const visit = (n: ts.Node): void => {
65
+ if (found) return;
66
+ if (
67
+ ts.isJsxExpression(n) &&
68
+ n.expression &&
69
+ ts.isIdentifier(n.expression) &&
70
+ n.expression.text === "children" &&
71
+ n.parent &&
72
+ (ts.isJsxElement(n.parent) || ts.isJsxFragment(n.parent)) &&
73
+ reached(n)
74
+ ) {
75
+ found = true;
76
+ return;
77
+ }
78
+ if (ts.isJsxSpreadAttribute(n) && reached(n)) {
79
+ const owner = n.parent?.parent;
80
+ const owned = owner ? resolvedOf(owner, hostTag) : undefined;
81
+ if (owned && acceptsChildren(owned) && !explicitChildrenOverrideSpread(owner)) {
82
+ found = true;
83
+ return;
84
+ }
85
+ }
86
+ ts.forEachChild(n, visit);
87
+ };
88
+ visit(root);
89
+ return found;
90
+ }
91
+
92
+ /**
93
+ * JSX explicit children REPLACE spread children — `<svg {...props}><path/></svg>`
94
+ * never renders `props.children`, and every icon in the corpus is exactly that
95
+ * shape (SVGProps-typed, children accepted via the spread, fixed paths rendered).
96
+ * The sentinel-child test against one is not a failing component, it is a test
97
+ * asserting a JSX precedence rule backwards, so the flow must not report true.
98
+ */
99
+ function explicitChildrenOverrideSpread(owner: ts.Node | undefined): boolean {
100
+ if (!owner || !ts.isJsxElement(owner.parent)) return false;
101
+ return owner.parent.children.some(
102
+ (c) =>
103
+ (ts.isJsxText(c) && c.getText().trim().length > 0) ||
104
+ ts.isJsxElement(c) ||
105
+ ts.isJsxSelfClosingElement(c) ||
106
+ ts.isJsxFragment(c) ||
107
+ (ts.isJsxExpression(c) && c.expression !== undefined),
108
+ );
109
+ }
110
+
111
+ /**
112
+ * The `className` identifier appearing inside a `className=` attribute on a HOST
113
+ * element — directly, as an argument to any call (cn/clsx/twMerge), or inside a
114
+ * template span.
115
+ *
116
+ * Host-only is the load-bearing part. `<SeparatorPrimitive className={cn(…)} />`
117
+ * hands the class to another component, and whether that component puts it on a
118
+ * DOM node is its business, not something this file can see. Asserting on it
119
+ * would be a claim about a third-party implementation.
120
+ *
121
+ * The other way this goes wrong is a `{...props}` spread positioned AFTER the
122
+ * attribute on the same element, which overwrites it. That is checked rather
123
+ * than assumed.
124
+ */
125
+ function classNameFlow(
126
+ root: ts.Node,
127
+ reached: (n: ts.Node) => boolean,
128
+ hostTag: HostTagResolver,
129
+ ): boolean {
130
+ let found = false;
131
+ const visit = (n: ts.Node): void => {
132
+ if (found) return;
133
+ if (
134
+ ts.isJsxAttribute(n) &&
135
+ ts.isIdentifier(n.name) &&
136
+ n.name.text === "className" &&
137
+ hostTagOf(n.parent?.parent, hostTag) !== undefined &&
138
+ reached(n) &&
139
+ mentionsClassName(n) &&
140
+ !overwrittenBySpread(n)
141
+ ) {
142
+ found = true;
143
+ return;
144
+ }
145
+ ts.forEachChild(n, visit);
146
+ };
147
+ visit(root);
148
+ return found;
149
+ }
150
+
151
+ function mentionsClassName(attr: ts.JsxAttribute): boolean {
152
+ let found = false;
153
+ const visit = (n: ts.Node): void => {
154
+ if (found) return;
155
+ if (ts.isIdentifier(n) && n.text === "className" && n !== attr.name) {
156
+ found = true;
157
+ return;
158
+ }
159
+ ts.forEachChild(n, visit);
160
+ };
161
+ if (attr.initializer) visit(attr.initializer);
162
+ return found;
163
+ }
164
+
165
+ /** A spread later in the same attribute list wins, so the className is lost. */
166
+ function overwrittenBySpread(attr: ts.JsxAttribute): boolean {
167
+ const list = attr.parent;
168
+ if (!list || !ts.isJsxAttributes(list)) return false;
169
+ const index = list.properties.indexOf(attr);
170
+ if (index < 0) return false;
171
+ return list.properties
172
+ .slice(index + 1)
173
+ .some((p) => ts.isJsxSpreadAttribute(p) && !isClassNameSafeSpread(p));
174
+ }
175
+
176
+ /**
177
+ * `{...props}` after className is only harmful when props can carry className.
178
+ * A rest binding that had className destructured out of it cannot.
179
+ */
180
+ function isClassNameSafeSpread(spread: ts.JsxSpreadAttribute): boolean {
181
+ if (!ts.isIdentifier(spread.expression)) return false;
182
+ const name = spread.expression.text;
183
+ // `const { className, ...props }` — the rest holds everything BUT className.
184
+ for (let n: ts.Node | undefined = spread; n; n = n.parent) {
185
+ if (!ts.isFunctionDeclaration(n) && !ts.isArrowFunction(n) && !ts.isFunctionExpression(n)) {
186
+ continue;
187
+ }
188
+ const first = n.parameters[0];
189
+ if (!first || !ts.isObjectBindingPattern(first.name)) return false;
190
+ let sawClassName = false;
191
+ let restMatches = false;
192
+ for (const el of first.name.elements) {
193
+ if (!ts.isBindingElement(el)) continue;
194
+ if (el.dotDotDotToken) {
195
+ restMatches = ts.isIdentifier(el.name) && el.name.text === name;
196
+ continue;
197
+ }
198
+ if (ts.isIdentifier(el.name) && el.name.text === "className") sawClassName = true;
199
+ }
200
+ return sawClassName && restMatches;
201
+ }
202
+ return false;
203
+ }
204
+
205
+ /** Whether a resolved host element can hold children, in either dialect. */
206
+ function acceptsChildren(el: { name: string; dialect: "dom" | "native" }): boolean {
207
+ return el.dialect === "native"
208
+ ? nativeAcceptsChildrenByName(el.name)
209
+ : !VOID_ELEMENTS.has(el.name);
210
+ }
211
+
212
+ function resolvedOf(node: ts.Node, hostTag: HostTagResolver) {
213
+ const tagName = ts.isJsxOpeningElement(node)
214
+ ? node.tagName
215
+ : ts.isJsxSelfClosingElement(node)
216
+ ? node.tagName
217
+ : undefined;
218
+ if (!tagName || !ts.isIdentifier(tagName)) return undefined;
219
+ return hostTag(tagName.text);
220
+ }
221
+
222
+ function hostTagOf(node: ts.Node | undefined, hostTag: HostTagResolver): string | undefined {
223
+ if (!node) return undefined;
224
+ const tagName = ts.isJsxOpeningElement(node)
225
+ ? node.tagName
226
+ : ts.isJsxSelfClosingElement(node)
227
+ ? node.tagName
228
+ : undefined;
229
+ if (!tagName || !ts.isIdentifier(tagName)) return undefined;
230
+ return hostTag(tagName.text)?.name;
231
+ }
232
+
233
+ /**
234
+ * The SECOND copy of VOID_ELEMENTS lived here, three lines below its own use,
235
+ * while hostProps.ts exported the authoritative one — and the two had already
236
+ * drifted (this copy was missing "param"). Deleted in favour of the table that
237
+ * also answers the question for React Native, where "does this element take
238
+ * children" is a per-element fact rather than a list of fourteen tag names.
239
+ */