From a70d90eb4ca906f6c3b130f4dd7a9f855a07cbac Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Mon, 28 Sep 2026 17:54:46 -0700 Subject: [PATCH] javascript: new this.constructor() builds the receiver's class at each site A method returning `new this.constructor(...)`, directly or through a const it initialises, was read as one merged value: every instantiated subclass of the declaring class, so each call on a clone fanned to all their overrides. Like a `this`-return, it is now substituted per site from the call's receiver. A `let` local and `new this()` in a static keep their merged value. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> Co-Authored-By: Claude --- .../engine/resolution/value-flow.dl | 22 +++++++++- graph/javascript/souffle/decls_all.dl | 2 + .../69-this-constructor-per-site/src/main.js | 43 +++++++++++++++++++ .../src/package.json | 1 + .../expected/07-esm-class-hierarchy.edges | 3 +- .../expected/37-this-constructor.edges | 6 +-- .../69-this-constructor-per-site.diag | 2 + .../69-this-constructor-per-site.edges | 41 ++++++++++++++++++ .../69-this-constructor-per-site.oracle | 29 +++++++++++++ 9 files changed, 142 insertions(+), 7 deletions(-) create mode 100644 graph/test/javascript/cases/69-this-constructor-per-site/src/main.js create mode 100644 graph/test/javascript/cases/69-this-constructor-per-site/src/package.json create mode 100644 graph/test/javascript/expected/69-this-constructor-per-site.diag create mode 100644 graph/test/javascript/expected/69-this-constructor-per-site.edges create mode 100644 graph/test/javascript/expected/69-this-constructor-per-site.oracle diff --git a/graph/javascript/engine/resolution/value-flow.dl b/graph/javascript/engine/resolution/value-flow.dl index ab3619141..15379c699 100644 --- a/graph/javascript/engine/resolution/value-flow.dl +++ b/graph/javascript/engine/resolution/value-flow.dl @@ -370,7 +370,7 @@ installed_function(val, m) :- expr_kind(_, "ASSIGNMENT", _, val), expr_child(_, // parser attributes a nested function's body to the nested function, so no // filtering by nesting is needed. Plus `@returns {T}`. return_value(m, k, i) :- expr_root(_, "RETURN", e), expr_owner(_, m, _, e), !expr_kind(_, "THIS", _, e), - !wrapped_result_return(e), expr_value(e, k, i). + !wrapped_result_return(e), !returns_own_class_new(m, e), expr_value(e, k, i). // A wrapper closure's carrying return keeps every value that does NOT come from the // parameter's call: `fn() || fallback`, `override || (cached = make())`, a variable // that starts as a default (below). Not an `undefined` placeholder (`let result = @@ -523,6 +523,26 @@ expr_value(ce, k, i) :- expr_kind(_, "CALL", _, ce), expr_resolves_to_method(ce, expr_value(ce, k, i) :- expr_kind(_, "CALL", _, ce), expr_resolves_to_method(ce, m), method_returns_this(m), call_site(_, ck, _, _, _, _, ce, _, _), !call_kind_is_member_form(ck), this_value(m, k, i). +// `return new this.constructor(...)` is polymorphic the same way: `clone()` declared on +// Base and called on a Sub builds a Sub. Read merged, the construction held every +// instantiated subclass of Base and each call on the clone fanned to all their +// overrides. So it is not a value of the method either; each site builds an instance of +// its own receiver's class. (`new this()` in a static is not read this way: the compiler +// types it as the declaring class, and the oracle would score the receiver's as WRONG.) +// Returned directly, or through a `const` it initialises (`const c = new +// this.constructor(); c.x = …; return c`, the everyday clone), which nothing rebinds. +own_class_new(m, e) :- expr_kind(_, "NEW", _, e), call_site(_, "CONSTRUCTOR_CALL", "constructor", "SYNTACTIC", _, _, e, _, _), + expr_child(_, e, "RECEIVER", _, r), expr_kind(_, "THIS", _, r), expr_owner(_, m, _, r). +returns_own_class_new(m, e) :- expr_root(_, "RETURN", e), expr_owner(_, m, _, e), own_class_new(m, e). +returns_own_class_new(m, e) :- expr_root(_, "RETURN", e), expr_owner(_, m, _, e), expr_kind(_, "IDENTIFIER", _, e), + expr_binding(_, v, e), var_decl(_, _, "CONST_BLOCK_TDZ", _, _, _, v), var_binding_form(_, "IDENTIFIER", v), + var_init(_, _, n, v), own_class_new(m, n). +expr_value(ce, "inst", t) :- expr_kind(_, "CALL", _, ce), expr_resolves_to_method(ce, m), returns_own_class_new(m, _), + call_site(_, ck, _, "SYNTACTIC", _, _, ce, _, _), call_kind_is_member_form(ck), + expr_child(_, ce, "RECEIVER", _, r), expr_value(r, "inst", t). +expr_value(ce, "inst", t) :- expr_kind(_, "CALL", _, ce), expr_resolves_to_method(ce, m), returns_own_class_new(m, _), + call_site(_, ck, _, _, _, _, ce, _, _), !call_kind_is_member_form(ck), + this_value(m, "inst", t). // ── prop_value(K, I, Name, K2, I2) — reading a property off a value ──────── // Object literal: `{ a: v }` (key + value children at the same child index), diff --git a/graph/javascript/souffle/decls_all.dl b/graph/javascript/souffle/decls_all.dl index 33d35464a..ed873176a 100644 --- a/graph/javascript/souffle/decls_all.dl +++ b/graph/javascript/souffle/decls_all.dl @@ -397,6 +397,8 @@ .decl var_regime_is_block(c0:symbol) .decl call_ambient_alternative(c0:symbol) .decl method_returns_this(c0:symbol) +.decl returns_own_class_new(c0:symbol, c1:symbol) +.decl own_class_new(c0:symbol, c1:symbol) .decl module_base_path(c0:symbol, c1:symbol, c2:symbol) // ── resolution/arrays.dl: a class extending Map / Set (#619) ── .decl coll_ctor(c0:symbol, c1:symbol) diff --git a/graph/test/javascript/cases/69-this-constructor-per-site/src/main.js b/graph/test/javascript/cases/69-this-constructor-per-site/src/main.js new file mode 100644 index 000000000..cc3033fe2 --- /dev/null +++ b/graph/test/javascript/cases/69-this-constructor-per-site/src/main.js @@ -0,0 +1,43 @@ +'use strict'; +// `new this.constructor()` builds the RECEIVER's class at each site, not every +// subclass of the class that declares it. +class Base { + clone() { return new this.constructor(); } + copy() { return new Base(); } + run() { return 'base'; } +} +class Sub extends Base { run() { return 'sub'; } } +class Other extends Base { run() { return 'other'; } } + +// The same shape on a constructor function and its prototype. +function Shape() {} +Shape.prototype.dup = function () { return new this.constructor(); }; +Shape.prototype.area = function () { return 0; }; +function Circle() { Shape.call(this); } +Circle.prototype = Object.create(Shape.prototype); +Circle.prototype.constructor = Circle; +Circle.prototype.area = function () { return 3; }; + +// The everyday clone: built into a `const`, filled in, returned. +class Query { + clone() { const q = new this.constructor(); q.opts = this.opts; return q; } + // Control: a `let` may be rebound before the return, so it keeps its merged value. + fork() { let q = new this.constructor(); return q; } + exec() { return 'query'; } +} +class Select extends Query { exec() { return 'select'; } } + +function main() { + new Select().clone().exec(); + new Query().clone().exec(); + new Select().fork().exec(); + new Sub().clone().run(); + new Other().clone().run(); + new Sub().clone().clone().run(); + new Base().clone().run(); + // Control: a fixed `new Base()` stays Base whatever the receiver. + new Sub().copy().run(); + new Circle().dup().area(); + new Shape().dup().area(); +} +main(); diff --git a/graph/test/javascript/cases/69-this-constructor-per-site/src/package.json b/graph/test/javascript/cases/69-this-constructor-per-site/src/package.json new file mode 100644 index 000000000..44f892347 --- /dev/null +++ b/graph/test/javascript/cases/69-this-constructor-per-site/src/package.json @@ -0,0 +1 @@ +{ "name": "this-constructor-per-site" } diff --git a/graph/test/javascript/expected/07-esm-class-hierarchy.edges b/graph/test/javascript/expected/07-esm-class-hierarchy.edges index 7e2895c72..feab8244d 100644 --- a/graph/test/javascript/expected/07-esm-class-hierarchy.edges +++ b/graph/test/javascript/expected/07-esm-class-hierarchy.edges @@ -10,8 +10,7 @@ Component.js:7:15 CONSTRUCTOR_CALL AbstractBlock -> known_edge AbstractBlock. Component.js:7:5 METHOD_CALL block.add -> known_edge Node.js:8:3 add Component.js:7:5 METHOD_CALL block.add(new AbstractBlock(this)).walk -> known_edge AbstractBlock.js:4:3 walk Component.js:8:18 METHOD_CALL block.clone -> known_edge Node.js:9:3 clone -Component.js:9:5 METHOD_CALL copy.walk -> multi_inferred AbstractBlock.js:4:3 walk -Component.js:9:5 METHOD_CALL copy.walk -> multi_inferred Node.js:7:3 walk +Component.js:9:5 METHOD_CALL copy.walk -> known_edge AbstractBlock.js:4:3 walk Node.js:7:12 METHOD_CALL this.component.warn -> known_edge Component.js:3:3 warn Node.js:7:39 METHOD_CALL this.children.forEach -> ambient_terminal - Node.js:7:39 METHOD_CALL this.children.forEach -> callback_registered Node.js:7:61 diff --git a/graph/test/javascript/expected/37-this-constructor.edges b/graph/test/javascript/expected/37-this-constructor.edges index f6cbd48d9..129a00d4f 100644 --- a/graph/test/javascript/expected/37-this-constructor.edges +++ b/graph/test/javascript/expected/37-this-constructor.edges @@ -1,14 +1,12 @@ main.js:11:13 CONSTRUCTOR_CALL Sub -> implicit_constructor - main.js:12:3 METHOD_CALL s.clone -> known_edge main.js:3:3 clone -main.js:12:3 METHOD_CALL s.clone().run -> multi_inferred main.js:5:3 run -main.js:12:3 METHOD_CALL s.clone().run -> multi_inferred main.js:8:26 run +main.js:12:3 METHOD_CALL s.clone().run -> known_edge main.js:8:26 run main.js:13:3 METHOD_CALL Sub.build -> known_edge main.js:6:3 build main.js:13:3 METHOD_CALL Sub.build().run -> multi_inferred main.js:5:3 run main.js:13:3 METHOD_CALL Sub.build().run -> multi_inferred main.js:8:26 run main.js:14:3 CONSTRUCTOR_CALL Base -> implicit_constructor - main.js:14:3 METHOD_CALL new Base().make -> known_edge main.js:4:3 make -main.js:14:3 METHOD_CALL new Base().make().run -> multi_inferred main.js:5:3 run -main.js:14:3 METHOD_CALL new Base().make().run -> multi_inferred main.js:8:26 run +main.js:14:3 METHOD_CALL new Base().make().run -> known_edge main.js:5:3 run main.js:16:1 FUNCTION_CALL main -> known_edge main.js:10:1 main main.js:3:20 CONSTRUCTOR_CALL this.constructor -> implicit_constructor - main.js:4:19 CONSTRUCTOR_CALL this.constructor -> implicit_constructor - diff --git a/graph/test/javascript/expected/69-this-constructor-per-site.diag b/graph/test/javascript/expected/69-this-constructor-per-site.diag new file mode 100644 index 000000000..71b17c3dd --- /dev/null +++ b/graph/test/javascript/expected/69-this-constructor-per-site.diag @@ -0,0 +1,2 @@ +package_entry this-constructor-per-site . [] DEFAULT_INDEX index.js MISSING_FILE -> - +unresolved main.js:17:20 METHOD_CALL create no_target diff --git a/graph/test/javascript/expected/69-this-constructor-per-site.edges b/graph/test/javascript/expected/69-this-constructor-per-site.edges new file mode 100644 index 000000000..b94812d28 --- /dev/null +++ b/graph/test/javascript/expected/69-this-constructor-per-site.edges @@ -0,0 +1,41 @@ +main.js:14:44 CONSTRUCTOR_CALL this.constructor -> multi_inferred main.js:13:1 Shape +main.js:14:44 CONSTRUCTOR_CALL this.constructor -> multi_inferred main.js:16:1 Circle +main.js:16:21 FUNCTION_CALL_CALL Shape.call -> known_edge main.js:13:1 Shape +main.js:17:20 METHOD_CALL Object.create -> ambient_terminal - +main.js:23:23 CONSTRUCTOR_CALL this.constructor -> implicit_constructor - +main.js:25:20 CONSTRUCTOR_CALL this.constructor -> implicit_constructor - +main.js:31:3 CONSTRUCTOR_CALL Select -> implicit_constructor - +main.js:31:3 METHOD_CALL new Select().clone -> known_edge main.js:23:3 clone +main.js:31:3 METHOD_CALL new Select().clone().exec -> known_edge main.js:28:30 exec +main.js:32:3 CONSTRUCTOR_CALL Query -> implicit_constructor - +main.js:32:3 METHOD_CALL new Query().clone -> known_edge main.js:23:3 clone +main.js:32:3 METHOD_CALL new Query().clone().exec -> known_edge main.js:26:3 exec +main.js:33:3 CONSTRUCTOR_CALL Select -> implicit_constructor - +main.js:33:3 METHOD_CALL new Select().fork -> known_edge main.js:25:3 fork +main.js:33:3 METHOD_CALL new Select().fork().exec -> multi_inferred main.js:26:3 exec +main.js:33:3 METHOD_CALL new Select().fork().exec -> multi_inferred main.js:28:30 exec +main.js:34:3 CONSTRUCTOR_CALL Sub -> implicit_constructor - +main.js:34:3 METHOD_CALL new Sub().clone -> known_edge main.js:5:3 clone +main.js:34:3 METHOD_CALL new Sub().clone().run -> known_edge main.js:9:26 run +main.js:35:3 CONSTRUCTOR_CALL Other -> implicit_constructor - +main.js:35:3 METHOD_CALL new Other().clone -> known_edge main.js:5:3 clone +main.js:35:3 METHOD_CALL new Other().clone().run -> known_edge main.js:10:28 run +main.js:36:3 CONSTRUCTOR_CALL Sub -> implicit_constructor - +main.js:36:3 METHOD_CALL new Sub().clone -> known_edge main.js:5:3 clone +main.js:36:3 METHOD_CALL new Sub().clone().clone -> known_edge main.js:5:3 clone +main.js:36:3 METHOD_CALL new Sub().clone().clone().run -> known_edge main.js:9:26 run +main.js:37:3 CONSTRUCTOR_CALL Base -> implicit_constructor - +main.js:37:3 METHOD_CALL new Base().clone -> known_edge main.js:5:3 clone +main.js:37:3 METHOD_CALL new Base().clone().run -> known_edge main.js:7:3 run +main.js:39:3 CONSTRUCTOR_CALL Sub -> implicit_constructor - +main.js:39:3 METHOD_CALL new Sub().copy -> known_edge main.js:6:3 copy +main.js:39:3 METHOD_CALL new Sub().copy().run -> known_edge main.js:7:3 run +main.js:40:3 CONSTRUCTOR_CALL Circle -> known_edge main.js:16:1 Circle +main.js:40:3 METHOD_CALL new Circle().dup -> known_edge main.js:14:23 dup +main.js:40:3 METHOD_CALL new Circle().dup().area -> known_edge main.js:19:25 area +main.js:41:3 CONSTRUCTOR_CALL Shape -> known_edge main.js:13:1 Shape +main.js:41:3 METHOD_CALL new Shape().dup -> known_edge main.js:14:23 dup +main.js:41:3 METHOD_CALL new Shape().dup().area -> known_edge main.js:15:24 area +main.js:43:1 FUNCTION_CALL main -> known_edge main.js:30:1 main +main.js:5:20 CONSTRUCTOR_CALL this.constructor -> implicit_constructor - +main.js:6:19 CONSTRUCTOR_CALL Base -> implicit_constructor - diff --git a/graph/test/javascript/expected/69-this-constructor-per-site.oracle b/graph/test/javascript/expected/69-this-constructor-per-site.oracle new file mode 100644 index 000000000..9a8085756 --- /dev/null +++ b/graph/test/javascript/expected/69-this-constructor-per-site.oracle @@ -0,0 +1,29 @@ +main.js:14:44 CONSTRUCTOR_CALL constructor SYNTHESIZED_OVER main.js:13:1;main.js:16:1 +main.js:16:21 FUNCTION_CALL_CALL call EXACT main.js:13:1 +main.js:17:20 METHOD_CALL create LIB_AMBIENT_OK +main.js:23:23 CONSTRUCTOR_CALL constructor SYNTHESIZED_OK +main.js:25:20 CONSTRUCTOR_CALL constructor SYNTHESIZED_OK +main.js:31:3 CONSTRUCTOR_CALL Select SYNTHESIZED_OK +main.js:31:3 METHOD_CALL clone EXACT main.js:23:3 +main.js:32:3 CONSTRUCTOR_CALL Query SYNTHESIZED_OK +main.js:32:3 METHOD_CALL clone EXACT main.js:23:3 +main.js:33:3 CONSTRUCTOR_CALL Select SYNTHESIZED_OK +main.js:33:3 METHOD_CALL fork EXACT main.js:25:3 +main.js:34:3 CONSTRUCTOR_CALL Sub SYNTHESIZED_OK +main.js:34:3 METHOD_CALL clone EXACT main.js:5:3 +main.js:35:3 CONSTRUCTOR_CALL Other SYNTHESIZED_OK +main.js:35:3 METHOD_CALL clone EXACT main.js:5:3 +main.js:36:3 CONSTRUCTOR_CALL Sub SYNTHESIZED_OK +main.js:36:3 METHOD_CALL clone EXACT main.js:5:3 +main.js:37:3 CONSTRUCTOR_CALL Base SYNTHESIZED_OK +main.js:37:3 METHOD_CALL clone EXACT main.js:5:3 +main.js:39:3 CONSTRUCTOR_CALL Sub SYNTHESIZED_OK +main.js:39:3 METHOD_CALL copy EXACT main.js:6:3 +main.js:39:3 METHOD_CALL run EXACT main.js:7:3 +main.js:40:3 CONSTRUCTOR_CALL Circle EXACT main.js:16:1 +main.js:41:3 CONSTRUCTOR_CALL Shape EXACT main.js:13:1 +main.js:41:3 METHOD_CALL dup EXACT main.js:14:23 +main.js:43:1 FUNCTION_CALL main EXACT main.js:30:1 +main.js:5:20 CONSTRUCTOR_CALL constructor SYNTHESIZED_OK +main.js:6:19 CONSTRUCTOR_CALL Base SYNTHESIZED_OK +# defects: 0