85453da49f
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled
173 lines
6.1 KiB
TypeScript
173 lines
6.1 KiB
TypeScript
/**
|
|
* Browser-safe static scan for composition-variable reads in script text.
|
|
*
|
|
* Compositions read variables by calling the runtime API — `getVariables()`
|
|
* bare (sub-comp scoped shadow) or via `__hyperframes.getVariables()` /
|
|
* `window.__hyperframes.getVariables()` — and there is no DOM-attribute
|
|
* binding to scan, so "which variables does this composition use" can only be
|
|
* derived from the scripts. This is a best-effort static analysis: the
|
|
* patterns agents actually write (destructuring, member access, a single
|
|
* alias variable) resolve to ids; anything opaque flips `scanIncomplete`
|
|
* so consumers can present usage as a lower bound instead of a fact.
|
|
*
|
|
* AST nodes are handled untyped (same convention as gsapParserAcorn.ts) —
|
|
* acorn's structural types don't survive acorn-walk's visitor signatures.
|
|
*/
|
|
|
|
import * as acorn from "acorn";
|
|
import * as acornWalk from "acorn-walk";
|
|
|
|
export interface VariableUsageScan {
|
|
/** Variable ids statically read by the script, in first-seen order. */
|
|
usedIds: string[];
|
|
/**
|
|
* True when the script accesses variables in a way the scan cannot resolve
|
|
* (computed keys, rest spreads, the values object escaping into a call…) or
|
|
* when the script fails to parse — usedIds is then a lower bound.
|
|
*/
|
|
scanIncomplete: boolean;
|
|
}
|
|
|
|
interface Sink {
|
|
use(id: string): void;
|
|
incomplete(): void;
|
|
}
|
|
|
|
// oxlint-disable no-explicit-any -- untyped acorn AST traversal, see header
|
|
|
|
function isGetVariablesCallee(callee: any): boolean {
|
|
if (callee?.type === "Identifier") return callee.name === "getVariables";
|
|
if (callee?.type === "MemberExpression" && !callee.computed) {
|
|
return callee.property?.type === "Identifier" && callee.property.name === "getVariables";
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** Collect ids from an ObjectPattern destructuring of the values object. */
|
|
// Exhaustive AST-node classification — branchy by nature, same as gsapParserAcorn.
|
|
// fallow-ignore-next-line complexity
|
|
function collectFromObjectPattern(pattern: any, out: Sink): void {
|
|
for (const prop of pattern.properties ?? []) {
|
|
if (prop?.type === "RestElement") {
|
|
out.incomplete();
|
|
continue;
|
|
}
|
|
if (prop?.type !== "Property") continue;
|
|
if (prop.computed === true) {
|
|
out.incomplete();
|
|
} else if (prop.key?.type === "Identifier") {
|
|
out.use(String(prop.key.name));
|
|
} else if (prop.key?.type === "Literal" && typeof prop.key.value === "string") {
|
|
out.use(prop.key.value);
|
|
} else {
|
|
out.incomplete();
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Collect an id from a MemberExpression reading the values object. */
|
|
function collectFromMemberAccess(member: any, out: Sink): void {
|
|
if (member.computed !== true && member.property?.type === "Identifier") {
|
|
out.use(String(member.property.name));
|
|
} else if (
|
|
member.computed === true &&
|
|
member.property?.type === "Literal" &&
|
|
typeof member.property.value === "string"
|
|
) {
|
|
out.use(member.property.value);
|
|
} else {
|
|
out.incomplete();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Classify one read of the values object (a getVariables() call result or an
|
|
* alias holding it) by its immediate syntactic context. Returns the alias
|
|
* name when the value is bound to a plain variable (`const vars = …`).
|
|
*/
|
|
// fallow-ignore-next-line complexity
|
|
function classifyValueRead(parent: any, valueNode: any, out: Sink): string | null {
|
|
if (!parent || parent.type === "ExpressionStatement") {
|
|
// Bare statement — value unused, nothing read.
|
|
return null;
|
|
}
|
|
if (parent.type === "MemberExpression" && parent.object === valueNode) {
|
|
collectFromMemberAccess(parent, out);
|
|
return null;
|
|
}
|
|
if (parent.type === "VariableDeclarator" && parent.init === valueNode) {
|
|
if (parent.id?.type === "ObjectPattern") {
|
|
collectFromObjectPattern(parent.id, out);
|
|
return null;
|
|
}
|
|
if (parent.id?.type === "Identifier") return String(parent.id.name);
|
|
out.incomplete();
|
|
return null;
|
|
}
|
|
// The values object escapes (argument, return, spread, assignment…) —
|
|
// reads beyond this point are invisible to the scan.
|
|
out.incomplete();
|
|
return null;
|
|
}
|
|
|
|
export function scanVariableUsage(scriptText: string): VariableUsageScan {
|
|
const usedIds: string[] = [];
|
|
const seen = new Set<string>();
|
|
let scanIncomplete = false;
|
|
|
|
const sink: Sink = {
|
|
use(id: string) {
|
|
if (!seen.has(id)) {
|
|
seen.add(id);
|
|
usedIds.push(id);
|
|
}
|
|
},
|
|
incomplete() {
|
|
scanIncomplete = true;
|
|
},
|
|
};
|
|
|
|
let ast: any;
|
|
try {
|
|
ast = acorn.parse(scriptText, { ecmaVersion: "latest", sourceType: "script" });
|
|
} catch {
|
|
return { usedIds: [], scanIncomplete: true };
|
|
}
|
|
|
|
const aliases = new Set<string>();
|
|
|
|
// Pass 1: classify every getVariables() call by its parent context.
|
|
acornWalk.ancestor(ast, {
|
|
CallExpression(node: any, _: unknown, ancestors: any[]) {
|
|
if (!isGetVariablesCallee(node.callee)) return;
|
|
const parent = ancestors[ancestors.length - 2];
|
|
const alias = classifyValueRead(parent, node, sink);
|
|
if (alias) aliases.add(alias);
|
|
},
|
|
} as any);
|
|
|
|
// Pass 2: classify every reference to an alias of the values object.
|
|
// Scope-naive by design: an unrelated same-named identifier can only make
|
|
// the scan report extra ids or flip scanIncomplete, never miss a read.
|
|
if (aliases.size > 0) {
|
|
acornWalk.ancestor(ast, {
|
|
// fallow-ignore-next-line complexity
|
|
Identifier(node: any, _: unknown, ancestors: any[]) {
|
|
if (!aliases.has(String(node.name))) return;
|
|
const parent = ancestors[ancestors.length - 2];
|
|
if (!parent) return;
|
|
// Skip the declarator that introduced the alias and property-position
|
|
// identifiers that merely share the name.
|
|
if (parent.type === "VariableDeclarator" && parent.id === node) return;
|
|
if (parent.type === "MemberExpression" && parent.property === node) return;
|
|
if (parent.type === "Property" && parent.key === node && parent.computed !== true) return;
|
|
// Chained aliases (const v2 = vars) are not followed — flag instead
|
|
// of silently missing reads through the second name.
|
|
if (classifyValueRead(parent, node, sink)) sink.incomplete();
|
|
},
|
|
} as any);
|
|
}
|
|
|
|
return { usedIds, scanIncomplete };
|
|
}
|