mirror of
https://github.com/morluto/rea.git
synced 2026-10-29 10:55:57 +00:00
Check the complete navigator.serviceWorker.register identity through the shared ambient-write owner, including replaced methods and aliased receiver mutations. Reuse the module URL reader for intact registrations while preserving unknown document-relative strings. Cover the unresolved review case and the CLI graph. Refs #3841 Co-authored-by: Chandan Chaudhary <chandankkumar156@gmail.com> Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
324 lines
12 KiB
TypeScript
324 lines
12 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)) {
|
|
// The wrapper module object is distinct from globalThis.module. Keep
|
|
// its identity through the same alias/projection owner used for writes.
|
|
if (isUnshadowedGlobal(root, state, "module")) {
|
|
retain(["commonjs-module", ...(suffix ?? [])]);
|
|
continue;
|
|
}
|
|
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 property path, optionally through a global object, 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 | readonly [string, ...string[]],
|
|
): boolean => {
|
|
const path: readonly [string, ...string[]] =
|
|
typeof name === "string" ? [name] : name;
|
|
if (state.ambientWrites.covers(["global", ...path])) return false;
|
|
const { root, members } = electronMemberPath(callee);
|
|
const matches = (expected: readonly string[]) =>
|
|
members.length === expected.length &&
|
|
members.every((key, index) => key === expected[index]);
|
|
if (isUnshadowedGlobal(root, state, path[0]) && matches(path.slice(1)))
|
|
return true;
|
|
const namespace = globalObjectName(root, state);
|
|
return (
|
|
namespace !== undefined &&
|
|
matches(path) &&
|
|
!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"]);
|