@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.
- package/.agents/skills/flawless-a11y/SKILL.md +113 -0
- package/.agents/skills/flawless-api/SKILL.md +159 -0
- package/.agents/skills/flawless-api-fixtures/SKILL.md +144 -0
- package/.agents/skills/flawless-e2e/SKILL.md +155 -0
- package/.agents/skills/flawless-expo/SKILL.md +283 -0
- package/.agents/skills/flawless-findings/SKILL.md +116 -0
- package/.agents/skills/flawless-load/SKILL.md +112 -0
- package/.agents/skills/flawless-nextjs/SKILL.md +198 -0
- package/.agents/skills/flawless-perf-harness/SKILL.md +158 -0
- package/.agents/skills/flawless-perf-static/SKILL.md +131 -0
- package/.agents/skills/flawless-react/SKILL.md +403 -0
- package/.agents/skills/flawless-react-rules/SKILL.md +96 -0
- package/.agents/skills/flawless-regen/SKILL.md +135 -0
- package/.agents/skills/flawless-unit-browser/SKILL.md +178 -0
- package/LICENSE +21 -0
- package/README.md +611 -2
- package/bin/flawless-react +4 -0
- package/package.json +67 -4
- package/scripts/postinstall.d.mts +46 -0
- package/scripts/postinstall.mjs +127 -0
- package/src/analyse/authShims.ts +220 -0
- package/src/analyse/bootability.ts +257 -0
- package/src/analyse/childBridge.ts +302 -0
- package/src/analyse/childrenIntoSlot.ts +66 -0
- package/src/analyse/compoundFamily.ts +100 -0
- package/src/analyse/dataExports.ts +200 -0
- package/src/analyse/extractChildAttrs.ts +112 -0
- package/src/analyse/extractDataDeps.ts +857 -0
- package/src/analyse/extractDisclosures.ts +186 -0
- package/src/analyse/extractFetchCalls.ts +81 -0
- package/src/analyse/extractFlows.ts +239 -0
- package/src/analyse/extractGuards.ts +270 -0
- package/src/analyse/extractHooks.ts +237 -0
- package/src/analyse/extractInteractions.ts +1421 -0
- package/src/analyse/extractLogging.ts +45 -0
- package/src/analyse/extractNativeA11y.ts +138 -0
- package/src/analyse/extractPredicates.ts +784 -0
- package/src/analyse/extractPropUsage.ts +162 -0
- package/src/analyse/extractProps.ts +1212 -0
- package/src/analyse/extractRenderMode.ts +165 -0
- package/src/analyse/extractRootTag.ts +117 -0
- package/src/analyse/extractState.ts +361 -0
- package/src/analyse/extractStaticText.ts +212 -0
- package/src/analyse/extractVariants.ts +186 -0
- package/src/analyse/harvestApiEndpoints.ts +284 -0
- package/src/analyse/hostProps.ts +185 -0
- package/src/analyse/hostTag.ts +72 -0
- package/src/analyse/htmlContentModel.ts +94 -0
- package/src/analyse/index.ts +744 -0
- package/src/analyse/mountPathCalls.ts +442 -0
- package/src/analyse/nativeContentModel.ts +129 -0
- package/src/analyse/nativeDialect.ts +83 -0
- package/src/analyse/nativeProps.ts +339 -0
- package/src/analyse/ownReturns.ts +31 -0
- package/src/analyse/parseFile.ts +919 -0
- package/src/analyse/pos.ts +8 -0
- package/src/analyse/providerWraps.ts +1977 -0
- package/src/analyse/pureModule.ts +88 -0
- package/src/analyse/reexportTable.ts +148 -0
- package/src/analyse/refs.ts +195 -0
- package/src/analyse/renderPath.ts +441 -0
- package/src/analyse/resolveDepReturns.ts +370 -0
- package/src/analyse/resolveIndexKeys.ts +98 -0
- package/src/analyse/resolveModule.ts +431 -0
- package/src/analyse/resolveTagRootTags.ts +103 -0
- package/src/analyse/serverActionReturns.ts +213 -0
- package/src/analyse/sourceCache.ts +263 -0
- package/src/analyse/spreadSiblings.ts +98 -0
- package/src/analyse/walkChildren.ts +98 -0
- package/src/analyse/zodSchema.ts +424 -0
- package/src/bin.ts +23 -0
- package/src/changed.ts +221 -0
- package/src/cli.ts +560 -0
- package/src/cliFlags.ts +281 -0
- package/src/core.ts +1229 -0
- package/src/docLine.ts +122 -0
- package/src/emitPaths.ts +51 -0
- package/src/expo/detect.ts +184 -0
- package/src/expo/refusal.ts +62 -0
- package/src/expo/routes.ts +145 -0
- package/src/index.ts +95 -0
- package/src/mcp/analysisTools.ts +720 -0
- package/src/mcp/infoTools.ts +135 -0
- package/src/mcp/protocol.ts +130 -0
- package/src/mcp/run.ts +269 -0
- package/src/mcp/runTools.ts +362 -0
- package/src/mcp/server.ts +279 -0
- package/src/mcp/spec.ts +86 -0
- package/src/mcp/support.ts +268 -0
- package/src/mcp/tools.ts +75 -0
- package/src/nextjs/apiRoutes.ts +116 -0
- package/src/nextjs/detect.ts +397 -0
- package/src/nextjs/emit.ts +106 -0
- package/src/nextjs/liveSpec.ts +79 -0
- package/src/nextjs/mswHandlers.ts +55 -0
- package/src/nextjs/providerWrapper.ts +603 -0
- package/src/nextjs/routes.ts +86 -0
- package/src/nextjs/swagger.ts +32 -0
- package/src/packageJson.ts +414 -0
- package/src/platform.ts +18 -0
- package/src/prompt.ts +132 -0
- package/src/reactCompiler.ts +165 -0
- package/src/reporters/findings.ts +156 -0
- package/src/reporters/report.ts +463 -0
- package/src/runner/runTests.ts +402 -0
- package/src/scaffold/adoptStyle.ts +181 -0
- package/src/scaffold/index.ts +253 -0
- package/src/scaffold/openapi.ts +183 -0
- package/src/scaffold/templates.ts +2379 -0
- package/src/scaffold-tests/index.ts +463 -0
- package/src/scaffold-tests/mockValues.ts +208 -0
- package/src/scaffold-tests/mocks.ts +794 -0
- package/src/scaffold-tests/nativeIndex.ts +52 -0
- package/src/scaffold-tests/nativeTemplates.ts +1528 -0
- package/src/scaffold-tests/postDispatch.ts +152 -0
- package/src/scaffold-tests/registry.ts +443 -0
- package/src/scaffold-tests/selectors.ts +493 -0
- package/src/scaffold-tests/selectorsNative.ts +273 -0
- package/src/scaffold-tests/stamp.ts +79 -0
- package/src/scaffold-tests/templates.ts +3578 -0
- package/src/skills/cli.ts +248 -0
- package/src/skills/compat.ts +166 -0
- package/src/skills/data.ts +192 -0
- package/src/skills/md.d.ts +7 -0
- package/src/skills/menu.ts +55 -0
- package/src/skills/spec.ts +227 -0
- package/src/suiteScripts.ts +110 -0
- package/src/tasks/a11yNative.ts +281 -0
- package/src/tasks/a11yScan.ts +67 -0
- package/src/tasks/biomeRunner.ts +301 -0
- package/src/tasks/biomeSweep.ts +166 -0
- package/src/tasks/e2eExpo.ts +436 -0
- package/src/tasks/e2eRoutes.ts +756 -0
- package/src/tasks/eslintReactRules.ts +67 -0
- package/src/tasks/index.ts +24 -0
- package/src/tasks/loadExpo.ts +273 -0
- package/src/tasks/loadPipes.ts +750 -0
- package/src/tasks/perfHarness.ts +145 -0
- package/src/tasks/staticPerfAnalysis.ts +510 -0
- package/src/tasks/types.ts +1 -0
- package/src/tasks/unitAndBrowser.ts +462 -0
- package/src/types.ts +1419 -0
- 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
|
+
}
|