mirror of
https://github.com/morluto/rea.git
synced 2026-11-03 05:16:16 +00:00
fix(javascript): resolve module-relative Worker URL references (#2104)
* fix(javascript): resolve module-relative Worker URL references * fix(javascript): validate Worker URL paths independently of suffixes * fix(javascript): preserve directory-form Worker URL paths * fix(javascript): unwrap type assertions in Worker URL references * fix(javascript): validate Worker bindings before URL resolution * fix(javascript): leave drive-letter Worker URLs unresolved Use WHATWG file URL parsing before pathname decoding to detect drive-letter forms whose resource identity depends on the unmodeled source URL scheme. Preserve encoded and ordinary relative pipe paths, and cover false matches through the compiled CLI and resolver boundary tests. * fix(javascript): preserve Worker URL identity boundaries Recognize known Worker bindings before filtering callee spellings so ESM aliases and transparent constructor wrappers reach URL analysis. Keep root-relative module URLs unknown without a modeled runtime base root. Index direct source-local global writes with the shared mutation collector before promoting URL and browser Worker constructors. Preserve lexical shadowing and unrelated property identities, and exercise all three review regressions through the compiled CLI. * fix(javascript): invalidate mutated import.meta URL bases Track source-local import.meta writes alongside ambient global writes so Worker URL expressions keep an unknown base after a possible URL mutation. Tag the identity roots independently and cover direct mutation and unrelated property cases through the compiled CLI. * fix(javascript): close Worker URL identity and pathname gaps Resolve ambient object identities before appending written properties, and retain possible source-local aliases across rebinding. Reuse intrinsic invocation handling for aliased and bound mutators, array spreads, and transparent TypeScript wrappers while preserving Electron write semantics. Keep malformed-percent nested Worker URL pathnames unknown because the base scheme is unavailable and Node rejects these file URLs. Preserve tolerant HTML URL handling, valid escapes, and query/fragment evidence. Cover the new cases and unrelated writes through the compiled CLI with existing-file decoys. All 591 targeted tests and npm run check pass. * fix(javascript): bound alias expansion and classify Worker paths Join ambient identity projections instead of enumerating alias histories. Memoize repeated value and provenance queries while preserving capture, depth, and cycle-dependent uncertainty. Keep Node Worker string arguments unresolved without filesystem or cwd context, and admit qualified global URL constructors through identity checks. Validate three 40-level alias branch shapes through the compiled CLI, 925 source tests, 25 CLI/boundary tests, and the full npm run check gate. * fix(server): preserve partial Evidence target-content provenance Pass the exact named contract returnsTargetContent flag through the shared partial-Evidence error delivery path introduced by the shortlist integration. Exercise successful and failed recording through in-memory MCP for marked and unmarked contracts, preserving source payload and original failures. Observe both shared-handler emission and wire delivery so the SDK error-marker fallback cannot hide omissions. Suppressing the flag fails exactly the two marked cases. Validation: check:fast, check, 736 semantic tests, 29 MCP transport/error tests and six compiled static Worker CLI journeys pass. No live target/provider execution or hosted publication. * fix(javascript): structurally share semantic dependency proofs --------- Co-authored-by: baron <richstyles@gmail.com>
This commit is contained in:
1 parent
3b0ea590fc
commit
653067d799
19 files changed
+1946
-171
No files matched your search
@@ -0,0 +1,301 @@
|
||||
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";
|
||||
|
||||
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): readonly AmbientPath[] => {
|
||||
const prior = cached.get(node);
|
||||
if (prior !== undefined) return prior;
|
||||
const result = new Map<string, AmbientPath>();
|
||||
const pending: { node: t.Node; suffix: AmbientProjection }[] = [
|
||||
{ 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 });
|
||||
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 initializers = possibleWrites
|
||||
? binding.initializers
|
||||
: stable === undefined
|
||||
? []
|
||||
: [stable];
|
||||
for (const initializer of initializers)
|
||||
pending.push({
|
||||
node: initializer.node,
|
||||
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()];
|
||||
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) => {
|
||||
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);
|
||||
},
|
||||
interop.intrinsicInvocation,
|
||||
interop.staticString,
|
||||
interop.isFreshObject,
|
||||
(_node, path) => {
|
||||
state.ambientWrites.retain(path);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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"]);
|
||||
Reference in new issue
Block a user