From 2496256cb7e3fcd83737cf3f0446dee408ccf609 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 07:42:13 +0800 Subject: [PATCH] fix(javascript): require complete receiver ownership proofs --- .../javascriptSemanticAmbientWrites.ts | 675 ++++++++++-------- .../javascript/javascriptSemanticAnalysis.ts | 2 +- .../javascriptSemanticMemberMutations.ts | 4 +- .../javascriptSemanticOpaqueEffects.ts | 2 +- ...ascriptStaticAnalysis.mapEndpoints.test.ts | 11 + 5 files changed, 383 insertions(+), 311 deletions(-) diff --git a/src/domain/javascript/javascriptSemanticAmbientWrites.ts b/src/domain/javascript/javascriptSemanticAmbientWrites.ts index 57f53e8509..1ccce96da6 100644 --- a/src/domain/javascript/javascriptSemanticAmbientWrites.ts +++ b/src/domain/javascript/javascriptSemanticAmbientWrites.ts @@ -90,6 +90,18 @@ const joinAmbientProjection = ( ? undefined : left.map((key, index) => (key === right[index] ? key : null)); +type AmbientIdentityUnknownReason = + | "unresolved-value" + | "widened-projection" + | "unresolved-call" + | "mixed-identities"; + +interface AmbientResolution { + readonly paths: readonly AmbientPath[]; + readonly ownership: "local" | "ambient" | "unknown"; + readonly unknownReasons: readonly AmbientIdentityUnknownReason[]; +} + interface AmbientReferenceFacts { readonly stores: WeakMap< t.Node, @@ -109,7 +121,7 @@ export const createAmbientPathResolver = ( possibleWrites: boolean, facts?: AmbientReferenceFacts, ) => { - const cached = new WeakMap(); + const cached = new WeakMap(); const returnedReferences = createSemanticReturnedReferences(state); return ( node: t.Node, @@ -117,10 +129,12 @@ export const createAmbientPathResolver = ( escapedContents = false, projection: AmbientProjection = [], localIdentity?: (node: t.Node, path: AmbientProjection) => void, - ): readonly AmbientPath[] => { + ): AmbientResolution => { const prior = possibleWrites ? undefined : cached.get(node); if (prior !== undefined) return prior; const result = new Map(); + let hasLocalIdentity = false; + const unknownReasons = new Set(); // The caller's write site selects initializers for the written target. // Reads reached through an initializer occur at their own position. const pending: { @@ -159,81 +173,61 @@ export const createAmbientPathResolver = ( 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 }), - }); - const continueReturned = (callee: t.Node, path: AmbientProjection) => { - for (const source of returnedReferences(callee).references) - continueWith( - source.node, - path === undefined - ? undefined - : [ - ...source.projection.map((key) => - key === null || typeof key === "object" - ? null - : String(key), - ), - ...path, - ], - ); - }; - if (!t.isIdentifier(root) && facts?.freshObject(root)) { - localIdentity?.(root, suffix); - for (const [source, path] of facts?.stores.get(root) ?? []) { - const selected = - suffix === undefined || - path === undefined || - path.every( - (key, index) => - index >= suffix.length || - key === null || - suffix[index] === null || - key === suffix[index], - ); - if ( - !selected || - (suffix !== undefined && - path !== undefined && - suffix.length < path.length && - !escapedContents) - ) + const queuedBefore = pending.length; + const pathsBefore = result.size; + let resolvedIdentity = false; + try { + 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) { + resolvedIdentity = true; continue; - const remaining = - suffix === undefined || path === undefined - ? undefined - : suffix.slice(path.length); + } + 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])) { + resolvedIdentity = true; + continue; + } + suffix = joined; + } else suffix = undefined; + if (!possibleWrites) { + const unresolved: AmbientResolution = { + paths: [], + ownership: "unknown", + unknownReasons: ["widened-projection"], + }; + cached.set(node, unresolved); + return unresolved; + } + } + expanded.set(root, suffix); + if (suffix === undefined || suffix.includes(null)) + unknownReasons.add("widened-projection"); + // 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: source.node, - readsAtSelf: true, - suffix: - remaining === undefined + node, + suffix: path, + ...(current.readsAtSelf && { readsAtSelf: true }), + }); + const continueReturned = (callee: t.Node, path: AmbientProjection) => { + const returned = returnedReferences(callee); + // Returned references retain possible source-local values, not a + // completeness proof for every runtime callable alternative. + unknownReasons.add("unresolved-call"); + for (const source of returned.references) + continueWith( + source.node, + path === undefined ? undefined : [ ...source.projection.map((key) => @@ -241,235 +235,311 @@ export const createAmbientPathResolver = ( ? null : String(key), ), - ...remaining, + ...path, ], - }); - } - } - 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 = possibleWrites - ? undefined - : semanticStableInitializer(binding, state); - const readAt = current.readsAtSelf ? root : capture; - const selected = - !possibleWrites || readAt === undefined - ? binding - : semanticBindingAtRead(binding, readAt, state.parentsByNode) - .binding; - const initializers = possibleWrites - ? [...selected.initializers, ...selected.referenceInitializers] - : stable === undefined - ? [] - : [stable]; - for (const initializer of initializers) { - if (semanticReferenceSourcesInactive(initializer, state)) continue; - // Pattern assignments share the source selection owner with member - // effects: copying a container preserves its children, not itself. - const path = semanticCopiedReferencePath( - initializer, - suffix ?? [null], - escapedContents ? "escape" : "identity", - ); - if (path === null) continue; - pending.push({ - node: initializer.node, - readsAtSelf: true, - suffix: - suffix === undefined || - semanticReferenceIterationUnknown(initializer, state) - ? undefined - : path.map((key) => - key === null || typeof key === "object" - ? null - : String(key), - ), - }); - } - continue; - } - if ( - possibleWrites && - t.isNewExpression(root) && - (suffix?.[0] === "__proto__" || suffix?.[0] === "constructor") - ) { - continueWith( - root.callee, - suffix[0] === "__proto__" - ? ["prototype", ...suffix.slice(1)] - : suffix.slice(1), - ); - continue; - } - const prototypeTarget = possibleWrites - ? facts?.prototypeTarget(root) - : undefined; - if (prototypeTarget !== undefined) { - continueWith( - prototypeTarget, - suffix === undefined ? undefined : ["__proto__", ...suffix], - ); - continue; - } - if ( - possibleWrites && - (t.isCallExpression(root) || - t.isOptionalCallExpression(root) || - t.isNewExpression(root) || - t.isTaggedTemplateExpression(root) || - (escapedContents && (t.isFunction(root) || t.isClass(root)))) - ) { - continueReturned( - t.isFunction(root) || t.isClass(root) - ? root - : t.isTaggedTemplateExpression(root) - ? root.tag - : root.callee, - 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; - } - if (possibleWrites && t.isSequenceExpression(root)) { - const value = root.expressions.at(-1); - if (value !== undefined) continueWith(value); - continue; - } - if ( - possibleWrites && - t.isAssignmentExpression(root) && - root.operator === "=" - ) { - continueWith(root.right); - continue; - } - const key = suffix?.[0]; - // Container escapes reuse the finite worklist, including alias cycles. - if (escapedContents && suffix?.length === 0) { - if (t.isObjectExpression(root)) { - for (const property of root.properties) - if (t.isSpreadElement(property)) - // A fresh copy exposes selected children, not its source container. - continueWith(property.argument, [null]); - else if ( - t.isObjectProperty(property) && - t.isExpression(property.value) - ) - continueWith(property.value, []); - else if (t.isObjectMethod(property) && property.kind === "get") - continueReturned(property, []); - continue; - } - if (t.isArrayExpression(root)) { - for (const element of root.elements) - if (t.isSpreadElement(element)) continueWith(element.argument, []); - else if (t.isExpression(element)) continueWith(element, []); - continue; - } - } - 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)) || - (t.isObjectMethod(property) && property.kind === "get") - ) { - const ownKey = semanticStaticPropertyKey( - property.key, - property.computed, - ); - if ( - suffix === undefined || - key === null || - ownKey === null || - ownKey === key - ) - if (t.isObjectMethod(property)) - continueReturned(property, suffix?.slice(1)); - else if (t.isExpression(property.value)) - continueWith(property.value, suffix?.slice(1)); - } + ); + }; + if (!t.isIdentifier(root) && facts?.freshObject(root)) { + localIdentity?.(root, suffix); + // The allocation owns itself; a projected child needs its own proof. + if (suffix?.length === 0) { + hasLocalIdentity = true; + resolvedIdentity = true; + } + for (const [source, path] of facts?.stores.get(root) ?? []) { + const selected = + suffix === undefined || + path === undefined || + path.every( + (key, index) => + index >= suffix.length || + key === null || + suffix[index] === null || + key === suffix[index], + ); + if ( + !selected || + (suffix !== undefined && + path !== undefined && + suffix.length < path.length && + !escapedContents) + ) + continue; + const remaining = + suffix === undefined || path === undefined + ? undefined + : suffix.slice(path.length); + pending.push({ + node: source.node, + readsAtSelf: true, + suffix: + remaining === undefined + ? undefined + : [ + ...source.projection.map((key) => + key === null || typeof key === "object" + ? null + : String(key), + ), + ...remaining, + ], + }); } - } 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) { + if ( + t.isMetaProperty(root) && + root.meta.name === "import" && + root.property.name === "meta" + ) { + retain(["import-meta", ...(suffix ?? [])]); + resolvedIdentity = true; + 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 ?? [])]); + resolvedIdentity = true; + continue; + } + if (globalObjectName(root, state) !== undefined) { + retain(["global", ...(suffix ?? [])]); + resolvedIdentity = true; + continue; + } + if (isUnshadowedGlobal(root, state, root.name)) { + retain(["global", root.name, ...(suffix ?? [])]); + resolvedIdentity = true; + continue; + } + const binding = resolveSemanticBindingState(state, root, root.name); + if (binding === undefined) continue; + const stable = possibleWrites + ? undefined + : semanticStableInitializer(binding, state); + const readAt = current.readsAtSelf ? root : capture; + const atRead = + possibleWrites && readAt !== undefined + ? semanticBindingAtRead(binding, readAt, state.parentsByNode) + : undefined; + const selected = atRead?.binding ?? binding; + if (possibleWrites && atRead?.hasDefiniteInitializer === false) + unknownReasons.add("unresolved-value"); + const initializers = possibleWrites + ? [...selected.initializers, ...selected.referenceInitializers] + : stable === undefined + ? [] + : [stable]; + for (const initializer of initializers) { + if (semanticReferenceSourcesInactive(initializer, state)) continue; + // Pattern assignments share the source selection owner with member + // effects: copying a container preserves its children, not itself. + const path = semanticCopiedReferencePath( + initializer, + suffix ?? [null], + escapedContents ? "escape" : "identity", + ); + if (path === null) continue; + pending.push({ + node: initializer.node, + readsAtSelf: true, + suffix: + suffix === undefined || + semanticReferenceIterationUnknown(initializer, state) + ? undefined + : path.map((key) => + key === null || typeof key === "object" + ? null + : String(key), + ), + }); + } + continue; + } + if ( + possibleWrites && + t.isNewExpression(root) && + (suffix?.[0] === "__proto__" || suffix?.[0] === "constructor") + ) { + continueWith( + root.callee, + suffix[0] === "__proto__" + ? ["prototype", ...suffix.slice(1)] + : suffix.slice(1), + ); + continue; + } + const prototypeTarget = possibleWrites + ? facts?.prototypeTarget(root) + : undefined; + if (prototypeTarget !== undefined) { + continueWith( + prototypeTarget, + suffix === undefined ? undefined : ["__proto__", ...suffix], + ); + continue; + } + if ( + possibleWrites && + (t.isCallExpression(root) || + t.isOptionalCallExpression(root) || + t.isNewExpression(root) || + t.isTaggedTemplateExpression(root) || + (escapedContents && (t.isFunction(root) || t.isClass(root)))) + ) { + continueReturned( + t.isFunction(root) || t.isClass(root) + ? root + : t.isTaggedTemplateExpression(root) + ? root.tag + : root.callee, + 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; + } + if (possibleWrites && t.isSequenceExpression(root)) { + const value = root.expressions.at(-1); + if (value !== undefined) continueWith(value); + continue; + } + if ( + possibleWrites && + t.isAssignmentExpression(root) && + root.operator === "=" + ) { + continueWith(root.right); + continue; + } + const key = suffix?.[0]; + // Container escapes reuse the finite worklist, including alias cycles. + if (escapedContents && suffix?.length === 0) { + if (t.isObjectExpression(root)) { + for (const property of root.properties) + if (t.isSpreadElement(property)) + // A fresh copy exposes selected children, not its source container. + continueWith(property.argument, [null]); + else if ( + t.isObjectProperty(property) && + t.isExpression(property.value) + ) + continueWith(property.value, []); + else if (t.isObjectMethod(property) && property.kind === "get") + continueReturned(property, []); + continue; + } + if (t.isArrayExpression(root)) { for (const element of root.elements) + if (t.isSpreadElement(element)) + continueWith(element.argument, []); + else if (t.isExpression(element)) continueWith(element, []); + continue; + } + } + 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)) || + (t.isObjectMethod(property) && property.kind === "get") + ) { + const ownKey = semanticStaticPropertyKey( + property.key, + property.computed, + ); + if ( + suffix === undefined || + key === null || + ownKey === null || + ownKey === key + ) + if (t.isObjectMethod(property)) + continueReturned(property, suffix?.slice(1)); + else if (t.isExpression(property.value)) + 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)); - } 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)); + } } } + } finally { + if ( + !resolvedIdentity && + pending.length === queuedBefore && + result.size === pathsBefore + ) + unknownReasons.add("unresolved-value"); } } const paths = [...result.values()]; - if (!possibleWrites) cached.set(node, paths); - return paths; + if (paths.length > 0 && hasLocalIdentity) + unknownReasons.add("mixed-identities"); + const resolution: AmbientResolution = { + paths, + ownership: + unknownReasons.size > 0 + ? "unknown" + : paths.length > 0 + ? "ambient" + : hasLocalIdentity + ? "local" + : "unknown", + unknownReasons: [...unknownReasons], + }; + if (!possibleWrites) cached.set(node, resolution); + return resolution; }; }; @@ -503,7 +573,7 @@ export function* indexJavaScriptAmbientWritesSteps( >(); const interop = createSemanticModuleInterop(state, { intrinsicPath: (node) => { - const [path] = ambientReads(node); + const [path] = ambientReads(node).paths; return path?.[0] === "global" && path.every((key) => key !== null) ? ["intrinsic", ...(path.slice(1) as readonly string[])] : undefined; @@ -640,17 +710,8 @@ export function* indexJavaScriptAmbientWritesSteps( } for (const source of storedReferences) { if (source.target === undefined) continue; - let local = false; - const ambient = ambientWrites( - source.target.object, - source.mutation, - false, - [], - () => { - local = true; - }, - ); - if (!local || ambient.length > 0) { + const receiver = ambientWrites(source.target.object, source.mutation); + if (receiver.ownership !== "local") { state.escapingReferenceStores.add(source.target); escapes.push({ node: source.mutation, @@ -668,7 +729,7 @@ export function* indexJavaScriptAmbientWritesSteps( const paths = target === undefined ? [path] - : ambientWrites(target.node, target.capture).map((identity) => [ + : ambientWrites(target.node, target.capture).paths.map((identity) => [ ...identity, ...path.slice(2), ]); @@ -677,7 +738,7 @@ export function* indexJavaScriptAmbientWritesSteps( } } for (const { node, target, browserReceiver, projection } of escapes) - for (const path of ambientWrites(target, node, true, projection)) + for (const path of ambientWrites(target, node, true, projection).paths) // Only proven browser APIs keep their global receiver from user code. if (!(browserReceiver && path.length === 1 && path[0] === "global")) { retainSemanticAmbientEffect(state, "escape", path, node); diff --git a/src/domain/javascript/javascriptSemanticAnalysis.ts b/src/domain/javascript/javascriptSemanticAnalysis.ts index ebbff87aa0..e629ec3bb9 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.ts @@ -283,7 +283,7 @@ const visitSemanticReferenceEffectsAtSite = ( } else if (lookup.kind === "ambient" || lookup.kind === "uncertain-ambient") { for (const node of state.ambientBindingWrites.get(root.name) ?? []) if (!retain("ambient-write", node)) return metadata; - const ambientRoots = createAmbientPathResolver(state, false)(root); + const ambientRoots = createAmbientPathResolver(state, false)(root).paths; for (const ambientRoot of ambientRoots) { const selectedPath = [...ambientRoot, ...path]; for (const [kind, effects] of [ diff --git a/src/domain/javascript/javascriptSemanticMemberMutations.ts b/src/domain/javascript/javascriptSemanticMemberMutations.ts index 27ab0be6cc..0877d3e6c4 100644 --- a/src/domain/javascript/javascriptSemanticMemberMutations.ts +++ b/src/domain/javascript/javascriptSemanticMemberMutations.ts @@ -688,7 +688,7 @@ export function* collectSemanticMemberMutationsSteps( current.path.map((key) => key === null || typeof key === "object" ? null : String(key), ), - )) + ).paths) if (retainSemanticAmbientEffect(state, effect, path, mutation)) { recordedEffectCount += 1; clearSemanticValueCaches(state); @@ -1810,7 +1810,7 @@ const observableMutationPaths = ( ["globalThis", "global", "window", "self"].includes(expression.name) ) (observer.origins ??= new Set()).add("ambient"); - else if (ambientPaths(expression).length > 0) + else if (ambientPaths(expression).paths.length > 0) (observer.origins ??= new Set()).add("opaque"); } else if ( t.isMemberExpression(expression) || diff --git a/src/domain/javascript/javascriptSemanticOpaqueEffects.ts b/src/domain/javascript/javascriptSemanticOpaqueEffects.ts index f602bc272a..9d91402419 100644 --- a/src/domain/javascript/javascriptSemanticOpaqueEffects.ts +++ b/src/domain/javascript/javascriptSemanticOpaqueEffects.ts @@ -63,7 +63,7 @@ export const createSemanticOpaqueGlobalObserver = ( let invocationSite: t.Node | undefined; const interop = createSemanticModuleInterop(state, { intrinsicPath: (node) => { - const paths = ambientPaths(node, invocationSite); + const paths = ambientPaths(node, invocationSite).paths; const path = paths.find((path) => path[1] === "eval") ?? paths[0]; if (path?.[0] !== "global" || path.some((key) => key === null)) return undefined; diff --git a/src/domain/javascript/javascriptStaticAnalysis.mapEndpoints.test.ts b/src/domain/javascript/javascriptStaticAnalysis.mapEndpoints.test.ts index fad6192813..4f55849da9 100644 --- a/src/domain/javascript/javascriptStaticAnalysis.mapEndpoints.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysis.mapEndpoints.test.ts @@ -37,6 +37,8 @@ it.each([ 'const box = {}; box.value = { name: "Alice" }; inspect(box); new Map().get("/accounts")', 'patch(() => ({ name: "Alice" })); new Map().get("/accounts")', 'const box = {}; box.prototype = Map.prototype; new Map().get("/accounts")', + 'function expose() { const holder = { child: {} }; holder.child.prototype = Map.prototype; } new Map().get("/accounts")', + 'const box = flag ? {} : {}; box.prototype = Map.prototype; new Map().get("/accounts")', 'const box = {}; const alias = box; alias.prototype = Map.prototype; inspect({}); new Map().get("/accounts")', 'const box = {}; box.make = () => Map.prototype; new Map().get("/accounts")', 'let box = {}; const first = box; box = {}; box.p = Map.prototype; patch(first); new Map().get("/accounts")', @@ -84,6 +86,15 @@ it.each([ 'const box = { set prototype(value) { patch(value); } }; box.prototype = Map.prototype; new Map().get("/accounts")', 'const box = new ExternalBox(); box.prototype = Map.prototype; new Map().get("/accounts")', 'function expose(box) { box.prototype = Map.prototype; } new Map().get("/accounts")', + 'function expose(box = {}) { box.prototype = Map.prototype; } new Map().get("/accounts")', + 'function expose(external, flag) { const box = flag ? {} : external; box.prototype = Map.prototype; } new Map().get("/accounts")', + 'function expose(external) { const holder = { child: external }; holder.child.prototype = Map.prototype; } new Map().get("/accounts")', + 'function expose(key) { const holder = { child: {} }; holder[key].prototype = Map.prototype; } new Map().get("/accounts")', + 'function expose(key) { const holder = [{}]; holder[key].prototype = Map.prototype; } new Map().get("/accounts")', + 'const box = flag ? {} : external(); box.prototype = Map.prototype; new Map().get("/accounts")', + 'function expose(external, flag) { const make = flag ? () => ({}) : external; const box = make(); box.prototype = Map.prototype; } new Map().get("/accounts")', + 'import external from "./external.mjs"; const box = flag ? {} : external; box.prototype = Map.prototype; new Map().get("/accounts")', + 'let box; if (flag) box = {}; box.prototype = Map.prototype; new Map().get("/accounts")', 'const factory = { make() { return Map.prototype; } }; factory.make().get = fetch; new Map().get("/accounts")', 'new Map().__proto__.get = fetch; new Map().get("/accounts")', 'new Map().constructor.prototype.get = fetch; new Map().get("/accounts")',