diff --git a/src/domain/javascript/javascriptSemanticAnalysis.member_mutations.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.member_mutations.test.ts index 3450060cbe..bf7d35c47f 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.member_mutations.test.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.member_mutations.test.ts @@ -611,3 +611,41 @@ describe("property mutation collection on minified bundles", () => { }); }, 30000); }); + +describe("results of methods on an escaped receiver", () => { + it("makes every method's distinct result uncertain, not only the called one", () => { + expect( + resultValue( + 'const first = { t: "A" }; const second = { t: "B" }; const kept = { t: "C" }; const box = { a() { return first; }, b() { return second; }, c() { return first; } }; box.a(); return [first.t, second.t, kept.t];', + ), + ).toMatchObject({ + status: "array", + items: [ + { value: { status: "unknown" } }, + { value: { status: "unknown" } }, + { value: { status: "literal", value: "C" } }, + ], + }); + }); + + it("expands a receiver's methods once rather than once per call (#1495)", () => { + const count = 2000; + const methods = Array.from( + { length: count }, + (_, index) => `m${index}() { return shared; },`, + ).join(" "); + const calls = Array.from( + { length: count }, + (_, index) => `box.m${index}().p${index} = ${index};`, + ).join(" "); + const start = performance.now(); + const value = resultValue( + `const shared = { token: "TOKEN" }; const box = { ${methods} }; ${calls} return [shared.token];`, + ); + expect(performance.now() - start).toBeLessThan(2000); + expect(value).toMatchObject({ + status: "array", + items: [{ value: { status: "unknown" } }], + }); + }, 30000); +}); diff --git a/src/domain/javascript/javascriptSemanticMemberMutations.ts b/src/domain/javascript/javascriptSemanticMemberMutations.ts index 4c0dfc91d1..352e1c5d75 100644 --- a/src/domain/javascript/javascriptSemanticMemberMutations.ts +++ b/src/domain/javascript/javascriptSemanticMemberMutations.ts @@ -251,19 +251,9 @@ export const collectSemanticMemberMutations = ( originAt: current.originAt, }); } else { - if ( - t.isArrayExpression(expression) && - ((current.path[0] === "__proto__" && - (effect === "escape" || current.path.length > 1)) || - (current.path[0] === "constructor" && - current.path[1] === "prototype")) - ) - arrayIterationUnknown = true; - for (const value of referencedValues( - expression, - current.path, - effect, - )) { + for (const value of effect === "escape" + ? escapedReferenceLeaves(expression, current.path) + : expandedValues(expression, current.path, effect)) { if (value.iterableFallbackPath !== undefined) deferIterable({ node: value.node, @@ -290,6 +280,77 @@ export const collectSemanticMemberMutations = ( mutation: t.Node, path: PropertyPath = [], ): void => markValue(node, path, new Set(), "escape", mutation); + function expandedValues( + expression: t.Node, + path: PropertyPath, + effect: ValueEffect, + ): readonly ReferencedValue[] { + if ( + t.isArrayExpression(expression) && + ((path[0] === "__proto__" && (effect === "escape" || path.length > 1)) || + (path[0] === "constructor" && path[1] === "prototype")) + ) + arrayIterationUnknown = true; + return referencedValues(expression, path, effect); + } + // Expanding a value below its references depends only on syntax, not on the + // escape site. Escaping an object reaches every method's results, so + // re-expanding it at each call through that object made N calls over N + // methods quadratic. Identifiers that resolve to the same binding and path + // are processed identically, so one stands for all of them. + const escapedLeaves = new WeakMap< + t.Node, + Map + >(); + function escapedReferenceLeaves( + root: t.Node, + rootPath: PropertyPath, + ): readonly ReferencedValue[] { + // Parser recovery can leave an argument hole, which expands to nothing. + if (!t.isNode(root)) return []; + const rootKey = JSON.stringify(rootPath); + const cached = escapedLeaves.get(root)?.get(rootKey); + if (cached !== undefined) return cached; + const leaves: ReferencedValue[] = []; + const leafIdentities = new Set(); + const expanded = new WeakMap>(); + const pending = [...expandedValues(root, rootPath, "escape")]; + for (let cursor = 0; cursor < pending.length; cursor++) { + const value = pending[cursor]; + if (value === undefined) break; + const node = unwrapJavaScriptExpression(value.node).node; + if (!t.isNode(node)) continue; + const pathKey = JSON.stringify(value.path); + if (t.isIdentifier(node) && value.iterableFallbackPath === undefined) { + const binding = resolveSemanticBindingState(state, node, node.name); + const identity = JSON.stringify([ + binding === undefined ? null : binding.bindingId, + binding === undefined ? node.name : null, + pathKey, + ]); + if (leafIdentities.has(identity)) continue; + leafIdentities.add(identity); + leaves.push(value); + } else if ( + value.iterableFallbackPath !== undefined || + t.isIdentifier(node) || + t.isMemberExpression(node) || + t.isOptionalMemberExpression(node) + ) + leaves.push(value); + else { + const paths = expanded.get(node) ?? new Set(); + if (paths.has(pathKey)) continue; + paths.add(pathKey); + expanded.set(node, paths); + pending.push(...expandedValues(node, value.path, "escape")); + } + } + const byPath = escapedLeaves.get(root) ?? new Map(); + byPath.set(rootKey, leaves); + escapedLeaves.set(root, byPath); + return leaves; + } const markReceiver = (callee: t.Node, mutation: t.Node): void => { const expression = unwrapJavaScriptExpression(callee).node; if (