Files
rea/src/domain/javascript/javascriptSemanticAmbientWrites.ts
T
Kaoru0822 7ad75bdc80 fix: restore JavaScript analysis and current regression checks (#3080)
Restore source-position-aware ambient aliases, scalar env/argv reads, candidate
endpoint worker data, Chrome DOMSnapshot empty strings, and accumulated Ghidra
LaunchSupport diagnostics. Align regression fixtures with declared current
contracts, while preserving identity, provenance, cancellation, and cleanup
checks. Assert workflow caveats independently at both established locations.

Validation: original five source failures reproduced on main; 307 focused tests
passed with one platform skip, 22 fixture follow-up tests and 22 workflow/MCP
tests passed; full static and docs checks passed. Real public http-server CLI
analyses preserve identical artifact identity, statistics, and 581-token selected
output. Real Chromium confirms restored DOM form destinations; isolated real
service-worker verification passes on main and the change. The complete local
browser run retained an unclassified later marker timeout; current-head CI
provides complete real-browser and recovery lane results. Native IDA/Ghidra
execution and complete local suite execution are not claimed.

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Co-authored-by: N0zoM1z0 <161784452+N0zoM1z0@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:17:54 +08:00

317 lines
11 KiB
TypeScript

import * as t from "@babel/types";
import {
semanticStaticPropertyKey,
unwrapJavaScriptExpression,
} from "./javascriptAstValues.js";
import {
collectJavaScriptIdentityWritesSteps,
electronMemberPath,
} from "./javascriptElectronMemberWrites.js";
import { createSemanticModuleInterop } from "./javascriptModuleInterop.js";
import {
isUnshadowedGlobal,
resolveSemanticBindingState,
semanticStableInitializer,
type JavaScriptSemanticAnalysisState,
} from "./javascriptSemanticState.js";
import { isImportMetaUrl } from "./javascriptStaticAnalysisHelpers.js";
import { semanticMutationInitializers } from "./javascriptSemanticMutationInitializers.js";
const GLOBAL_OBJECT_NAMES = ["globalThis", "global", "window", "self"];
const globalObjectName = (
node: t.Node,
state: JavaScriptSemanticAnalysisState,
): string | undefined =>
GLOBAL_OBJECT_NAMES.find((name) => isUnshadowedGlobal(node, state, name));
/** Global self references retain object identity, rather than adding a property. */
const canonicalAmbientPath = (
path: readonly (string | null)[],
): readonly (string | null)[] => {
if (path[0] !== "global") return path;
let firstProperty = 1;
while (GLOBAL_OBJECT_NAMES.includes(path[firstProperty] ?? ""))
firstProperty += 1;
return ["global", ...path.slice(firstProperty)];
};
type AmbientPath = readonly (string | null)[];
/** Undefined means a cyclic or conflicting projection can select any depth. */
type AmbientProjection = AmbientPath | undefined;
/**
* Follow local object identities without equating shallow copies with their
* sources. Write lookup retains every visible initializer: reassigning an alias
* later cannot erase a possible earlier mutation. Intrinsic reads stay strict.
*/
const createAmbientPathResolver = (
state: JavaScriptSemanticAnalysisState,
possibleWrites: boolean,
) => {
const cached = new WeakMap<t.Node, readonly AmbientPath[]>();
return (node: t.Node, capture?: t.Node): readonly AmbientPath[] => {
const prior = possibleWrites ? undefined : cached.get(node);
if (prior !== undefined) return prior;
const result = new Map<string, AmbientPath>();
// The caller's write site selects initializers for the written target.
// Reads reached through an initializer occur at their own position.
const pending: {
node: t.Node;
suffix: AmbientProjection;
readsAtSelf?: true;
}[] = [{ node, suffix: [] }];
// Join projections at their producing expression, rather than enumerating
// alias histories. Each key can only widen to a wildcard, and conflicting
// depths widen to unknown, so diamonds and cycles have finite state.
const expanded = new WeakMap<t.Node, AmbientProjection>();
const retain = (path: AmbientPath) => {
let canonical = canonicalAmbientPath(path);
for (;;) {
result.set(JSON.stringify(canonical), canonical);
// An unknown key may select a global self reference as well as a
// distinct property. Retain both possible object identities for writes.
if (
!possibleWrites ||
canonical[0] !== "global" ||
canonical[1] !== null
)
break;
canonical = canonicalAmbientPath(["global", ...canonical.slice(2)]);
}
};
while (pending.length > 0) {
const current = pending.pop();
if (current === undefined) break;
const { root, members } = electronMemberPath(current.node);
let suffix: AmbientProjection =
current.suffix === undefined
? undefined
: [...members, ...current.suffix];
if (expanded.has(root)) {
const previous = expanded.get(root);
if (previous === undefined) continue;
if (suffix !== undefined && previous.length === suffix.length) {
const joined: AmbientPath = previous.map((key, index) =>
key === suffix?.[index] ? key : null,
);
if (joined.every((key, index) => key === previous[index])) continue;
suffix = joined;
} else suffix = undefined;
if (!possibleWrites) {
cached.set(node, []);
return [];
}
}
expanded.set(root, suffix);
// A widened projection is a possible write, never proof of an intrinsic.
if (suffix === undefined && !possibleWrites) continue;
const continueWith = (node: t.Node, path: AmbientProjection = suffix) =>
pending.push({
node,
suffix: path,
...(current.readsAtSelf && { readsAtSelf: true }),
});
if (
t.isMetaProperty(root) &&
root.meta.name === "import" &&
root.property.name === "meta"
) {
retain(["import-meta", ...(suffix ?? [])]);
continue;
}
if (t.isIdentifier(root)) {
if (globalObjectName(root, state) !== undefined) {
retain(["global", ...(suffix ?? [])]);
continue;
}
if (isUnshadowedGlobal(root, state, root.name)) {
retain(["global", root.name, ...(suffix ?? [])]);
continue;
}
const binding = resolveSemanticBindingState(state, root, root.name);
if (binding === undefined) continue;
const stable = semanticStableInitializer(binding, state);
const readAt = current.readsAtSelf ? root : capture;
const initializers = possibleWrites
? readAt === undefined
? binding.initializers
: semanticMutationInitializers(binding, readAt, state.parentsByNode)
.initializers
: stable === undefined
? []
: [stable];
for (const initializer of initializers)
pending.push({
node: initializer.node,
readsAtSelf: true,
suffix:
suffix === undefined
? undefined
: [
...initializer.projection.map((key) =>
key === null ? null : String(key),
),
...suffix,
],
});
continue;
}
if (possibleWrites && t.isConditionalExpression(root)) {
continueWith(root.consequent);
continueWith(root.alternate);
continue;
}
if (possibleWrites && t.isLogicalExpression(root)) {
continueWith(root.left);
continueWith(root.right);
continue;
}
const key = suffix?.[0];
if (suffix !== undefined && key === undefined) continue;
if (t.isObjectExpression(root)) {
if (possibleWrites) {
for (const property of root.properties) {
if (t.isSpreadElement(property)) continueWith(property.argument);
else if (
t.isObjectProperty(property) &&
t.isExpression(property.value)
) {
const ownKey = semanticStaticPropertyKey(
property.key,
property.computed,
);
if (
suffix === undefined ||
key === null ||
ownKey === null ||
ownKey === key
)
continueWith(property.value, suffix?.slice(1));
}
}
} else if (
suffix !== undefined &&
key !== null &&
root.properties.every(
(property) =>
t.isObjectProperty(property) &&
semanticStaticPropertyKey(property.key, property.computed) !==
null,
)
) {
const property = root.properties.findLast(
(property) =>
t.isObjectProperty(property) &&
semanticStaticPropertyKey(property.key, property.computed) ===
key,
);
if (t.isObjectProperty(property) && t.isExpression(property.value))
continueWith(property.value, suffix.slice(1));
}
} else if (t.isArrayExpression(root)) {
if (suffix === undefined) {
for (const element of root.elements)
if (t.isSpreadElement(element)) continueWith(element.argument);
else if (t.isExpression(element)) continueWith(element);
} else if (!root.elements.some(t.isSpreadElement)) {
const index = Number(key);
if (key === null && possibleWrites) {
for (const element of root.elements)
if (t.isExpression(element))
continueWith(element, suffix.slice(1));
} else if (
key !== null &&
Number.isInteger(index) &&
index >= 0 &&
String(index) === key
) {
const element = root.elements[index];
if (t.isExpression(element)) continueWith(element, suffix.slice(1));
}
}
}
}
const paths = [...result.values()];
if (!possibleWrites) cached.set(node, paths);
return paths;
};
};
/**
* Index source-local writes through ambient identities and stable aliases.
* The shared invocation owner resolves intrinsic aliases, bind/call/apply and
* literal argument spreads. Runtime reachability and arbitrary call effects
* remain unevaluated; a visible possible write is sufficient.
*/
export function* indexJavaScriptAmbientWritesSteps(
program: t.Program,
state: JavaScriptSemanticAnalysisState,
): Generator<void> {
const ambientReads = createAmbientPathResolver(state, false);
const ambientWrites = createAmbientPathResolver(state, true);
const interop = createSemanticModuleInterop(state, {
intrinsicPath: (node) => {
const [path] = ambientReads(node);
return path?.[0] === "global" && path.every((key) => key !== null)
? ["intrinsic", ...(path.slice(1) as readonly string[])]
: undefined;
},
});
yield* collectJavaScriptIdentityWritesSteps(
program,
(root, kind, writeSite) => {
if (kind === "opaque-call") return [];
// Reassigning a local alias changes the binding, not the referenced object.
if (kind === "binding")
return t.isIdentifier(root) &&
isUnshadowedGlobal(root, state, root.name)
? [["global", root.name]]
: [];
return ambientWrites(root, writeSite);
},
interop.intrinsicInvocation,
interop.staticString,
interop.isFreshObject,
(node, path) => {
state.ambientWrites.retain(path);
state.ambientWriteEffects.push({ path, node });
},
);
}
/**
* Recognize an ambient name or direct global-object property without a visible
* source-local overwrite. This is a syntax-local identity fact, not proof that
* the runtime's globals have never been patched by other code.
*/
export const isUnmodifiedGlobalReference = (
callee: t.Node,
state: JavaScriptSemanticAnalysisState,
name: string,
): boolean => {
const node = unwrapJavaScriptExpression(callee).node;
if (state.ambientWrites.covers(["global", name])) return false;
if (isUnshadowedGlobal(node, state, name)) return true;
if (
!t.isMemberExpression(node) ||
semanticStaticPropertyKey(node.property, node.computed) !== name
)
return false;
const root = unwrapJavaScriptExpression(node.object).node;
const namespace = globalObjectName(root, state);
return (
namespace !== undefined &&
!state.ambientWrites.covers(["global", namespace])
);
};
/** A module URL base is known only while its property has no visible write. */
export const isUnmodifiedImportMetaUrl = (
node: t.Node | undefined,
state: JavaScriptSemanticAnalysisState,
): boolean =>
isImportMetaUrl(node) && !state.ambientWrites.covers(["import-meta", "url"]);