@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,441 @@
1
+ import ts from "typescript";
2
+ import type { PropValue, StateInfo } from "../types.ts";
3
+
4
+ /**
5
+ * Which JSX a component actually renders at the props and state a generated spec
6
+ * gives it, and whether a given node inside that JSX is reached.
7
+ *
8
+ * The problem this solves: extractRootTag, extractStaticText and
9
+ * extractRenderMode all described a component by the LAST return in its body. A
10
+ * component that opens with `const [open] = useState(false)` and
11
+ * `if (!open) return <Trigger/>` renders the trigger — but every assertion was
12
+ * derived from the expanded branch below it, describing markup the spec never
13
+ * produces. In the dogfood app that was twelve red tests in two files.
14
+ *
15
+ * The rule is asymmetric on purpose:
16
+ *
17
+ * - A guard we CAN decide is followed exactly. That is new evidence, and it
18
+ * makes the analysis strictly more accurate.
19
+ * - A guard we CANNOT decide falls through to the tail return, which is what
20
+ * every consumer did before this module existed. Bailing there would delete
21
+ * working assertions from components that pass today, trading real coverage
22
+ * for no correctness gain — so the result is flagged `guarded` instead and
23
+ * the caller decides.
24
+ *
25
+ * Purely syntactic. `evaluate` handles a deliberately closed set of expressions
26
+ * and returns undefined — meaning "unknown", which propagates — for everything
27
+ * else. There is no type checker here and no attempt to model JavaScript.
28
+ */
29
+
30
+ export interface RenderEnv {
31
+ /** Prop name → the value the spec will pass. */
32
+ props: Map<string, PropValue>;
33
+ /** useState variable / local const → its initial value. */
34
+ state: Map<string, PropValue>;
35
+ /** Names that exist but whose value is not knowable. */
36
+ opaque: Set<string>;
37
+ }
38
+
39
+ export type Verdict = "yes" | "no" | "unknown";
40
+
41
+ export interface InitialRender {
42
+ expr: ts.Expression;
43
+ /**
44
+ * True when an undecidable early return stands between the component's entry
45
+ * and this expression, so it is the best guess rather than a certainty.
46
+ */
47
+ guarded: boolean;
48
+ /**
49
+ * The env as it stood at the render, with every local const folded in. Pass
50
+ * THIS to reachability, not the env you started with — otherwise a condition
51
+ * like `boosted` (declared two lines above the JSX) reads as unknown and every
52
+ * node behind it becomes undecidable.
53
+ */
54
+ scope: RenderEnv;
55
+ }
56
+
57
+ export function emptyEnv(): RenderEnv {
58
+ return { props: new Map(), state: new Map(), opaque: new Set() };
59
+ }
60
+
61
+ export function makeEnv(
62
+ props: Record<string, PropValue>,
63
+ unresolvedProps: string[],
64
+ state: StateInfo[],
65
+ ): RenderEnv {
66
+ const env = emptyEnv();
67
+ for (const [k, v] of Object.entries(props)) env.props.set(k, v);
68
+ // A prop whose type could not be resolved is passed as undefined, so it exists
69
+ // but decides nothing.
70
+ for (const name of unresolvedProps) env.opaque.add(name);
71
+ for (const s of state) {
72
+ if (s.kind === "useRef") {
73
+ // A ref is a { current } object for the whole lifetime of the render —
74
+ // `useRef(null)` still hands back a truthy object. Binding the variable
75
+ // to its init arg folded `useRef(null)` into null, and a condition that
76
+ // reads the ref identifier itself (`ref ? A : B`) resolved the wrong
77
+ // branch. The object itself is known (truthy, plain); what `.current`
78
+ // holds is not — it can be reassigned before the JSX runs — so it
79
+ // evaluates to unknown, never to the init arg.
80
+ env.state.set(s.varName, {});
81
+ continue;
82
+ }
83
+ if (s.initialValue !== undefined) env.state.set(s.varName, s.initialValue);
84
+ else env.opaque.add(s.varName);
85
+ }
86
+ return env;
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // evaluate
91
+ // ---------------------------------------------------------------------------
92
+
93
+ /**
94
+ * A literal-composable expression's value, or undefined for "unknown".
95
+ *
96
+ * Unknown propagates: any operand we cannot value makes the whole expression
97
+ * unknown, except where a short-circuit operator has already been decided by the
98
+ * operand we do know.
99
+ */
100
+ export function evaluate(expr: ts.Expression, env: RenderEnv): PropValue | undefined {
101
+ const node = unwrap(expr);
102
+
103
+ const literal = literalToValue(node);
104
+ if (literal !== undefined) return literal;
105
+
106
+ if (ts.isIdentifier(node)) {
107
+ if (node.text === "undefined") return undefined;
108
+ if (env.opaque.has(node.text)) return undefined;
109
+ // has(), not ??: a stored null must not fall through to the props map.
110
+ if (env.state.has(node.text)) return env.state.get(node.text);
111
+ if (env.props.has(node.text)) return env.props.get(node.text);
112
+ return undefined;
113
+ }
114
+
115
+ if (ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.ExclamationToken) {
116
+ const inner = evaluate(node.operand, env);
117
+ return inner === undefined ? undefined : !truthy(inner);
118
+ }
119
+
120
+ if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.name)) {
121
+ const target = evaluate(node.expression, env);
122
+ if (target === undefined) return undefined;
123
+ if (node.name.text === "length") {
124
+ if (Array.isArray(target)) return target.length;
125
+ if (typeof target === "string") return target.length;
126
+ return undefined;
127
+ }
128
+ if (target !== null && typeof target === "object" && !Array.isArray(target)) {
129
+ return (target as { [k: string]: PropValue })[node.name.text];
130
+ }
131
+ return undefined;
132
+ }
133
+
134
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
135
+ if (node.expression.text === "Boolean" && node.arguments.length === 1) {
136
+ const arg = evaluate(node.arguments[0] as ts.Expression, env);
137
+ return arg === undefined ? undefined : truthy(arg);
138
+ }
139
+ return undefined;
140
+ }
141
+
142
+ if (ts.isBinaryExpression(node)) return evaluateBinary(node, env);
143
+
144
+ return undefined;
145
+ }
146
+
147
+ function evaluateBinary(node: ts.BinaryExpression, env: RenderEnv): PropValue | undefined {
148
+ const op = node.operatorToken.kind;
149
+
150
+ // Short-circuit operators can resolve from the left operand alone.
151
+ // An unknown operand does not always make the result unknown: `x || true` is
152
+ // true whatever x is, and `x && false` is false. Bailing on the left operand
153
+ // alone left `disabled={pending || picked.length === 0}` undecidable — with
154
+ // `picked` starting as [] the control is disabled at mount, and clicking it
155
+ // cost a full 30-second Playwright timeout.
156
+ if (op === ts.SyntaxKind.AmpersandAmpersandToken) {
157
+ const left = evaluate(node.left, env);
158
+ if (left !== undefined && !truthy(left)) return left;
159
+ const right = evaluate(node.right, env);
160
+ if (left !== undefined) return right;
161
+ // Left unknown: only a falsy right settles it, and && yields that operand.
162
+ return right !== undefined && !truthy(right) ? right : undefined;
163
+ }
164
+ if (op === ts.SyntaxKind.BarBarToken) {
165
+ const left = evaluate(node.left, env);
166
+ if (left !== undefined && truthy(left)) return left;
167
+ const right = evaluate(node.right, env);
168
+ if (left !== undefined) return right;
169
+ return right !== undefined && truthy(right) ? right : undefined;
170
+ }
171
+ if (op === ts.SyntaxKind.QuestionQuestionToken) {
172
+ const left = evaluate(node.left, env);
173
+ if (left === undefined) return undefined;
174
+ return left === null ? evaluate(node.right, env) : left;
175
+ }
176
+
177
+ const left = evaluate(node.left, env);
178
+ const right = evaluate(node.right, env);
179
+ if (left === undefined || right === undefined) return undefined;
180
+
181
+ if (
182
+ op === ts.SyntaxKind.EqualsEqualsEqualsToken ||
183
+ op === ts.SyntaxKind.ExclamationEqualsEqualsToken
184
+ ) {
185
+ const same = sameValue(left, right);
186
+ // Guard the negation: `!undefined` is true, which would turn "cannot tell"
187
+ // into a confident "not equal".
188
+ if (same === undefined) return undefined;
189
+ return op === ts.SyntaxKind.EqualsEqualsEqualsToken ? same : !same;
190
+ }
191
+
192
+ // Ordering comparisons are only meaningful between numbers; anything else
193
+ // (string collation, date coercion) is not worth modelling.
194
+ if (typeof left !== "number" || typeof right !== "number") return undefined;
195
+ switch (op) {
196
+ case ts.SyntaxKind.LessThanToken:
197
+ return left < right;
198
+ case ts.SyntaxKind.LessThanEqualsToken:
199
+ return left <= right;
200
+ case ts.SyntaxKind.GreaterThanToken:
201
+ return left > right;
202
+ case ts.SyntaxKind.GreaterThanEqualsToken:
203
+ return left >= right;
204
+ default:
205
+ return undefined;
206
+ }
207
+ }
208
+
209
+ /** Reference equality for objects and arrays is never decidable from syntax. */
210
+ function sameValue(a: PropValue, b: PropValue): boolean | undefined {
211
+ if (a !== null && typeof a === "object") return undefined;
212
+ if (b !== null && typeof b === "object") return undefined;
213
+ return a === b;
214
+ }
215
+
216
+ export function truthy(v: PropValue): boolean {
217
+ if (v === null) return false;
218
+ if (typeof v === "boolean") return v;
219
+ if (typeof v === "number") return v !== 0;
220
+ if (typeof v === "string") return v.length > 0;
221
+ // Arrays and objects are always truthy in JS, empty or not.
222
+ return true;
223
+ }
224
+
225
+ /** Literal-composable expression → PropValue; anything dynamic → undefined. */
226
+ export function literalToValue(expr: ts.Expression): PropValue | undefined {
227
+ if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) return expr.text;
228
+ if (ts.isNumericLiteral(expr)) return Number(expr.text);
229
+ if (expr.kind === ts.SyntaxKind.TrueKeyword) return true;
230
+ if (expr.kind === ts.SyntaxKind.FalseKeyword) return false;
231
+ if (expr.kind === ts.SyntaxKind.NullKeyword) return null;
232
+ if (isNegativeNumber(expr)) return -Number((expr.operand as ts.NumericLiteral).text);
233
+ if (ts.isArrayLiteralExpression(expr)) {
234
+ const items: PropValue[] = [];
235
+ for (const el of expr.elements) {
236
+ const v = literalToValue(el);
237
+ if (v === undefined) return undefined;
238
+ items.push(v);
239
+ }
240
+ return items;
241
+ }
242
+ if (ts.isObjectLiteralExpression(expr)) {
243
+ const obj: { [k: string]: PropValue } = {};
244
+ for (const prop of expr.properties) {
245
+ if (!ts.isPropertyAssignment(prop)) return undefined;
246
+ const key =
247
+ ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name) ? prop.name.text : undefined;
248
+ if (key === undefined) return undefined;
249
+ const v = literalToValue(prop.initializer);
250
+ if (v === undefined) return undefined;
251
+ obj[key] = v;
252
+ }
253
+ return obj;
254
+ }
255
+ return undefined;
256
+ }
257
+
258
+ function isNegativeNumber(expr: ts.Expression): expr is ts.PrefixUnaryExpression {
259
+ return (
260
+ ts.isPrefixUnaryExpression(expr) &&
261
+ expr.operator === ts.SyntaxKind.MinusToken &&
262
+ ts.isNumericLiteral(expr.operand)
263
+ );
264
+ }
265
+
266
+ // ---------------------------------------------------------------------------
267
+ // initialRenderExpression
268
+ // ---------------------------------------------------------------------------
269
+
270
+ /**
271
+ * The JSX the component yields on its first render with `env`'s values.
272
+ *
273
+ * Walks the body in statement order so an early return that we can prove is
274
+ * taken wins over the tail return. Local `const`s are folded into the env as we
275
+ * pass them, which is what lets `const show = weight > 1` decide a guard below
276
+ * it without any special-casing.
277
+ */
278
+ export function initialRenderExpression(
279
+ bodyNode: ts.Node,
280
+ env: RenderEnv,
281
+ ): InitialRender | undefined {
282
+ const fn = bodyNode as ts.FunctionLikeDeclaration;
283
+ const body = fn.body;
284
+ if (!body) return undefined;
285
+ if (!ts.isBlock(body)) return { expr: unwrap(body), guarded: false, scope: env };
286
+
287
+ // Cloned so folding locals never leaks back into the caller's env.
288
+ const scope: RenderEnv = {
289
+ props: env.props,
290
+ state: new Map(env.state),
291
+ opaque: new Set(env.opaque),
292
+ };
293
+ let guarded = false;
294
+
295
+ for (const stmt of body.statements) {
296
+ if (ts.isVariableStatement(stmt)) {
297
+ foldLocals(stmt, scope);
298
+ continue;
299
+ }
300
+
301
+ if (ts.isReturnStatement(stmt)) {
302
+ return stmt.expression ? { expr: unwrap(stmt.expression), guarded, scope } : undefined;
303
+ }
304
+
305
+ if (ts.isIfStatement(stmt)) {
306
+ const taken = evaluate(stmt.expression, scope);
307
+ if (taken === undefined) {
308
+ // Undecidable. If the guard renders nothing we can still describe the
309
+ // tail; if it renders content, no single answer describes the component.
310
+ const bail = returnedExpression(stmt.thenStatement);
311
+ if (bail && !rendersNothing(bail)) return undefined;
312
+ guarded = true;
313
+ continue;
314
+ }
315
+ const branch = truthy(taken) ? stmt.thenStatement : stmt.elseStatement;
316
+ if (!branch) continue;
317
+ const returned = returnedExpression(branch);
318
+ if (returned) return { expr: unwrap(returned), guarded, scope };
319
+ // A taken branch that does not return (a side effect, a throw) leaves the
320
+ // walk to continue; a throw is not a render we can describe.
321
+ if (containsThrow(branch)) return undefined;
322
+ }
323
+
324
+ // Anything else — an expression statement, a hook call — does not change
325
+ // which JSX comes back.
326
+ }
327
+
328
+ return undefined;
329
+ }
330
+
331
+ /** `const x = <literal>` folded into scope; anything dynamic becomes opaque. */
332
+ function foldLocals(stmt: ts.VariableStatement, scope: RenderEnv): void {
333
+ for (const decl of stmt.declarationList.declarations) {
334
+ if (!ts.isIdentifier(decl.name)) continue;
335
+ const value = decl.initializer ? evaluate(decl.initializer, scope) : undefined;
336
+ if (value === undefined) {
337
+ scope.opaque.add(decl.name.text);
338
+ scope.state.delete(decl.name.text);
339
+ } else {
340
+ scope.opaque.delete(decl.name.text);
341
+ scope.state.set(decl.name.text, value);
342
+ }
343
+ }
344
+ }
345
+
346
+ /** The expression of a `return` that is the whole branch, if there is one. */
347
+ function returnedExpression(branch: ts.Statement): ts.Expression | undefined {
348
+ if (ts.isReturnStatement(branch)) return branch.expression;
349
+ if (ts.isBlock(branch)) {
350
+ for (const s of branch.statements) {
351
+ if (ts.isReturnStatement(s)) return s.expression;
352
+ }
353
+ }
354
+ return undefined;
355
+ }
356
+
357
+ function containsThrow(branch: ts.Statement): boolean {
358
+ if (ts.isThrowStatement(branch)) return true;
359
+ if (ts.isBlock(branch)) return branch.statements.some((s) => ts.isThrowStatement(s));
360
+ return false;
361
+ }
362
+
363
+ function rendersNothing(expr: ts.Expression): boolean {
364
+ const node = unwrap(expr);
365
+ if (node.kind === ts.SyntaxKind.NullKeyword || node.kind === ts.SyntaxKind.FalseKeyword) {
366
+ return true;
367
+ }
368
+ return ts.isIdentifier(node) && node.text === "undefined";
369
+ }
370
+
371
+ // ---------------------------------------------------------------------------
372
+ // reachability
373
+ // ---------------------------------------------------------------------------
374
+
375
+ /**
376
+ * Whether `node` is rendered when the component renders `root`.
377
+ *
378
+ * "no" beats "unknown": one ancestor proving the node is on an untaken branch
379
+ * settles it regardless of what the others say.
380
+ */
381
+ export function reachability(node: ts.Node, root: ts.Node, env: RenderEnv): Verdict {
382
+ let sawUnknown = false;
383
+
384
+ for (let child = node, p = node.parent; p && child !== root; child = p, p = p.parent) {
385
+ const verdict = stepVerdict(child, p, env);
386
+ if (verdict === "no") return "no";
387
+ if (verdict === "unknown") sawUnknown = true;
388
+ }
389
+
390
+ return sawUnknown ? "unknown" : "yes";
391
+ }
392
+
393
+ function stepVerdict(child: ts.Node, parent: ts.Node, env: RenderEnv): Verdict {
394
+ if (ts.isConditionalExpression(parent)) {
395
+ if (child === parent.condition) return "yes";
396
+ const cond = evaluate(parent.condition, env);
397
+ if (cond === undefined) return "unknown";
398
+ const takenBranch = truthy(cond) ? parent.whenTrue : parent.whenFalse;
399
+ return child === takenBranch ? "yes" : "no";
400
+ }
401
+
402
+ if (ts.isBinaryExpression(parent)) {
403
+ const op = parent.operatorToken.kind;
404
+ const shortCircuit =
405
+ op === ts.SyntaxKind.AmpersandAmpersandToken ||
406
+ op === ts.SyntaxKind.BarBarToken ||
407
+ op === ts.SyntaxKind.QuestionQuestionToken;
408
+ if (!shortCircuit) return "yes";
409
+ if (child === parent.left) return "yes";
410
+ const left = evaluate(parent.left, env);
411
+ if (left === undefined) return "unknown";
412
+ if (op === ts.SyntaxKind.AmpersandAmpersandToken) return truthy(left) ? "yes" : "no";
413
+ if (op === ts.SyntaxKind.BarBarToken) return truthy(left) ? "no" : "yes";
414
+ return left === null ? "yes" : "no";
415
+ }
416
+
417
+ if (ts.isIfStatement(parent)) {
418
+ if (child === parent.expression) return "yes";
419
+ const cond = evaluate(parent.expression, env);
420
+ if (cond === undefined) return "unknown";
421
+ const takenBranch = truthy(cond) ? parent.thenStatement : parent.elseStatement;
422
+ return child === takenBranch ? "yes" : "no";
423
+ }
424
+
425
+ // A row count is never knowable from syntax, and a callback body may not run
426
+ // at all — a render prop, an event handler, a `.map` over an empty array.
427
+ if (ts.isIterationStatement(parent, false)) return "unknown";
428
+ if (ts.isArrowFunction(parent) || ts.isFunctionExpression(parent)) return "unknown";
429
+ if (ts.isFunctionDeclaration(parent)) return "unknown";
430
+ // The call decides whether and how its argument reaches the DOM.
431
+ if (ts.isCallExpression(parent) && child !== parent.expression) return "unknown";
432
+ if (ts.isSwitchStatement(parent) || ts.isCaseClause(parent)) return "unknown";
433
+
434
+ return "yes";
435
+ }
436
+
437
+ export function unwrap(expr: ts.Expression): ts.Expression {
438
+ let current = expr;
439
+ while (ts.isParenthesizedExpression(current)) current = current.expression;
440
+ return current;
441
+ }