@lariocpt/flawless-react 0.0.0-stage → 0.3.1

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 +611 -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,88 @@
1
+ import { readFileSync } from "node:fs";
2
+ import ts from "typescript";
3
+
4
+ /**
5
+ * Is this LOCAL module a pure calculation — same inputs, same output, no
6
+ * world it can touch?
7
+ *
8
+ * The spec's mock loop replaces whole modules a server component reads data
9
+ * from, and a formatter like `splitCode` gets swept up with them: from one
10
+ * imported call site it looks exactly like a query. The pure-binding
11
+ * exemption rescued only names it had heard of (`cn`), so when the mock loop
12
+ * widened to a render tree's data modules, `drop-format` was mocked and
13
+ * `splitCode` came back as `{}` — `segments.map is not a function`.
14
+ *
15
+ * So ask the module itself. Pure here means: no async, no await, no yield;
16
+ * nothing imported except types (so it reaches no database through a helper
17
+ * of its own); no top-level call (nothing runs, or fetches, at import). A
18
+ * module that passes is not mocked at all — its real code renders.
19
+ */
20
+ export function isPureLocalModule(absPath: string): boolean {
21
+ let text: string;
22
+ try {
23
+ text = readFileSync(absPath, "utf8");
24
+ } catch {
25
+ return false;
26
+ }
27
+ if (/^\s*['"]use server['"]/.test(text)) return false;
28
+ const kind = absPath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
29
+ let sf: ts.SourceFile;
30
+ try {
31
+ sf = ts.createSourceFile(absPath, text, ts.ScriptTarget.Latest, true, kind);
32
+ } catch {
33
+ return false;
34
+ }
35
+
36
+ let impure = false;
37
+ const walk = (n: ts.Node, fnDepth: number): void => {
38
+ if (impure) return;
39
+ if (ts.isAwaitExpression(n) || ts.isYieldExpression(n)) {
40
+ impure = true;
41
+ return;
42
+ }
43
+ if (fnDepth === 0 && ts.isCallExpression(n)) {
44
+ impure = true; // something RUNS at import
45
+ return;
46
+ }
47
+ const isFn =
48
+ ts.isFunctionDeclaration(n) ||
49
+ ts.isFunctionExpression(n) ||
50
+ ts.isArrowFunction(n) ||
51
+ ts.isMethodDeclaration(n);
52
+ if (isFn) {
53
+ if ((n.modifiers ?? []).some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) {
54
+ impure = true;
55
+ return;
56
+ }
57
+ fnDepth += 1;
58
+ }
59
+ ts.forEachChild(n, (c) => walk(c, fnDepth));
60
+ };
61
+ walk(sf, 0);
62
+ if (impure) return false;
63
+
64
+ for (const st of sf.statements) {
65
+ if (ts.isImportDeclaration(st)) {
66
+ const clause = st.importClause;
67
+ const named =
68
+ clause !== undefined && clause.namedBindings !== undefined
69
+ ? clause.namedBindings
70
+ : undefined;
71
+ const typeOnly =
72
+ clause?.isTypeOnly === true ||
73
+ // `import { type A, type B } from …` marks each SPECIFIER type-only.
74
+ // `NamedImports` has no `isTypeOnly` of its own, so reading it off the
75
+ // list returned undefined and every such module counted as live.
76
+ (named !== undefined &&
77
+ ts.isNamedImports(named) &&
78
+ named.elements.length > 0 &&
79
+ named.elements.every((e) => e.isTypeOnly));
80
+ if (!typeOnly) return false; // imports something LIVE
81
+ } else if (ts.isExportDeclaration(st) && !st.isTypeOnly) {
82
+ return false; // `export { x } from "./live"`
83
+ } else if (ts.isExpressionStatement(st) || ts.isIfStatement(st) || ts.isForStatement(st)) {
84
+ return false; // a side effect at import time
85
+ }
86
+ }
87
+ return true;
88
+ }
@@ -0,0 +1,148 @@
1
+ import ts from "typescript";
2
+
3
+ /**
4
+ * What a file hands to another module instead of declaring: the re-export
5
+ * table. Feature-Sliced apps are built out of these — a slice's `index.ts` is
6
+ * `export * from './model'`, and the model index is
7
+ * `export { useRateQuery } from './queries'` — so the name a component imports
8
+ * is usually not one the imported file declares.
9
+ */
10
+ export interface ReexportTable {
11
+ /** `export { useX, useY as useZ } from './m'`: the EXPORTED name → the module
12
+ * it came from and the name that module uses (`useY` for `useZ`). The same
13
+ * entry is recorded for a name the file imported and handed back out with a
14
+ * module-less `export { … }`. */
15
+ named: Map<string, { specifier: string; imported: string }[]>;
16
+ /** The bare `export * from './m'` specifiers, which carry every name. */
17
+ stars: string[];
18
+ }
19
+
20
+ const EMPTY: ReexportTable = { named: new Map(), stars: [] };
21
+
22
+ /**
23
+ * Read a source file's hand-offs. Forms that are NOT a hand-off of one of the
24
+ * target's names: `export * as ns from './m'`, which exposes the namespace
25
+ * rather than a member, `export type { T } from './m'`, which is erased, and
26
+ * `export { useLocal }` of a name the file declares, which is its own.
27
+ * The bare star is the clauseless form in this parser (typescript 5.9.3,
28
+ * probed); a NamespaceExport with an empty name is the same thing written the
29
+ * other way, so both are read as a star.
30
+ */
31
+ export function reexportTableOf(sourceFile: ts.SourceFile): ReexportTable {
32
+ const named = new Map<string, { specifier: string; imported: string }[]>();
33
+ const stars: string[] = [];
34
+ const add = (exported: string, hop: { specifier: string; imported: string }) => {
35
+ const arr = named.get(exported);
36
+ if (arr) arr.push(hop);
37
+ else named.set(exported, [hop]);
38
+ };
39
+ // The local name → where it came from, read only if the file has a
40
+ // module-less `export { … }` clause to answer for.
41
+ let imported: Map<string, { specifier: string; imported: string }> | undefined;
42
+ const importedBindings = () => {
43
+ if (!imported) {
44
+ imported = new Map();
45
+ for (const stmt of sourceFile.statements) {
46
+ if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier)) continue;
47
+ const bindings = stmt.importClause?.namedBindings;
48
+ if (!bindings || !ts.isNamedImports(bindings)) continue;
49
+ for (const el of bindings.elements) {
50
+ imported.set(el.name.text, {
51
+ specifier: stmt.moduleSpecifier.text,
52
+ imported: (el.propertyName ?? el.name).text,
53
+ });
54
+ }
55
+ }
56
+ }
57
+ return imported;
58
+ };
59
+ for (const stmt of sourceFile.statements) {
60
+ if (!ts.isExportDeclaration(stmt) || stmt.isTypeOnly) continue;
61
+ const ref = stmt.moduleSpecifier;
62
+ const clause = stmt.exportClause;
63
+ if (!ref) {
64
+ // `import { Card } from './card'; export { Card }` — ESM counts that as an
65
+ // export of this module, and it is how a package republishes a component
66
+ // it does not own. Only a name the file IMPORTED is a hand-off.
67
+ if (!clause || !ts.isNamedExports(clause)) continue;
68
+ const byLocal = importedBindings();
69
+ for (const spec of clause.elements) {
70
+ const hop = byLocal.get((spec.propertyName ?? spec.name).text);
71
+ if (hop) add(spec.name.text, hop);
72
+ }
73
+ continue;
74
+ }
75
+ if (!ts.isStringLiteral(ref)) continue;
76
+ if (!clause || (ts.isNamespaceExport(clause) && !clause.name.text)) {
77
+ if (!stars.includes(ref.text)) stars.push(ref.text);
78
+ continue;
79
+ }
80
+ if (!ts.isNamedExports(clause)) continue;
81
+ for (const spec of clause.elements) {
82
+ add(spec.name.text, {
83
+ specifier: ref.text,
84
+ imported: spec.propertyName?.text ?? spec.name.text,
85
+ });
86
+ }
87
+ }
88
+ // A file with no hand-offs — which is most files — shares one empty table
89
+ // instead of allocating a map per parse.
90
+ return named.size === 0 && stars.length === 0 ? EMPTY : { named, stars };
91
+ }
92
+
93
+ /** What `followHandoff` needs from its caller: read a file's syntax tree,
94
+ * resolve a specifier the way the importer would, and say whether a file
95
+ * declares a name. A component and a hook are found by different lookups, so
96
+ * the caller decides. */
97
+ export interface HandoffReader {
98
+ read: (file: string) => Promise<ts.SourceFile | undefined> | ts.SourceFile | undefined;
99
+ resolve: (specifier: string, fromFile: string) => string | undefined;
100
+ declares: (file: string, name: string) => Promise<boolean> | boolean;
101
+ }
102
+
103
+ /**
104
+ * Follow `name` from `file` through its hand-offs to the module that DECLARES
105
+ * it — the caller checks its own file first, this answers only for the hop.
106
+ *
107
+ * Named hand-offs are exact, so they go first and a star is consulted only when
108
+ * no named hop carries the name. A hop whose module does not resolve, or whose
109
+ * target does not answer for the name, ends that hop rather than the search:
110
+ * `export * from './ui'` pointing at nothing must not lose the sibling that
111
+ * does hold it. The seen-set makes a barrel cycle cost one visit.
112
+ */
113
+ export async function followHandoff(
114
+ file: string,
115
+ name: string,
116
+ reader: HandoffReader,
117
+ ): Promise<{ file: string; name: string } | undefined> {
118
+ const seen = new Set<string>();
119
+ const search = async (
120
+ file: string,
121
+ name: string,
122
+ ): Promise<{ file: string; name: string } | undefined> => {
123
+ const key = `${file}::${name}`;
124
+ if (seen.has(key)) return undefined;
125
+ seen.add(key);
126
+ const source = await reader.read(file);
127
+ if (!source) return undefined;
128
+ const table = reexportTableOf(source);
129
+ const hops = table.named.get(name) ?? [];
130
+ for (const hop of hops) {
131
+ const target = reader.resolve(hop.specifier, file);
132
+ if (!target) continue;
133
+ if (await reader.declares(target, hop.imported)) return { file: target, name: hop.imported };
134
+ const deeper = await search(target, hop.imported);
135
+ if (deeper) return deeper;
136
+ }
137
+ if (hops.length > 0) return undefined;
138
+ for (const spec of table.stars) {
139
+ const target = reader.resolve(spec, file);
140
+ if (!target) continue;
141
+ if (await reader.declares(target, name)) return { file: target, name };
142
+ const deeper = await search(target, name);
143
+ if (deeper) return deeper;
144
+ }
145
+ return undefined;
146
+ };
147
+ return search(file, name);
148
+ }
@@ -0,0 +1,195 @@
1
+ import ts from "typescript";
2
+ import type { StateInfo } from "../types.ts";
3
+ import type { ExtractedProps } from "./extractProps.ts";
4
+ import type { ParsedComponentUnit } from "./parseFile.ts";
5
+
6
+ /**
7
+ * Shared identifier-reference machinery for the usage/state/hook extractors.
8
+ * Purely syntactic: no symbol table, no type checker. Known accepted limit:
9
+ * shadowing inside nested closures is not tracked (declaration names are
10
+ * excluded, but later reads of a shadow are misattributed to the outer name).
11
+ */
12
+
13
+ export interface ComponentScope {
14
+ /** Prop names as declared on the props type (shape keys ∪ unresolved). */
15
+ propNames: Set<string>;
16
+ /** local name → original prop name (destructuring aliases, body `const {a} = props`). */
17
+ propAliases: Map<string, string>;
18
+ /** First param identifier when the component takes a bare props object. */
19
+ paramName: string | undefined;
20
+ stateVars: Set<string>;
21
+ setterNames: Set<string>;
22
+ refNames: Set<string>;
23
+ /** `const x = useMemo(...)` / `useCallback(...)` result names. */
24
+ memoResultNames: Set<string>;
25
+ /** Other top-level body bindings: name → whether the value is function-like. */
26
+ derivedNames: Map<string, { isFunctionValued: boolean }>;
27
+ }
28
+
29
+ export function buildComponentScope(
30
+ unit: ParsedComponentUnit,
31
+ props: ExtractedProps,
32
+ state: StateInfo[],
33
+ bodyNode: ts.Node,
34
+ ): ComponentScope {
35
+ const propNames = new Set<string>([...Object.keys(props.propsShape), ...props.unresolvedProps]);
36
+ const propAliases = new Map<string, string>(unit.destructuredAliases);
37
+
38
+ const stateVars = new Set<string>();
39
+ const setterNames = new Set<string>();
40
+ const refNames = new Set<string>();
41
+ for (const s of state) {
42
+ if (s.kind === "useRef") refNames.add(s.varName);
43
+ else stateVars.add(s.varName);
44
+ if (s.setterName) setterNames.add(s.setterName);
45
+ }
46
+
47
+ const memoResultNames = new Set<string>();
48
+ const derivedNames = new Map<string, { isFunctionValued: boolean }>();
49
+ const claimed = (name: string): boolean =>
50
+ propNames.has(name) ||
51
+ propAliases.has(name) ||
52
+ stateVars.has(name) ||
53
+ setterNames.has(name) ||
54
+ refNames.has(name);
55
+
56
+ for (const stmt of bodyStatements(bodyNode)) {
57
+ if (ts.isFunctionDeclaration(stmt) && stmt.name) {
58
+ if (!claimed(stmt.name.text)) derivedNames.set(stmt.name.text, { isFunctionValued: true });
59
+ continue;
60
+ }
61
+ if (!ts.isVariableStatement(stmt)) continue;
62
+ for (const decl of stmt.declarationList.declarations) {
63
+ // `const { a, b: c } = props` → aliases onto the props object.
64
+ if (
65
+ ts.isObjectBindingPattern(decl.name) &&
66
+ decl.initializer &&
67
+ ts.isIdentifier(decl.initializer) &&
68
+ unit.paramName !== undefined &&
69
+ decl.initializer.text === unit.paramName
70
+ ) {
71
+ for (const el of decl.name.elements) {
72
+ if (!ts.isBindingElement(el) || !ts.isIdentifier(el.name)) continue;
73
+ const original =
74
+ el.propertyName && ts.isIdentifier(el.propertyName)
75
+ ? el.propertyName.text
76
+ : el.name.text;
77
+ propAliases.set(el.name.text, original);
78
+ }
79
+ continue;
80
+ }
81
+ if (!ts.isIdentifier(decl.name)) continue;
82
+ const name = decl.name.text;
83
+ if (claimed(name)) continue;
84
+ const init = decl.initializer;
85
+ if (init && ts.isCallExpression(init)) {
86
+ const callee = hookCalleeName(init.expression);
87
+ if (callee === "useMemo" || callee === "useCallback") {
88
+ memoResultNames.add(name);
89
+ continue;
90
+ }
91
+ }
92
+ const isFunctionValued =
93
+ init !== undefined && (ts.isArrowFunction(init) || ts.isFunctionExpression(init));
94
+ derivedNames.set(name, { isFunctionValued });
95
+ }
96
+ }
97
+
98
+ return {
99
+ propNames,
100
+ propAliases,
101
+ paramName: unit.paramName,
102
+ stateVars,
103
+ setterNames,
104
+ refNames,
105
+ memoResultNames,
106
+ derivedNames,
107
+ };
108
+ }
109
+
110
+ /** Statements of the component's function body (block or none for concise arrows). */
111
+ export function bodyStatements(bodyNode: ts.Node): readonly ts.Statement[] {
112
+ const fn = bodyNode as ts.FunctionLikeDeclaration;
113
+ if (fn.body && ts.isBlock(fn.body)) return fn.body.statements;
114
+ return [];
115
+ }
116
+
117
+ export interface CollectedRef {
118
+ /** Canonical name from the requested set. */
119
+ name: string;
120
+ node: ts.Identifier;
121
+ /** Reference reached through `props.x` rather than a bare identifier. */
122
+ viaParam: boolean;
123
+ }
124
+
125
+ /**
126
+ * All identifier references to `names` inside `root`, excluding non-reference
127
+ * positions (declaration names, property keys, JSX attribute names, member
128
+ * names, import/export specifiers, signature names). When `paramName` is set,
129
+ * `props.x` property accesses count as references to `x` if `x` ∈ names.
130
+ */
131
+ export function collectRefs(
132
+ root: ts.Node,
133
+ _sf: ts.SourceFile,
134
+ names: Set<string>,
135
+ paramName: string | undefined,
136
+ ): CollectedRef[] {
137
+ const out: CollectedRef[] = [];
138
+
139
+ const visit = (n: ts.Node): void => {
140
+ if (ts.isIdentifier(n) && names.has(n.text) && isReferencePosition(n)) {
141
+ out.push({ name: n.text, node: n, viaParam: false });
142
+ }
143
+ if (
144
+ paramName !== undefined &&
145
+ ts.isPropertyAccessExpression(n) &&
146
+ ts.isIdentifier(n.expression) &&
147
+ n.expression.text === paramName &&
148
+ ts.isIdentifier(n.name) &&
149
+ names.has(n.name.text)
150
+ ) {
151
+ out.push({ name: n.name.text, node: n.name, viaParam: true });
152
+ }
153
+ ts.forEachChild(n, visit);
154
+ };
155
+ visit(root);
156
+ return out;
157
+ }
158
+
159
+ function isReferencePosition(id: ts.Identifier): boolean {
160
+ const parent = id.parent;
161
+ if (!parent) return true;
162
+
163
+ // x.title — `title` is a member name, not a reference (the receiver is visited separately).
164
+ if (ts.isPropertyAccessExpression(parent) && parent.name === id) return false;
165
+ // { title: value } — non-shorthand property key.
166
+ if (ts.isPropertyAssignment(parent) && parent.name === id) return false;
167
+ // Declaration names of any kind.
168
+ if (
169
+ (ts.isVariableDeclaration(parent) ||
170
+ ts.isParameter(parent) ||
171
+ ts.isBindingElement(parent) ||
172
+ ts.isFunctionDeclaration(parent) ||
173
+ ts.isFunctionExpression(parent) ||
174
+ ts.isClassDeclaration(parent)) &&
175
+ parent.name === id
176
+ ) {
177
+ return false;
178
+ }
179
+ // JSX attribute names.
180
+ if (ts.isJsxAttribute(parent) && parent.name === id) return false;
181
+ // Import / export specifiers.
182
+ if (ts.isImportSpecifier(parent) || ts.isExportSpecifier(parent)) return false;
183
+ if (ts.isImportClause(parent) || ts.isNamespaceImport(parent)) return false;
184
+ // Type-position property/method signature names.
185
+ if ((ts.isPropertySignature(parent) || ts.isMethodSignature(parent)) && parent.name === id) {
186
+ return false;
187
+ }
188
+ return true;
189
+ }
190
+
191
+ export function hookCalleeName(expr: ts.Expression): string | undefined {
192
+ if (ts.isIdentifier(expr)) return expr.text;
193
+ if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.name)) return expr.name.text;
194
+ return undefined;
195
+ }