Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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).
64 changes: 64 additions & 0 deletions graph/javascript/engine/resolution/frameworks.dl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions graph/javascript/souffle/decls_all.dl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
32 changes: 32 additions & 0 deletions graph/test/javascript/cases/74-orm-model-factory/src/models.js
Original file line number Diff line number Diff line change
@@ -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 };
31 changes: 31 additions & 0 deletions graph/test/javascript/cases/74-orm-model-factory/src/repo.js
Original file line number Diff line number Diff line change
@@ -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 };
17 changes: 17 additions & 0 deletions graph/test/javascript/expected/74-orm-model-factory.diag
Original file line number Diff line number Diff line change
@@ -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
36 changes: 36 additions & 0 deletions graph/test/javascript/expected/74-orm-model-factory.edges
Original file line number Diff line number Diff line change
@@ -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 <function-expression>
models.js:12:48 METHOD_CALL this.findLive -> known_edge models.js:11:30 <function-expression>
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 <function-expression>
models.js:15:48 METHOD_CALL this.toRecord -> known_edge models.js:14:30 <function-expression>
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 <function-expression>
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 <constructor>
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 <function-expression>
repo.js:7:19 METHOD_CALL this.Doc.claim -> known_edge models.js:12:27 <function-expression>
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
12 changes: 12 additions & 0 deletions graph/test/javascript/expected/74-orm-model-factory.oracle
Original file line number Diff line number Diff line change
@@ -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
Loading