From ad56060fed957481343ac6702e3dba2b187ca216 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Wed, 30 Sep 2026 03:08:42 -0700 Subject: [PATCH] javascript: a model made by an ORM factory carries its schema's statics `connection.model(name, schema)` builds the model class at runtime, so a call such as `this.Doc.findLive()` on it had an untyped receiver and was only matched by name, with same-named methods elsewhere as equal candidates. The schema argument now gives the call a value: a `new Schema(...)` reached through an import of the ORM package (named, default, namespace, require, or a local destructure) is tracked, and whatever its `statics` holds (member writes, `static('n', f)`, `static({...})`, the `statics` option) is a member of every model made from it; its `methods` are members of `new Model(...)`. The package's own members stay receiver_untyped as before. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- .../callee-resolution.dl | 9 ++- .../engine/resolution/frameworks.dl | 64 +++++++++++++++++++ graph/javascript/souffle/decls_all.dl | 6 ++ .../cases/74-orm-model-factory/src/models.js | 32 ++++++++++ .../cases/74-orm-model-factory/src/repo.js | 31 +++++++++ .../expected/74-orm-model-factory.diag | 17 +++++ .../expected/74-orm-model-factory.edges | 36 +++++++++++ .../expected/74-orm-model-factory.oracle | 12 ++++ 8 files changed, 204 insertions(+), 3 deletions(-) create mode 100644 graph/test/javascript/cases/74-orm-model-factory/src/models.js create mode 100644 graph/test/javascript/cases/74-orm-model-factory/src/repo.js create mode 100644 graph/test/javascript/expected/74-orm-model-factory.diag create mode 100644 graph/test/javascript/expected/74-orm-model-factory.edges create mode 100644 graph/test/javascript/expected/74-orm-model-factory.oracle diff --git a/graph/javascript/engine/expression-resolution/callee-resolution.dl b/graph/javascript/engine/expression-resolution/callee-resolution.dl index 946d7cb3..a59664b2 100644 --- a/graph/javascript/engine/expression-resolution/callee-resolution.dl +++ b/graph/javascript/engine/expression-resolution/callee-resolution.dl @@ -133,7 +133,10 @@ call_target_is_implicit_ctor(ce) :- call_site(_, "SUPER_CALL", _, _, _, em, ce, // Splits the unresolved population into "the receiver is untyped" (a staging gap // or a genuinely dynamic value) and "the receiver is known and has no such // member" (an engine or parser defect — the rows worth reading first). +// A value a package model builds (frameworks.dl, package_model_kind) carries only the +// members the project wrote onto it; the rest are the uninstalled package's, so a +// site that finds nothing on it is as untyped as it was before the model existed. receiver_value_known(ce) :- call_site(_, _, _, "SYNTACTIC", _, _, ce, _, _), - expr_child(_, ce, "RECEIVER", _, r), expr_value(r, _, _). -callee_value_known(ce) :- callee_value(ce, _, _). -callee_value_known(ce) :- new_callee_value(ce, _, _). + expr_child(_, ce, "RECEIVER", _, r), expr_value(r, k, _), !package_model_kind(k). +callee_value_known(ce) :- callee_value(ce, k, _), !package_model_kind(k). +callee_value_known(ce) :- new_callee_value(ce, k, _), !package_model_kind(k). diff --git a/graph/javascript/engine/resolution/frameworks.dl b/graph/javascript/engine/resolution/frameworks.dl index dc932573..a7361097 100644 --- a/graph/javascript/engine/resolution/frameworks.dl +++ b/graph/javascript/engine/resolution/frameworks.dl @@ -153,6 +153,70 @@ express_error_value(k, i) :- expr_root("client", "THROW", e), expr_value(e, k, i express_error_value(k, i) :- call_site("client", _, _, _, _, _, ce, _, _), callee_value(ce, "func", m), model_express_value("next", "func", m), call_arg(ce, 0, arg), expr_value(arg, k, i). +// ── ORM model factories: a model carries its schema's statics ─────────────── +// `const s = new Schema({...}); s.statics.findLive = function () {...}; +// const Doc = connection.model('Doc', s); Doc.findLive()` — the package builds the +// model class at runtime from the schema, so nothing in the tree declares it, and +// `connection` is usually a parameter nobody visible passes. The SCHEMA is the value +// that says what the model has: whatever `schema.statics` holds (member writes, +// `schema.static('n', f)`, `schema.static({ n: f })`, `new Schema(def, { statics })`) +// is a member of every model made from it, and whatever `schema.methods` holds is a +// member of every document `new Model(...)` makes. The factory call is recognised by +// its schema ARGUMENT, not by its receiver. +// +// A schema is a `new` of the package's `Schema`, reached through an import of a +// modelled package — named, default, namespace or require, directly or through a +// local alias or destructure (`const { Schema } = mongoose`). The package need not be +// installed. The five values are all keyed by the `new Schema(...)` site, so every +// model of one schema shares its members. Every other member of them (`find`, +// `create`, `save`) is the package's and stays unknown: package_model_kind keeps +// those sites receiver_untyped (callee-resolution.dl), as they were. +orm_schema_package("mongoose"). +orm_ref(e, "") :- expr_kind(_, "MODULE_EDGE_CALL", _, e), expr_module_edge(_, imp, e), + import_decl(_, spec, _, _, _, _, _, _, imp), orm_schema_package(spec). +orm_ref(e, "") :- expr_kind(_, "IDENTIFIER", _, e), expr_binding(_, v, e), var_import(_, imp, v), + import_decl(_, spec, _, bf, _, _, _, _, imp), import_binds_whole_module(bf), orm_schema_package(spec). +orm_ref(e, n) :- expr_kind(_, "IDENTIFIER", _, e), expr_binding(_, v, e), var_import(_, imp, v), + import_decl(_, spec, _, bf, n, _, _, _, imp), import_binding_is_named(bf), n != "", orm_schema_package(spec). +orm_ref(e, n) :- expr_kind(_, "IDENTIFIER", _, e), expr_binding(_, v, e), !var_import(_, _, v), + var_init(_, _, init, v), var_binding_path(_, n, v), orm_ref(init, ""). +orm_ref(e, n) :- expr_kind(_, "IDENTIFIER", _, e), expr_binding(_, v, e), !var_import(_, _, v), + var_init(_, _, init, v), !var_binding_path(_, _, v), orm_ref(init, n). +orm_ref(e, n) :- expr_kind(_, k, _, e), access_kind_reads_member(k), expr_name(_, n, e), n != "", + expr_child(_, e, "ACCESS_TARGET", _, r), orm_ref(r, ""). +orm_schema_site(ne) :- call_site(_, "CONSTRUCTOR_CALL", _, _, _, _, ne, _, _), + expr_child(_, ne, "CALLEE", _, c), orm_ref(c, "Schema"). +orm_schema_site(ne) :- call_site(_, "CONSTRUCTOR_CALL", "Schema", "SYNTACTIC", _, _, ne, _, _), + expr_child(_, ne, "RECEIVER", _, r), orm_ref(r, ""). +package_model_kind("orm_schema"). +package_model_kind("orm_statics"). +package_model_kind("orm_methods"). +package_model_kind("orm_model"). +package_model_kind("orm_doc"). +orm_part("statics", "orm_statics", "static"). +orm_part("methods", "orm_methods", "method"). +expr_value(ne, "orm_schema", ne) :- orm_schema_site(ne). +prop_value("orm_schema", s, part, pk, s) :- orm_schema_site(s), orm_part(part, pk, _). +prop_value("orm_schema", s, part, k, i) :- orm_schema_site(s), orm_part(part, _, _), + call_arg(s, 1, o), expr_value(o, "obj", l), prop_value("obj", l, part, k, i). +member_write(pk, s, n, k, i) :- call_site(_, ck, cn, "SYNTACTIC", _, _, ce, _, _), call_kind_is_member_form(ck), + orm_part(_, pk, cn), expr_child(_, ce, "RECEIVER", _, r), expr_value(r, "orm_schema", s), + call_arg(ce, 0, a), expr_value(a, "str", n), call_arg(ce, 1, f), expr_value(f, k, i). +member_write(pk, s, n, k, i) :- call_site(_, ck, cn, "SYNTACTIC", _, _, ce, _, _), call_kind_is_member_form(ck), + orm_part(_, pk, cn), expr_child(_, ce, "RECEIVER", _, r), expr_value(r, "orm_schema", s), + call_arg(ce, 0, a), expr_value(a, "obj", l), prop_value("obj", l, n, k, i). +// `x.model(name, schema)` / `model(name, schema)`: the model. `new Model(...)`: a document. +orm_model_call(ce, s) :- call_site(_, ck, "model", _, _, _, ce, _, _), + (call_kind_is_member_form(ck) ; call_kind_is_callee_form(ck)), + call_arg(ce, 1, a), expr_value(a, "orm_schema", s). +expr_value(ce, "orm_model", s) :- orm_model_call(ce, s). +expr_value(ne, "orm_doc", s) :- expr_kind(_, "NEW", _, ne), new_callee_value(ne, "orm_model", s). +prop_value("orm_model", s, n, k, i) :- prop_value("orm_schema", s, "statics", k0, i0), prop_value(k0, i0, n, k, i). +prop_value("orm_doc", s, n, k, i) :- prop_value("orm_schema", s, "methods", k0, i0), prop_value(k0, i0, n, k, i). +// A static runs with the model as `this`, a method with the document. +this_value(m, "orm_model", s) :- prop_value("orm_model", s, _, "func", m), method_this_binding(_, "DYNAMIC", m), !method_owner_type(m, _). +this_value(m, "orm_doc", s) :- prop_value("orm_doc", s, _, "func", m), method_this_binding(_, "DYNAMIC", m), !method_owner_type(m, _). + // ── property descriptors (#489) ───────────────────────────────────────────── // `Object.defineProperty(o, 'm', { value: f })`, `Object.defineProperties(o, { m: {...} })`, // `Object.create(proto, { m: {...} })`, and the export form a bundler-compiled CommonJS diff --git a/graph/javascript/souffle/decls_all.dl b/graph/javascript/souffle/decls_all.dl index d02c17d9..7f73c459 100644 --- a/graph/javascript/souffle/decls_all.dl +++ b/graph/javascript/souffle/decls_all.dl @@ -503,6 +503,12 @@ .decl express_error_slot(c0:symbol, c1:symbol) .decl express_param_slot(c0:symbol, c1:symbol) .decl express_error_value(c0:symbol, c1:symbol) +.decl orm_schema_package(c0:symbol) +.decl orm_ref(c0:symbol, c1:symbol) +.decl orm_schema_site(c0:symbol) +.decl package_model_kind(c0:symbol) +.decl orm_part(c0:symbol, c1:symbol, c2:symbol) +.decl orm_model_call(c0:symbol, c1:symbol) .decl heritage_text(c0:symbol, c1:symbol, c2:symbol) .decl type_own_getter(c0:symbol, c1:symbol, c2:symbol) .decl type_own_static_getter(c0:symbol, c1:symbol, c2:symbol) diff --git a/graph/test/javascript/cases/74-orm-model-factory/src/models.js b/graph/test/javascript/cases/74-orm-model-factory/src/models.js new file mode 100644 index 00000000..c5b18612 --- /dev/null +++ b/graph/test/javascript/cases/74-orm-model-factory/src/models.js @@ -0,0 +1,32 @@ +'use strict'; +// A model made by an ORM factory, `connection.model(name, schema)`, is a class the +// package builds at runtime from the schema: its statics are the functions written +// onto `schema.statics` (or handed to `schema.static(...)`), and a document it +// constructs has the schema's methods. The package is not installed, so nothing in +// the tree declares the model; the schema argument is what carries its members. +const mongoose = require('mongoose'); +const { Schema } = mongoose; + +const docSchema = new Schema({ title: String }); +docSchema.statics.findLive = function () { return this.find({}); }; +docSchema.static('claim', function () { return this.findLive(); }); +docSchema.static({ purge() { return 0; } }); +docSchema.methods.toRecord = function () { return this.title; }; +docSchema.method('touch', function () { return this.toRecord(); }); + +// The options form, on the package's namespace. +const jobSchema = new mongoose.Schema({}, { statics: { due() { return 1; } } }); + +// Control: a schema-like object from a package that is not modelled stays unknown. +const { Schema: OtherSchema } = require('other-orm'); +const otherSchema = new OtherSchema({}); +otherSchema.statics.findLive = function () { return 2; }; + +function registerModels(connection) { + return { + Doc: connection.models.Doc || connection.model('Doc', docSchema), + Job: mongoose.model('Job', jobSchema), + Other: connection.model('Other', otherSchema), + }; +} +module.exports = { registerModels }; diff --git a/graph/test/javascript/cases/74-orm-model-factory/src/repo.js b/graph/test/javascript/cases/74-orm-model-factory/src/repo.js new file mode 100644 index 00000000..55f3b77d --- /dev/null +++ b/graph/test/javascript/cases/74-orm-model-factory/src/repo.js @@ -0,0 +1,31 @@ +'use strict'; +const { registerModels } = require('./models'); + +class DocRepository { + constructor(models) { this.Doc = models.Doc; this.Job = models.Job; this.Other = models.Other; } + list() { return this.Doc.findLive(); } + take() { return this.Doc.claim(); } + purge() { return this.Doc.purge(); } + due() { return this.Job.due(); } + record() { return new this.Doc({}).touch(); } + save() { return this.Doc.create({}); } // the package's own member: stays unknown + other() { return this.Other.findLive(); } // control: the unmodelled package +} + +// Control: an unrelated repository with same-named methods, reached by nothing above. +class MemoryRepository { + findLive() { return []; } + claim() { return null; } +} + +// Control: a project `.model(name, x)` whose second argument is not a schema keeps +// its own return value. +const registry = { model(name, def) { return def; } }; +const plain = registry.model('x', { findLive() { return 3; } }); + +function start(connection) { + const repo = new DocRepository(registerModels(connection)); + return [repo.list(), repo.take(), repo.purge(), repo.due(), repo.record(), repo.save(), repo.other(), + plain.findLive(), new MemoryRepository()]; +} +module.exports = { start }; diff --git a/graph/test/javascript/expected/74-orm-model-factory.diag b/graph/test/javascript/expected/74-orm-model-factory.diag new file mode 100644 index 00000000..f191f565 --- /dev/null +++ b/graph/test/javascript/expected/74-orm-model-factory.diag @@ -0,0 +1,17 @@ +import_cause models.js:21:17 other-orm not_staged +import_cause models.js:7:18 mongoose not_staged +package_entry @axiomcode/code-graph . [] MAIN dist/reason.js NOT_STAGED -> - +unresolved models.js:10:19 CONSTRUCTOR_CALL Schema callee_untyped +unresolved models.js:11:51 METHOD_CALL find receiver_untyped +unresolved models.js:12:1 METHOD_CALL static receiver_untyped +unresolved models.js:13:1 METHOD_CALL static receiver_untyped +unresolved models.js:15:1 METHOD_CALL method receiver_untyped +unresolved models.js:18:19 CONSTRUCTOR_CALL Schema receiver_untyped +unresolved models.js:22:21 CONSTRUCTOR_CALL OtherSchema callee_untyped +unresolved models.js:27:35 METHOD_CALL model receiver_untyped +unresolved models.js:28:10 METHOD_CALL model receiver_untyped +unresolved models.js:29:12 METHOD_CALL model receiver_untyped +unresolved repo.js:10:21 CONSTRUCTOR_CALL Doc member_absent +unresolved repo.js:11:19 METHOD_CALL create receiver_untyped +unresolved repo.js:12:20 METHOD_CALL findLive receiver_untyped +value_callee models.js:10:19 Schema module_variable diff --git a/graph/test/javascript/expected/74-orm-model-factory.edges b/graph/test/javascript/expected/74-orm-model-factory.edges new file mode 100644 index 00000000..f6b6bf7a --- /dev/null +++ b/graph/test/javascript/expected/74-orm-model-factory.edges @@ -0,0 +1,36 @@ +models.js:10:19 CONSTRUCTOR_CALL Schema -> ambiguous_unknown - +models.js:11:51 METHOD_CALL this.find -> ambiguous_unknown - +models.js:12:1 METHOD_CALL docSchema.static -> ambiguous_unknown - +models.js:12:1 METHOD_CALL docSchema.static -> callback_registered models.js:12:27 +models.js:12:48 METHOD_CALL this.findLive -> known_edge models.js:11:30 +models.js:13:1 METHOD_CALL docSchema.static -> ambiguous_unknown - +models.js:13:1 METHOD_CALL docSchema.static -> callback_registered models.js:13:20 purge +models.js:15:1 METHOD_CALL docSchema.method -> ambiguous_unknown - +models.js:15:1 METHOD_CALL docSchema.method -> callback_registered models.js:15:27 +models.js:15:48 METHOD_CALL this.toRecord -> known_edge models.js:14:30 +models.js:18:19 CONSTRUCTOR_CALL mongoose.Schema -> ambiguous_unknown - +models.js:18:19 CONSTRUCTOR_CALL mongoose.Schema -> callback_registered models.js:18:56 due +models.js:22:21 CONSTRUCTOR_CALL OtherSchema -> ambiguous_unknown - +models.js:27:35 METHOD_CALL connection.model -> ambiguous_unknown - +models.js:28:10 METHOD_CALL mongoose.model -> ambiguous_unknown - +models.js:29:12 METHOD_CALL connection.model -> ambiguous_unknown - +repo.js:10:21 CONSTRUCTOR_CALL this.Doc -> ambiguous_unknown - +repo.js:10:21 METHOD_CALL new this.Doc({}).touch -> known_edge models.js:15:27 +repo.js:11:19 METHOD_CALL this.Doc.create -> ambiguous_unknown - +repo.js:12:20 METHOD_CALL this.Other.findLive -> ambiguous_unknown - +repo.js:24:15 METHOD_CALL registry.model -> known_edge repo.js:23:20 model +repo.js:27:16 CONSTRUCTOR_CALL DocRepository -> known_edge repo.js:5:3 +repo.js:27:34 FUNCTION_CALL registerModels -> known_edge models.js:25:1 registerModels +repo.js:28:11 METHOD_CALL repo.list -> known_edge repo.js:6:3 list +repo.js:28:24 METHOD_CALL repo.take -> known_edge repo.js:7:3 take +repo.js:28:37 METHOD_CALL repo.purge -> known_edge repo.js:8:3 purge +repo.js:28:51 METHOD_CALL repo.due -> known_edge repo.js:9:3 due +repo.js:28:63 METHOD_CALL repo.record -> known_edge repo.js:10:3 record +repo.js:28:78 METHOD_CALL repo.save -> known_edge repo.js:11:3 save +repo.js:28:91 METHOD_CALL repo.other -> known_edge repo.js:12:3 other +repo.js:29:23 CONSTRUCTOR_CALL MemoryRepository -> implicit_constructor - +repo.js:29:5 METHOD_CALL plain.findLive -> known_edge repo.js:24:37 findLive +repo.js:6:19 METHOD_CALL this.Doc.findLive -> known_edge models.js:11:30 +repo.js:7:19 METHOD_CALL this.Doc.claim -> known_edge models.js:12:27 +repo.js:8:20 METHOD_CALL this.Doc.purge -> known_edge models.js:13:20 purge +repo.js:9:18 METHOD_CALL this.Job.due -> known_edge models.js:18:56 due diff --git a/graph/test/javascript/expected/74-orm-model-factory.oracle b/graph/test/javascript/expected/74-orm-model-factory.oracle new file mode 100644 index 00000000..914865e5 --- /dev/null +++ b/graph/test/javascript/expected/74-orm-model-factory.oracle @@ -0,0 +1,12 @@ +repo.js:24:15 METHOD_CALL model EXACT repo.js:23:20 +repo.js:27:16 CONSTRUCTOR_CALL DocRepository EXACT repo.js:5:3 +repo.js:27:34 FUNCTION_CALL registerModels EXACT models.js:25:1 +repo.js:28:11 METHOD_CALL list EXACT repo.js:6:3 +repo.js:28:24 METHOD_CALL take EXACT repo.js:7:3 +repo.js:28:37 METHOD_CALL purge EXACT repo.js:8:3 +repo.js:28:51 METHOD_CALL due EXACT repo.js:9:3 +repo.js:28:63 METHOD_CALL record EXACT repo.js:10:3 +repo.js:28:78 METHOD_CALL save EXACT repo.js:11:3 +repo.js:28:91 METHOD_CALL other EXACT repo.js:12:3 +repo.js:29:23 CONSTRUCTOR_CALL MemoryRepository SYNTHESIZED_OK +# defects: 0