@suss/adapter-ruby 0.29.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -63,7 +63,52 @@ var OWN_BODY_TYPES = /* @__PURE__ */ new Set([
63
63
  "singleton_class"
64
64
  ]);
65
65
  var NESTING_TYPES = /* @__PURE__ */ new Set(["class", "module"]);
66
+ var INCLUDE_CALL = "include";
67
+ var PREPEND_CALL = "prepend";
66
68
  var ARGUMENT_LIST_TYPE = "argument_list";
69
+ var BLOCK_TYPES = /* @__PURE__ */ new Set(["block", "do_block"]);
70
+ var NO_BODY_BLOCKS = /* @__PURE__ */ new Map();
71
+ function isModuleBody(body) {
72
+ return body.parent?.type === "module";
73
+ }
74
+ function declaredBlockAt(statement, isModule, blocks) {
75
+ if (statement.type !== "call" || field(statement, "receiver") !== null) {
76
+ return null;
77
+ }
78
+ const method = field(statement, "method");
79
+ const kind = method === null ? void 0 : blocks.get(method.text);
80
+ if (kind === void 0 || kind.moduleOnly && !isModule) {
81
+ return null;
82
+ }
83
+ return field(statement, "block") === null ? null : kind;
84
+ }
85
+ function bodyStatementsRun(body, isModule, blocks) {
86
+ return bodyStatements(body).flatMap((statement) => {
87
+ if (declaredBlockAt(statement, isModule, blocks) === null) {
88
+ return [statement];
89
+ }
90
+ const block = field(statement, "block");
91
+ const inner = block === null ? null : field(block, "body");
92
+ return inner === null ? [] : bodyStatementsRun(inner, isModule, blocks);
93
+ });
94
+ }
95
+ function definedAtClassLevel(node, body, blocks) {
96
+ if (blocks.size === 0) {
97
+ return false;
98
+ }
99
+ const isModule = isModuleBody(body);
100
+ let current = node.parent;
101
+ while (current !== null && current.id !== body.id) {
102
+ const parent = current.parent;
103
+ if (BLOCK_TYPES.has(current.type) && parent !== null) {
104
+ if (declaredBlockAt(parent, isModule, blocks)?.definesClassMethods) {
105
+ return true;
106
+ }
107
+ }
108
+ current = parent;
109
+ }
110
+ return false;
111
+ }
67
112
  function runStatements(body, blockConfigures = () => false) {
68
113
  const found = [];
69
114
  const readsItsChildren = (node) => {
@@ -99,10 +144,10 @@ function stringLiteralValue(node) {
99
144
  function symbolValue(node) {
100
145
  return node.type === "simple_symbol" ? node.text.slice(1) : null;
101
146
  }
102
- function instanceMethodsByName(body) {
147
+ function instanceMethodsByName(body, blocks = NO_BODY_BLOCKS) {
103
148
  const methods = /* @__PURE__ */ new Map();
104
149
  for (const stmt of runStatements(body)) {
105
- if (stmt.type !== "method") {
150
+ if (stmt.type !== "method" || definedAtClassLevel(stmt, body, blocks)) {
106
151
  continue;
107
152
  }
108
153
  const name = field(stmt, "name")?.text;
@@ -164,10 +209,11 @@ function instanceMethodVisibility(body) {
164
209
  }
165
210
  return visibility;
166
211
  }
167
- function singletonMethodsByName(body) {
212
+ function singletonMethodsByName(body, blocks = NO_BODY_BLOCKS) {
168
213
  const methods = /* @__PURE__ */ new Map();
169
214
  for (const stmt of runStatements(body)) {
170
- if (stmt.type !== "singleton_method") {
215
+ const classLevel = stmt.type === "method" && definedAtClassLevel(stmt, body, blocks);
216
+ if (stmt.type !== "singleton_method" && !classLevel) {
171
217
  continue;
172
218
  }
173
219
  const name = field(stmt, "name")?.text;
@@ -280,7 +326,8 @@ function autoloadDirectories(root) {
280
326
  }
281
327
 
282
328
  // src/defineMethod.ts
283
- import { force as force2, literalOf as literalOf2 } from "@suss/values";
329
+ import { nodeOfKey as nodeOfKey2 } from "@suss/resolution";
330
+ import { force as force2, hole as hole2, literalOf as literalOf2, piecesOf } from "@suss/values";
284
331
 
285
332
  // src/values/evaluator.ts
286
333
  import { nodeOfKey } from "@suss/resolution";
@@ -321,6 +368,29 @@ var RUBY_RULES = [
321
368
  lit("givesBackOne", v("n"), v("m"))
322
369
  ],
323
370
  "declared finder"
371
+ ),
372
+ // Ruby cannot refer to a method without running it, so
373
+ // `Settings.filters` is worth what the method returns. The shared
374
+ // rules hang that step on a `call` fact this spelling never gets.
375
+ rule(
376
+ "invokes",
377
+ [v("x"), v("f")],
378
+ [
379
+ lit("readsProperty", v("x"), v("o"), v("m")),
380
+ lit("comesTo", v("x"), v("f")),
381
+ lit("func", v("f"))
382
+ ],
383
+ "property read runs a method"
384
+ ),
385
+ // `%i[a b].freeze` is worth the list it was written as. The evaluator
386
+ // says the same in its row table, for a value it reads in one file.
387
+ ...["freeze", "dup"].map(
388
+ (method) => rule(
389
+ "stepsTo",
390
+ [v("x"), v("o"), VALUE_STEP],
391
+ [lit("readsProperty", v("x"), v("o"), constant(method))],
392
+ "hands back the receiver"
393
+ )
324
394
  )
325
395
  ];
326
396
  var RUBY_PROGRAM = resolutionProgram(RUBY_RULES);
@@ -340,8 +410,8 @@ import { startsAtName, valueLeftByWrites } from "@suss/resolution";
340
410
 
341
411
  // src/facts/locals.ts
342
412
  import { writesRunInOrder } from "@suss/resolution";
343
- var BLOCK_TYPES = /* @__PURE__ */ new Set(["block", "do_block"]);
344
- var LATER_BODY_TYPES = /* @__PURE__ */ new Set([...OWN_BODY_TYPES, ...BLOCK_TYPES]);
413
+ var BLOCK_TYPES2 = /* @__PURE__ */ new Set(["block", "do_block"]);
414
+ var LATER_BODY_TYPES = /* @__PURE__ */ new Set([...OWN_BODY_TYPES, ...BLOCK_TYPES2]);
345
415
  var RUBY_NAME_TYPES = /* @__PURE__ */ new Set([
346
416
  "identifier",
347
417
  "instance_variable"
@@ -494,7 +564,7 @@ function ownerOfName(node, name, enclosing) {
494
564
  function blockDeclaring(node, name) {
495
565
  let current = node.parent;
496
566
  while (current !== null) {
497
- if (BLOCK_TYPES.has(current.type) && parameterNames(current).has(name)) {
567
+ if (BLOCK_TYPES2.has(current.type) && parameterNames(current).has(name)) {
498
568
  return current;
499
569
  }
500
570
  if (OWN_BODY_TYPES.has(current.type)) {
@@ -744,17 +814,70 @@ function emitHash(emitter, hash) {
744
814
  add(emitter, "holdsProperty", objectKey, keyText, valueKey(emitter, value));
745
815
  }
746
816
  }
747
- function walkExpressions(node, visit) {
817
+ var BLOCK_TYPES3 = /* @__PURE__ */ new Set(["block", "do_block"]);
818
+ var LOOP_METHODS = /* @__PURE__ */ new Set(["each", "each_with_index", "map"]);
819
+ var DEFINE_METHOD_CALL = "define_method";
820
+ function loopTurnAt(emitter, block) {
821
+ const call = block.parent;
822
+ if (call?.type !== "call") {
823
+ return null;
824
+ }
825
+ const receiver = field(call, "receiver");
826
+ const method = field(call, "method");
827
+ if (receiver === null || method === null || !LOOP_METHODS.has(method.text)) {
828
+ return null;
829
+ }
830
+ const parameters = field(block, "parameters");
831
+ const names = parameters === null ? [] : children(parameters).map((p) => p.text);
832
+ const element = names[0];
833
+ if (element === void 0) {
834
+ return null;
835
+ }
836
+ return {
837
+ element,
838
+ index: names[1] ?? "",
839
+ overKey: nodeId(emitter.filePath, receiver)
840
+ };
841
+ }
842
+ function emitDynamicDefinition(emitter, call, turns) {
843
+ if (emitter.selfKey === null || // A call inside a method runs when that method does, not at load time.
844
+ emitter.enclosing !== null || call.type !== "call" || field(call, "receiver") !== null || field(call, "method")?.text !== DEFINE_METHOD_CALL) {
845
+ return;
846
+ }
847
+ const args = field(call, "arguments");
848
+ const first = args === null ? void 0 : children(args)[0];
849
+ const nameKey2 = nodeId(emitter.filePath, first ?? call);
850
+ add(emitter, "definesMethodFrom", emitter.selfKey, nameKey2);
851
+ for (const turn of turns) {
852
+ add(
853
+ emitter,
854
+ "nameTurnsOn",
855
+ nameKey2,
856
+ turn.element,
857
+ turn.index,
858
+ turn.overKey
859
+ );
860
+ }
861
+ }
862
+ function walkExpressions(node, emitter, visit, turns = []) {
748
863
  for (const child of children(node)) {
749
864
  if (OWN_BODY_TYPES.has(child.type)) {
750
865
  continue;
751
866
  }
752
- visit(child);
753
- walkExpressions(child, visit);
867
+ visit(child, turns);
868
+ const opened = BLOCK_TYPES3.has(child.type) ? loopTurnAt(emitter, child) : null;
869
+ walkExpressions(
870
+ child,
871
+ emitter,
872
+ visit,
873
+ opened === null ? turns : [...turns, opened]
874
+ );
754
875
  }
755
876
  }
756
877
  function emitExpressionFacts(emitter, node) {
757
- walkExpressions(node, (child) => {
878
+ emitDynamicDefinition(emitter, node, []);
879
+ walkExpressions(node, emitter, (child, turns) => {
880
+ emitDynamicDefinition(emitter, child, turns);
758
881
  if (isPropertyRead(child)) {
759
882
  emitPropertyRead(emitter, child);
760
883
  } else if (child.type === "call") {
@@ -843,7 +966,20 @@ function implicitReturn(body) {
843
966
  (child) => child.type !== "rescue" && child.type !== "ensure"
844
967
  );
845
968
  const last = statements[statements.length - 1];
846
- return last === void 0 || last.type === "return" ? null : last;
969
+ if (last === void 0 || last.type === "return") {
970
+ return null;
971
+ }
972
+ return assignedValueOf(last) ?? last;
973
+ }
974
+ function assignedValueOf(node) {
975
+ if (!ASSIGNMENT_TYPES.has(node.type)) {
976
+ return null;
977
+ }
978
+ const operator = field(node, "operator")?.text;
979
+ if (node.type !== "assignment" && !WHOLE_VALUE_OPERATORS.has(operator ?? "")) {
980
+ return null;
981
+ }
982
+ return field(node, "right");
847
983
  }
848
984
  function emitMethodFacts(emitter, method) {
849
985
  const funcKey = nodeId(emitter.filePath, method);
@@ -873,7 +1009,7 @@ function emitMethodFacts(emitter, method) {
873
1009
  }
874
1010
  };
875
1011
  recordNested(body);
876
- walkExpressions(body, (child) => {
1012
+ walkExpressions(body, inside, (child) => {
877
1013
  if (child.type === "return") {
878
1014
  const first = children(child)[0];
879
1015
  const returned = first?.type === "argument_list" ? children(first)[0] : first;
@@ -963,23 +1099,37 @@ function emitCandidates(emitter, key, group) {
963
1099
  }
964
1100
  }
965
1101
  var METHOD_TYPES = /* @__PURE__ */ new Set(["method", "singleton_method"]);
1102
+ var CONSTANT_REF_TYPES = /* @__PURE__ */ new Set(["constant", "scope_resolution"]);
1103
+ function mixedInConstants(body, callName) {
1104
+ return bareCallArgumentGroups(body, callName).flatMap((group) => [...group].reverse()).reverse().map(readThrough).filter((argument) => CONSTANT_REF_TYPES.has(argument.type));
1105
+ }
1106
+ function emitMixinFacts(emitter, classKey, body, callName) {
1107
+ for (const mixin of mixedInConstants(body, callName)) {
1108
+ add(emitter, "extends", classKey, valueKey(emitter, mixin));
1109
+ }
1110
+ }
966
1111
  function emitClassFacts(emitter, cls) {
967
1112
  const classKey = nodeId(emitter.filePath, cls);
968
1113
  add(emitter, "objectValue", classKey);
1114
+ const body = field(cls, "body");
1115
+ if (body !== null) {
1116
+ emitMixinFacts(emitter, classKey, body, PREPEND_CALL);
1117
+ emitMixinFacts(emitter, classKey, body, INCLUDE_CALL);
1118
+ }
969
1119
  const superclass = field(cls, "superclass");
970
1120
  const base = superclass === null ? null : children(superclass)[0] ?? null;
971
1121
  if (base !== null) {
972
1122
  add(emitter, "extends", classKey, valueKey(emitter, base));
973
1123
  add(emitter, "extendsNamed", classKey, base.text);
974
1124
  }
975
- const body = field(cls, "body");
976
1125
  const collected = /* @__PURE__ */ new Map();
977
1126
  const within = {
978
1127
  ...emitter,
979
1128
  selfKey: classKey,
980
1129
  instanceWrites: collected
981
1130
  };
982
- for (const statement of body === null ? [] : children(body)) {
1131
+ const statements = body === null ? [] : bodyStatementsRun(body, cls.type === "module", emitter.bodyBlocks);
1132
+ for (const statement of statements) {
983
1133
  if (statement.type === "assignment") {
984
1134
  const left = field(statement, "left");
985
1135
  const right = field(statement, "right");
@@ -1000,6 +1150,7 @@ function emitClassFacts(emitter, cls) {
1000
1150
  continue;
1001
1151
  }
1002
1152
  if (!METHOD_TYPES.has(statement.type)) {
1153
+ emitExpressionFacts(within, statement);
1003
1154
  continue;
1004
1155
  }
1005
1156
  const funcKey = emitMethodFacts(within, statement);
@@ -1011,13 +1162,14 @@ function emitClassFacts(emitter, cls) {
1011
1162
  emitInstanceWrites(within, classKey, collected);
1012
1163
  return classKey;
1013
1164
  }
1014
- function emitValueFacts(db, filePath, root) {
1165
+ function emitValueFacts(db, filePath, root, bodyBlocks = NO_BODY_BLOCKS) {
1015
1166
  const emitter = {
1016
1167
  db,
1017
1168
  filePath,
1018
1169
  enclosing: null,
1019
1170
  selfKey: null,
1020
- instanceWrites: null
1171
+ instanceWrites: null,
1172
+ bodyBlocks
1021
1173
  };
1022
1174
  const declaresName = (child, key) => {
1023
1175
  const name = field(child, "name");
@@ -1057,7 +1209,7 @@ var OWN_ROOT_TYPES = /* @__PURE__ */ new Set([
1057
1209
  "module",
1058
1210
  "singleton_class"
1059
1211
  ]);
1060
- var BLOCK_TYPES2 = /* @__PURE__ */ new Set(["block", "do_block"]);
1212
+ var BLOCK_TYPES4 = /* @__PURE__ */ new Set(["block", "do_block"]);
1061
1213
  var METHOD_TYPES2 = /* @__PURE__ */ new Set(["method", "singleton_method"]);
1062
1214
  var NAME_TYPES = /* @__PURE__ */ new Set([
1063
1215
  "identifier",
@@ -1566,7 +1718,7 @@ function isTailExpression(node) {
1566
1718
  if (owner.type === "block" && owner.parent?.type === "lambda") {
1567
1719
  return true;
1568
1720
  }
1569
- if (METHOD_TYPES2.has(owner.type) || BLOCK_TYPES2.has(owner.type)) {
1721
+ if (METHOD_TYPES2.has(owner.type) || BLOCK_TYPES4.has(owner.type)) {
1570
1722
  return true;
1571
1723
  }
1572
1724
  if (owner.type === "when") {
@@ -1585,7 +1737,7 @@ function isRoot(node) {
1585
1737
  if (OWN_ROOT_TYPES.has(node.type)) {
1586
1738
  return true;
1587
1739
  }
1588
- return BLOCK_TYPES2.has(node.type) && node.parent?.type === "call";
1740
+ return BLOCK_TYPES4.has(node.type) && node.parent?.type === "call";
1589
1741
  }
1590
1742
  function isPathStatement(node, parent) {
1591
1743
  if (node.type === "elsif") {
@@ -1639,7 +1791,7 @@ function functionOf(node) {
1639
1791
  body: bodyOf(block)
1640
1792
  };
1641
1793
  }
1642
- if (BLOCK_TYPES2.has(node.type)) {
1794
+ if (BLOCK_TYPES4.has(node.type)) {
1643
1795
  return {
1644
1796
  parameters: parametersOf2(field(node, "parameters")),
1645
1797
  body: bodyOf(node)
@@ -2128,113 +2280,119 @@ function contextOver(db, nodes) {
2128
2280
  }
2129
2281
 
2130
2282
  // src/defineMethod.ts
2131
- var DEFINE_METHOD_CALL = "define_method";
2132
- var LOOP_METHODS = /* @__PURE__ */ new Set(["each", "each_with_index", "map"]);
2133
- var BLOCK_TYPES3 = /* @__PURE__ */ new Set(["block", "do_block"]);
2134
- var NOTHING = { names: /* @__PURE__ */ new Set(), unreadable: false };
2135
- var byTree = /* @__PURE__ */ new WeakMap();
2136
- function defineMethodNames(body) {
2137
- let perTree = byTree.get(body.tree);
2138
- if (perTree === void 0) {
2139
- perTree = /* @__PURE__ */ new Map();
2140
- byTree.set(body.tree, perTree);
2141
- }
2142
- const cached = perTree.get(body.id);
2143
- if (cached !== void 0) {
2144
- return cached;
2145
- }
2146
- const read = readNames(body);
2147
- perTree.set(body.id, read);
2148
- return read;
2149
- }
2150
- function readNames(body) {
2151
- const calls = bareCalls(body, DEFINE_METHOD_CALL);
2152
- if (calls.length === 0) {
2153
- return NOTHING;
2283
+ var NOTHING = {
2284
+ names: /* @__PURE__ */ new Set(),
2285
+ patterns: [],
2286
+ unreadable: false
2287
+ };
2288
+ function couldBeDefined(defined, name) {
2289
+ return defined.unreadable || defined.patterns.some((pattern) => pattern.test(name));
2290
+ }
2291
+ function definedNamesOf(dynamic, classKey) {
2292
+ return dynamic?.get(classKey) ?? NOTHING;
2293
+ }
2294
+ function readDynamicNames(db, rootsByFile) {
2295
+ const byClass = /* @__PURE__ */ new Map();
2296
+ for (const [classKey, nameKey2] of db.facts("definesMethodFrom")) {
2297
+ const key = String(classKey);
2298
+ byClass.set(
2299
+ key,
2300
+ foldReading(
2301
+ byClass.get(key) ?? NOTHING,
2302
+ readOne(db, rootsByFile, String(nameKey2))
2303
+ )
2304
+ );
2154
2305
  }
2155
- const names = /* @__PURE__ */ new Set();
2156
- let unreadable = false;
2157
- for (const call of calls) {
2158
- const defined = namesDefinedBy(call);
2159
- if (defined === null) {
2160
- unreadable = true;
2161
- continue;
2162
- }
2163
- for (const name of defined) {
2164
- names.add(name);
2306
+ for (const [classKey, reading] of byClass) {
2307
+ for (const name of reading.names) {
2308
+ db.add("declaresName", [classKey, name]);
2165
2309
  }
2166
2310
  }
2167
- return { names, unreadable };
2311
+ return byClass;
2168
2312
  }
2169
- function namesDefinedBy(call) {
2170
- const args = field(call, "arguments");
2171
- const first = args === null ? void 0 : bodyStatements(args)[0];
2172
- if (first === void 0) {
2173
- return null;
2313
+ function foldReading(soFar, reading) {
2314
+ if (reading === null) {
2315
+ return { ...soFar, unreadable: true };
2174
2316
  }
2317
+ return {
2318
+ names: /* @__PURE__ */ new Set([...soFar.names, ...reading.names]),
2319
+ patterns: [...soFar.patterns, ...reading.patterns],
2320
+ unreadable: soFar.unreadable
2321
+ };
2322
+ }
2323
+ function readOne(db, rootsByFile, nameKey2) {
2324
+ const node = nodeOfKey2(rootsByFile, nameKey2);
2175
2325
  const names = [];
2176
- for (const bindings of loopTurns(call)) {
2177
- const name = stringValueOf(
2178
- first,
2179
- void 0,
2180
- bindings.size === 0 ? void 0 : bindings
2181
- );
2182
- if (name === null) {
2326
+ const patterns = [];
2327
+ for (const bindings of loopTurns(db, rootsByFile, nameKey2)) {
2328
+ const value = node === null ? hole2(nameKey2) : evaluatedValue(node, db, bindings.size === 0 ? void 0 : bindings);
2329
+ const name = literalOf2(value);
2330
+ if (name !== null) {
2331
+ names.push(name);
2332
+ continue;
2333
+ }
2334
+ const pattern = patternOf(value);
2335
+ if (pattern === null) {
2183
2336
  return null;
2184
2337
  }
2185
- names.push(name);
2338
+ patterns.push(pattern);
2186
2339
  }
2187
- return names;
2340
+ return { names, patterns };
2188
2341
  }
2189
- function loopTurns(call) {
2190
- let turns = [/* @__PURE__ */ new Map()];
2191
- for (const taken of blocksAround(call)) {
2192
- const perElement = elementBindings(taken);
2193
- if (perElement === null) {
2342
+ function patternOf(value) {
2343
+ const pieces = piecesOf(value);
2344
+ let source = "^";
2345
+ let readSomething = false;
2346
+ for (const piece of pieces) {
2347
+ if (piece.kind === "hole") {
2348
+ source += "[\\s\\S]*";
2349
+ continue;
2350
+ }
2351
+ const options = piece.options.filter((option) => option.length > 0);
2352
+ if (options.length === 0) {
2194
2353
  continue;
2195
2354
  }
2355
+ readSomething = true;
2356
+ source += `(?:${options.map(escapeForPattern).join("|")})`;
2357
+ }
2358
+ return readSomething ? new RegExp(`${source}$`) : null;
2359
+ }
2360
+ function escapeForPattern(text2) {
2361
+ return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2362
+ }
2363
+ function loopTurns(db, rootsByFile, nameKey2) {
2364
+ let turns = [/* @__PURE__ */ new Map()];
2365
+ for (const over of listsBehind(db, rootsByFile, nameKey2)) {
2196
2366
  turns = turns.flatMap(
2197
- (base) => perElement.map((element) => new Map([...base, ...element]))
2367
+ (base) => over.elements.map((value, position) => {
2368
+ const binding = new Map([...base, [over.element, value]]);
2369
+ if (over.index !== "") {
2370
+ binding.set(over.index, String(position));
2371
+ }
2372
+ return binding;
2373
+ })
2198
2374
  );
2199
2375
  }
2200
2376
  return turns;
2201
2377
  }
2202
- function blocksAround(call) {
2203
- const blocks = [];
2204
- let current = call.parent;
2205
- while (current !== null && !OWN_BODY_TYPES.has(current.type)) {
2206
- const parent = current.parent;
2207
- if (BLOCK_TYPES3.has(current.type) && parent?.type === "call") {
2208
- blocks.unshift({ block: current, call: parent });
2378
+ function listsBehind(db, rootsByFile, nameKey2) {
2379
+ const found = [];
2380
+ for (const [, element, index, overKey] of db.lookup(
2381
+ "nameTurnsOn",
2382
+ 0,
2383
+ nameKey2
2384
+ )) {
2385
+ const elements = literalElementsOf(rootsByFile, String(overKey), db);
2386
+ if (elements !== null) {
2387
+ found.push({ element: String(element), index: String(index), elements });
2209
2388
  }
2210
- current = parent;
2211
2389
  }
2212
- return blocks;
2390
+ return found;
2213
2391
  }
2214
- function elementBindings(taken) {
2215
- const { block, call } = taken;
2216
- const receiver = field(call, "receiver");
2217
- const method = field(call, "method")?.text;
2218
- if (receiver === null || method === void 0 || !LOOP_METHODS.has(method)) {
2219
- return null;
2220
- }
2221
- const parameters = field(block, "parameters");
2222
- const [element, index] = parameters === null ? [] : bodyStatements(parameters).map((parameter) => parameter.text);
2223
- const elements = element === void 0 ? null : literalElementsOf(receiver);
2224
- if (element === void 0 || elements === null) {
2225
- return null;
2226
- }
2227
- return elements.map((value, position) => {
2228
- const binding = /* @__PURE__ */ new Map([[element, value]]);
2229
- if (index !== void 0) {
2230
- binding.set(index, String(position));
2231
- }
2232
- return binding;
2233
- });
2234
- }
2235
- function literalElementsOf(node) {
2236
- const value = evaluatedValue(node);
2237
- if (value.kind !== "sequence") {
2392
+ function literalElementsOf(rootsByFile, key, db) {
2393
+ const node = nodeOfKey2(rootsByFile, key);
2394
+ const value = node === null ? null : evaluatedValue(node, db);
2395
+ if (value === null || value.kind !== "sequence") {
2238
2396
  return null;
2239
2397
  }
2240
2398
  const elements = [];
@@ -2374,8 +2532,6 @@ function graphqlTypeNameFromQualified(qualifiedName, convention) {
2374
2532
  }
2375
2533
 
2376
2534
  // src/ancestry.ts
2377
- var INCLUDE_CALL = "include";
2378
- var PREPEND_CALL = "prepend";
2379
2535
  async function reachDefinition(qualifiedName, lookup) {
2380
2536
  const filePath = resolveConstantFile(
2381
2537
  lookup.root,
@@ -2502,7 +2658,7 @@ function superclassCandidatesOf(blocks) {
2502
2658
  }
2503
2659
  return [];
2504
2660
  }
2505
- function methodInAncestry(ancestry, name) {
2661
+ function methodInAncestry(ancestry, name, read = {}) {
2506
2662
  for (const entry of ancestry) {
2507
2663
  if (entry.type === "root") {
2508
2664
  return { type: "none" };
@@ -2514,7 +2670,7 @@ function methodInAncestry(ancestry, name) {
2514
2670
  cause: "unreadAncestor"
2515
2671
  };
2516
2672
  }
2517
- const found = definitionIn(entry.blocks, name);
2673
+ const found = definitionIn(entry.blocks, name, read);
2518
2674
  if (found.method !== null && found.block !== null) {
2519
2675
  return { type: "found", method: found.method, block: found.block };
2520
2676
  }
@@ -2528,7 +2684,7 @@ function methodInAncestry(ancestry, name) {
2528
2684
  }
2529
2685
  return { type: "none" };
2530
2686
  }
2531
- function definitionIn(blocks, name) {
2687
+ function definitionIn(blocks, name, read) {
2532
2688
  let method = null;
2533
2689
  let block = null;
2534
2690
  let definedDynamically = false;
@@ -2538,14 +2694,17 @@ function definitionIn(blocks, name) {
2538
2694
  if (body === null) {
2539
2695
  continue;
2540
2696
  }
2541
- const found = instanceMethodsByName(body).get(name);
2697
+ const found = instanceMethodsByName(body, read.bodyBlocks).get(name);
2542
2698
  if (found !== void 0) {
2543
2699
  method = found;
2544
2700
  block = candidate;
2545
2701
  }
2546
- const defined = defineMethodNames(body);
2702
+ const defined = definedNamesOf(
2703
+ read.dynamicNames,
2704
+ nodeId(candidate.file, candidate.info.node)
2705
+ );
2547
2706
  definedDynamically ||= defined.names.has(name);
2548
- unreadableDefine ||= defined.unreadable;
2707
+ unreadableDefine ||= couldBeDefined(defined, name);
2549
2708
  }
2550
2709
  return { method, block, definedDynamically, unreadableDefine };
2551
2710
  }
@@ -2959,7 +3118,7 @@ function lowerRubyBody(body, terminals, responseCalls = []) {
2959
3118
 
2960
3119
  // src/paths/effects.ts
2961
3120
  var RAISE_NAMES2 = /* @__PURE__ */ new Set(["raise", "fail"]);
2962
- var BLOCK_TYPES4 = /* @__PURE__ */ new Set([
3121
+ var BLOCK_TYPES5 = /* @__PURE__ */ new Set([
2963
3122
  "body_statement",
2964
3123
  "then",
2965
3124
  "else",
@@ -3088,7 +3247,7 @@ function argsOf(call) {
3088
3247
  function enclosingStatement(call, body) {
3089
3248
  let current = call;
3090
3249
  while (current.parent !== null) {
3091
- if (BLOCK_TYPES4.has(current.parent.type) || current.parent.id === body.id) {
3250
+ if (BLOCK_TYPES5.has(current.parent.type) || current.parent.id === body.id) {
3092
3251
  return current;
3093
3252
  }
3094
3253
  current = current.parent;
@@ -3837,7 +3996,7 @@ function isDefaultedAt(node) {
3837
3996
  }
3838
3997
 
3839
3998
  // src/filters.ts
3840
- function controllerFilters(pattern, ancestry) {
3999
+ function controllerFilters(pattern, ancestry, read = {}) {
3841
4000
  const forms = pattern.filters ?? [];
3842
4001
  if (forms.length === 0) {
3843
4002
  return [];
@@ -3872,7 +4031,7 @@ function controllerFilters(pattern, ancestry) {
3872
4031
  }
3873
4032
  const resolved = [];
3874
4033
  for (const declaration of declared) {
3875
- const found = methodInAncestry(ancestry, declaration.methodName);
4034
+ const found = methodInAncestry(ancestry, declaration.methodName, read);
3876
4035
  if (found.type !== "found") {
3877
4036
  continue;
3878
4037
  }
@@ -4076,28 +4235,98 @@ function modelPattern(constant2, file, options) {
4076
4235
  (candidate) => reachesBase(options.facts, classKey, candidate.baseClasses)
4077
4236
  );
4078
4237
  }
4079
- function selectorOf(node) {
4080
- const picked = [];
4081
- const walk = (current) => {
4082
- const args = field(current, "arguments");
4083
- for (const argument of args === null ? [] : children3(args)) {
4084
- if (argument.type !== "pair") {
4085
- continue;
4086
- }
4087
- const key = field(argument, "key");
4238
+ function chainLinks(call) {
4239
+ const links = [];
4240
+ let current = call;
4241
+ while (current !== null && current.type === "call") {
4242
+ links.push(current);
4243
+ current = receiverOf(current);
4244
+ }
4245
+ return links;
4246
+ }
4247
+ function kindOfCall(pattern, method) {
4248
+ if (pattern.writes.includes(method)) {
4249
+ return "write";
4250
+ }
4251
+ if (pattern.reads.includes(method)) {
4252
+ return "read";
4253
+ }
4254
+ return void 0;
4255
+ }
4256
+ function someLibraryDefines(options, method) {
4257
+ return options.patterns.some(
4258
+ (pattern) => kindOfCall(pattern, method) !== void 0
4259
+ );
4260
+ }
4261
+ function argumentsOf2(call) {
4262
+ const args = field(call, "arguments");
4263
+ return args === null ? [] : children3(args);
4264
+ }
4265
+ function isKeywordArgument(argument) {
4266
+ return argument.type === "pair" || argument.type === "hash";
4267
+ }
4268
+ function bareName(node) {
4269
+ return node.text.replace(/^:/, "").replace(/:$/, "");
4270
+ }
4271
+ function pairsIn(argument) {
4272
+ if (argument.type === "pair") {
4273
+ return [argument];
4274
+ }
4275
+ if (argument.type === "hash") {
4276
+ return children3(argument).filter((child) => child.type === "pair");
4277
+ }
4278
+ return [];
4279
+ }
4280
+ function keywordKeys(call) {
4281
+ const keys = [];
4282
+ for (const argument of argumentsOf2(call)) {
4283
+ for (const pair of pairsIn(argument)) {
4284
+ const key = field(pair, "key");
4088
4285
  if (key !== null) {
4089
- picked.push(key.text.replace(/^:/, "").replace(/:$/, ""));
4286
+ keys.push(bareName(key));
4090
4287
  }
4091
4288
  }
4092
- const receiver = receiverOf(current);
4093
- if (receiver !== null) {
4094
- walk(receiver);
4289
+ }
4290
+ return keys;
4291
+ }
4292
+ function hasPositionalArgument(call) {
4293
+ return argumentsOf2(call).some((argument) => !isKeywordArgument(argument));
4294
+ }
4295
+ function columnsAskedFor(call, pattern) {
4296
+ if (!(pattern.columnArguments ?? []).includes(methodOf(call))) {
4297
+ return [];
4298
+ }
4299
+ return argumentsOf2(call).filter((argument) => argument.type === "simple_symbol").map(bareName);
4300
+ }
4301
+ function primaryKeySelector(call, pattern) {
4302
+ const byPrimaryKey = pattern.byPrimaryKey;
4303
+ if (byPrimaryKey === void 0) {
4304
+ return [];
4305
+ }
4306
+ const picks = byPrimaryKey.methods.includes(methodOf(call)) && hasPositionalArgument(call);
4307
+ return picks ? [byPrimaryKey.column] : [];
4308
+ }
4309
+ function selectorOf(call, pattern) {
4310
+ const picked = [];
4311
+ for (const link of chainLinks(call)) {
4312
+ picked.push(...primaryKeySelector(link, pattern));
4313
+ if (pattern.reads.includes(methodOf(link))) {
4314
+ picked.push(...keywordKeys(link));
4095
4315
  }
4096
- };
4097
- walk(node);
4316
+ }
4098
4317
  return [...new Set(picked)];
4099
4318
  }
4100
- function storageEffect(call, container, pattern, kind, selector) {
4319
+ function fieldsOf(call, pattern, kind) {
4320
+ if (kind === "write") {
4321
+ return [...new Set(keywordKeys(call))];
4322
+ }
4323
+ return [
4324
+ ...new Set(
4325
+ chainLinks(call).flatMap((link) => columnsAskedFor(link, pattern))
4326
+ )
4327
+ ];
4328
+ }
4329
+ function storageEffect(call, container, pattern, kind, selector, fields) {
4101
4330
  const operation = methodOf(call);
4102
4331
  return {
4103
4332
  type: "interaction",
@@ -4111,35 +4340,12 @@ function storageEffect(call, container, pattern, kind, selector) {
4111
4340
  interaction: {
4112
4341
  class: "storage-access",
4113
4342
  kind,
4114
- fields: [],
4343
+ fields,
4115
4344
  operation,
4116
4345
  ...selector.length > 0 ? { selector } : {}
4117
4346
  }
4118
4347
  };
4119
4348
  }
4120
- function modelCallEffects(call, file, options) {
4121
- const constant2 = rootConstant(call);
4122
- if (constant2 === null) {
4123
- return [];
4124
- }
4125
- const pattern = modelPattern(constant2, file, options);
4126
- if (pattern === void 0) {
4127
- return [];
4128
- }
4129
- const kind = pattern.writes.includes(methodOf(call)) ? "write" : "read";
4130
- return [
4131
- storageEffect(
4132
- call,
4133
- constantName(constant2),
4134
- pattern,
4135
- kind,
4136
- selectorOf(call)
4137
- )
4138
- ];
4139
- }
4140
- function writeMethods(patterns) {
4141
- return new Set(patterns.flatMap((pattern) => pattern.writes));
4142
- }
4143
4349
  function classSettledOn(facts, key) {
4144
4350
  askResolution2(facts, [key], "wanted", RUBY_PROGRAM);
4145
4351
  const objects = new Set(
@@ -4156,27 +4362,64 @@ function declaredName(facts, classKey) {
4156
4362
  function projectDeclares(facts, classKey, method) {
4157
4363
  return facts.lookup("wantedDeclaredName", 0, classKey).some((row) => String(row[1]) === method);
4158
4364
  }
4159
- function instanceWriteEffects(call, file, options, enclosing) {
4365
+ function constantReceiver(constant2, file, facts) {
4366
+ const classKey = classBehind(facts, file, constant2);
4367
+ return classKey === void 0 ? void 0 : { classKey, container: constantName(constant2) };
4368
+ }
4369
+ function settledReceiver(receiver, file, facts, enclosing) {
4370
+ const classKey = classSettledOn(facts, readKey(file, receiver, enclosing));
4371
+ if (classKey === void 0) {
4372
+ return void 0;
4373
+ }
4374
+ const container = declaredName(facts, classKey);
4375
+ return container === void 0 ? void 0 : { classKey, container };
4376
+ }
4377
+ function receiverClass(call, file, options, enclosing) {
4378
+ const constant2 = rootConstant(call);
4379
+ if (constant2 !== null) {
4380
+ return constantReceiver(constant2, file, options.facts);
4381
+ }
4160
4382
  const receiver = receiverOf(call);
4161
- const method = methodOf(call);
4162
- if (receiver === null || !writeMethods(options.patterns).has(method)) {
4163
- return [];
4383
+ if (receiver === null) {
4384
+ return void 0;
4164
4385
  }
4165
- const classKey = classSettledOn(
4166
- options.facts,
4167
- readKey(file, receiver, enclosing)
4168
- );
4169
- if (classKey === void 0) {
4386
+ return settledReceiver(receiver, file, options.facts, enclosing);
4387
+ }
4388
+ function libraryCallIn(call, options) {
4389
+ return chainLinks(call).find(
4390
+ (link) => someLibraryDefines(options, methodOf(link))
4391
+ ) ?? null;
4392
+ }
4393
+ function modelCallEffects(call, file, options, enclosing) {
4394
+ const worked = libraryCallIn(call, options);
4395
+ if (worked === null) {
4170
4396
  return [];
4171
4397
  }
4172
- const pattern = options.patterns.find(
4173
- (candidate) => candidate.writes.includes(method) && reachesBase(options.facts, classKey, candidate.baseClasses)
4174
- );
4175
- if (pattern === void 0 || projectDeclares(options.facts, classKey, method)) {
4398
+ const method = methodOf(worked);
4399
+ const target = receiverClass(worked, file, options, enclosing);
4400
+ if (target === void 0) {
4176
4401
  return [];
4177
4402
  }
4178
- const container = declaredName(options.facts, classKey);
4179
- return container === void 0 ? [] : [storageEffect(call, container, pattern, "write", [])];
4403
+ for (const pattern of options.patterns) {
4404
+ const kind = kindOfCall(pattern, method);
4405
+ if (kind === void 0 || !reachesBase(options.facts, target.classKey, pattern.baseClasses)) {
4406
+ continue;
4407
+ }
4408
+ if (projectDeclares(options.facts, target.classKey, method)) {
4409
+ return [];
4410
+ }
4411
+ return [
4412
+ storageEffect(
4413
+ worked,
4414
+ target.container,
4415
+ pattern,
4416
+ kind,
4417
+ selectorOf(worked, pattern),
4418
+ fieldsOf(worked, pattern, kind)
4419
+ )
4420
+ ];
4421
+ }
4422
+ return [];
4180
4423
  }
4181
4424
  function isLoader(node, loader) {
4182
4425
  if (node === null) {
@@ -4214,7 +4457,7 @@ function loaderCallEffects(call, file, options) {
4214
4457
  const pattern = modelPattern(argument, file, options);
4215
4458
  if (pattern !== void 0) {
4216
4459
  effects.push(
4217
- storageEffect(call, constantName(argument), pattern, "read", [])
4460
+ storageEffect(call, constantName(argument), pattern, "read", [], [])
4218
4461
  );
4219
4462
  }
4220
4463
  }
@@ -4222,22 +4465,21 @@ function loaderCallEffects(call, file, options) {
4222
4465
  return effects;
4223
4466
  }
4224
4467
  function effectsOfCall(call, file, options, enclosing) {
4225
- const onModel = modelCallEffects(call, file, options);
4226
- if (onModel.length > 0) {
4227
- return onModel;
4228
- }
4229
- const onInstance = rootConstant(call) === null ? instanceWriteEffects(call, file, options, enclosing) : [];
4230
- return onInstance.length > 0 ? onInstance : loaderCallEffects(call, file, options);
4468
+ const onModel = modelCallEffects(call, file, options, enclosing);
4469
+ return onModel.length > 0 ? onModel : loaderCallEffects(call, file, options);
4231
4470
  }
4232
4471
  function storageClaims(call, file, options, enclosing = null) {
4233
4472
  return options.patterns.length > 0 && effectsOfCall(call, file, options, enclosing).length > 0;
4234
4473
  }
4235
4474
  function receiverKeysToAsk(chains, file, options, enclosing) {
4236
- const writes = writeMethods(options.patterns);
4237
4475
  const keys = [];
4238
4476
  for (const call of chains) {
4239
- const receiver = receiverOf(call);
4240
- if (receiver !== null && writes.has(methodOf(call)) && rootConstant(call) === null) {
4477
+ const worked = libraryCallIn(call, options);
4478
+ if (worked === null || rootConstant(worked) !== null) {
4479
+ continue;
4480
+ }
4481
+ const receiver = receiverOf(worked);
4482
+ if (receiver !== null) {
4241
4483
  keys.push(readKey(file, receiver, enclosing));
4242
4484
  }
4243
4485
  }
@@ -4334,11 +4576,17 @@ function createFileCache(parse, readFile) {
4334
4576
  }
4335
4577
  };
4336
4578
  }
4337
- function fieldReadContext(pattern, cache, fileBlocks, bodyRead) {
4579
+ function fieldReadContext(pattern, cache, fileBlocks, bodyRead, facts) {
4338
4580
  return {
4339
4581
  pattern,
4340
4582
  cache,
4341
4583
  bodyRead,
4584
+ facts,
4585
+ read: {
4586
+ facts,
4587
+ bodyBlocks: bodyRead.bodyBlocks,
4588
+ dynamicNames: bodyRead.dynamicNames
4589
+ },
4342
4590
  lookup: {
4343
4591
  root: pattern.root,
4344
4592
  pathConvention: pattern.pathConvention,
@@ -4421,7 +4669,13 @@ async function graphqlObjectFieldUnits(pattern, pack, info, ownBlocks, fileBlock
4421
4669
  if (info.bodyNode === null || pattern.baseClassNames.includes(info.qualifiedName)) {
4422
4670
  return [];
4423
4671
  }
4424
- const ctx = fieldReadContext(pattern, options.cache, fileBlocks, options);
4672
+ const ctx = fieldReadContext(
4673
+ pattern,
4674
+ options.cache,
4675
+ fileBlocks,
4676
+ options,
4677
+ options.facts
4678
+ );
4425
4679
  const ancestry = await ancestryOf(info.qualifiedName, ownBlocks, ctx.lookup);
4426
4680
  if (!reachesConfiguredBase(ancestry, info.qualifiedName, pattern.baseClassNames)) {
4427
4681
  return [];
@@ -4469,7 +4723,11 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
4469
4723
  ])) {
4470
4724
  return [];
4471
4725
  }
4472
- const filters = controllerFilters(pattern, ancestry);
4726
+ const filters = controllerFilters(pattern, ancestry, {
4727
+ facts: options.facts,
4728
+ bodyBlocks: options.bodyBlocks,
4729
+ dynamicNames: options.dynamicNames
4730
+ });
4473
4731
  const units = [];
4474
4732
  for (const filter of filters) {
4475
4733
  const displayPath = options.displayPathOf?.(filter.file) ?? filter.file;
@@ -4514,7 +4772,10 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
4514
4772
  });
4515
4773
  };
4516
4774
  const own = /* @__PURE__ */ new Set();
4517
- for (const [actionName, method, block] of publicInstanceMethods(ownBlocks)) {
4775
+ for (const [actionName, method, block] of publicInstanceMethods(
4776
+ ownBlocks,
4777
+ options.bodyBlocks
4778
+ )) {
4518
4779
  own.add(actionName);
4519
4780
  emitAction(actionName, method, block);
4520
4781
  }
@@ -4524,7 +4785,8 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
4524
4785
  continue;
4525
4786
  }
4526
4787
  for (const [actionName, method, block] of publicInstanceMethods(
4527
- entry.blocks
4788
+ entry.blocks,
4789
+ options.bodyBlocks
4528
4790
  )) {
4529
4791
  if (seen.has(actionName)) {
4530
4792
  continue;
@@ -4538,14 +4800,17 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
4538
4800
  }
4539
4801
  return units;
4540
4802
  }
4541
- function publicInstanceMethods(blocks) {
4803
+ function publicInstanceMethods(blocks, bodyBlocks) {
4542
4804
  const found = [];
4543
4805
  for (const block of blocks) {
4544
4806
  if (block.info.bodyNode === null) {
4545
4807
  continue;
4546
4808
  }
4547
4809
  const visibility = instanceMethodVisibility(block.info.bodyNode);
4548
- for (const [name, method] of instanceMethodsByName(block.info.bodyNode)) {
4810
+ for (const [name, method] of instanceMethodsByName(
4811
+ block.info.bodyNode,
4812
+ bodyBlocks
4813
+ )) {
4549
4814
  if ((visibility.get(name) ?? "public") === "public") {
4550
4815
  found.push([name, method, block]);
4551
4816
  }
@@ -4808,7 +5073,7 @@ async function readFieldShape(symbol, callArgs, scope, ctx, ancestry, range) {
4808
5073
  return {
4809
5074
  contract: literalContract(callArgs, scope, ctx),
4810
5075
  body: bodyFromLookup(
4811
- methodInAncestry(ancestry, symbol),
5076
+ methodInAncestry(ancestry, symbol, ctx.read),
4812
5077
  range,
4813
5078
  "This field",
4814
5079
  NO_METHOD_BEHIND_IT,
@@ -4853,7 +5118,7 @@ async function readWiredClass(ref, scope, ctx) {
4853
5118
  return {
4854
5119
  contract: readClassContract(ancestry, ctx.pattern),
4855
5120
  body: bodyFromLookup(
4856
- methodInAncestry(ancestry, ctx.pattern.resolverMethodName),
5121
+ methodInAncestry(ancestry, ctx.pattern.resolverMethodName, ctx.read),
4857
5122
  range,
4858
5123
  `This field's ${targetQualifiedName}`,
4859
5124
  methodNotSettled(
@@ -5025,6 +5290,202 @@ function buildFieldUnit(pack, typeName, decl, filePath) {
5025
5290
  };
5026
5291
  }
5027
5292
 
5293
+ // src/inflect.ts
5294
+ var SINGULAR_RULES = [
5295
+ [/s$/i, ""],
5296
+ [/(ss)$/i, "$1"],
5297
+ [/(n)ews$/i, "$1ews"],
5298
+ [/([ti])a$/i, "$1um"],
5299
+ [
5300
+ /((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(sis|ses)$/i,
5301
+ "$1sis"
5302
+ ],
5303
+ [/(^analy)(sis|ses)$/i, "$1sis"],
5304
+ [/([^f])ves$/i, "$1fe"],
5305
+ [/(hive)s$/i, "$1"],
5306
+ [/(tive)s$/i, "$1"],
5307
+ [/([lr])ves$/i, "$1f"],
5308
+ [/([^aeiouy]|qu)ies$/i, "$1y"],
5309
+ [/(s)eries$/i, "$1eries"],
5310
+ [/(m)ovies$/i, "$1ovie"],
5311
+ [/(x|ch|ss|sh)es$/i, "$1"],
5312
+ [/^(m|l)ice$/i, "$1ouse"],
5313
+ [/(bus)(es)?$/i, "$1"],
5314
+ [/(o)es$/i, "$1"],
5315
+ [/(shoe)s$/i, "$1"],
5316
+ [/(cris|test)(is|es)$/i, "$1is"],
5317
+ [/^(a)x[ie]s$/i, "$1xis"],
5318
+ [/(octop|vir)(us|i)$/i, "$1us"],
5319
+ [/(alias|status)(es)?$/i, "$1"],
5320
+ [/^(ox)en/i, "$1"],
5321
+ [/(vert|ind)ices$/i, "$1ex"],
5322
+ [/(matr)ices$/i, "$1ix"],
5323
+ [/(quiz)zes$/i, "$1"],
5324
+ [/(database)s$/i, "$1"],
5325
+ // The words ActiveSupport calls irregular. It declares them after the
5326
+ // rules above, which is what puts them ahead in the search.
5327
+ [/(p)erson$/i, "$1erson"],
5328
+ [/(p)eople$/i, "$1erson"],
5329
+ [/(m)an$/i, "$1an"],
5330
+ [/(m)en$/i, "$1an"],
5331
+ [/(c)hild$/i, "$1hild"],
5332
+ [/(c)hildren$/i, "$1hild"],
5333
+ [/(s)ex$/i, "$1ex"],
5334
+ [/(s)exes$/i, "$1ex"],
5335
+ [/(m)ove$/i, "$1ove"],
5336
+ [/(m)oves$/i, "$1ove"],
5337
+ [/(z)ombie$/i, "$1ombie"],
5338
+ [/(z)ombies$/i, "$1ombie"]
5339
+ ];
5340
+ var UNCOUNTABLE = /* @__PURE__ */ new Set([
5341
+ "equipment",
5342
+ "information",
5343
+ "rice",
5344
+ "money",
5345
+ "species",
5346
+ "series",
5347
+ "fish",
5348
+ "sheep",
5349
+ "jeans",
5350
+ "police"
5351
+ ]);
5352
+ function singularize(word, project) {
5353
+ if (UNCOUNTABLE.has(word.toLowerCase()) || (project?.uncountable ?? []).some(
5354
+ (uncountable) => uncountable.toLowerCase() === word.toLowerCase()
5355
+ )) {
5356
+ return word;
5357
+ }
5358
+ const own = projectSingular(word, project);
5359
+ if (own !== null) {
5360
+ return own;
5361
+ }
5362
+ for (let at = SINGULAR_RULES.length - 1; at >= 0; at--) {
5363
+ const [pattern, replacement] = SINGULAR_RULES[at];
5364
+ if (pattern.test(word)) {
5365
+ return word.replace(pattern, replacement);
5366
+ }
5367
+ }
5368
+ return word;
5369
+ }
5370
+ function projectSingular(word, project) {
5371
+ const irregular = [...project?.irregular ?? []].reverse();
5372
+ for (const [plural, singular] of irregular) {
5373
+ if (plural.toLowerCase() === word.toLowerCase()) {
5374
+ return matchingCase(word, singular);
5375
+ }
5376
+ }
5377
+ const rules = [...project?.singular ?? []].reverse();
5378
+ for (const [rule3, replacement] of rules) {
5379
+ const applied = applyRule(word, rule3, replacement);
5380
+ if (applied !== null) {
5381
+ return applied;
5382
+ }
5383
+ }
5384
+ return null;
5385
+ }
5386
+ function applyRule(word, rule3, replacement) {
5387
+ const written = /^\/(.*)\/([a-z]*)$/s.exec(rule3);
5388
+ if (written === null) {
5389
+ return word.includes(rule3) ? word.replace(rule3, replacement) : null;
5390
+ }
5391
+ const flags = (written[2] ?? "").replace("m", "s").replace(/[^is]/g, "");
5392
+ const pattern = new RegExp(written[1] ?? "", flags);
5393
+ return pattern.test(word) ? word.replace(pattern, replacement.replace(/\\(\d)/g, "$$$1")) : null;
5394
+ }
5395
+ function matchingCase(word, replacement) {
5396
+ const first = word[0];
5397
+ if (first === void 0 || first !== first.toUpperCase()) {
5398
+ return replacement;
5399
+ }
5400
+ return replacement.charAt(0).toUpperCase() + replacement.slice(1);
5401
+ }
5402
+ function camelize(word, acronyms = []) {
5403
+ return word.split("_").map((part) => capitalize(part, acronyms)).join("");
5404
+ }
5405
+ function capitalize(part, acronyms) {
5406
+ if (part === "") {
5407
+ return "";
5408
+ }
5409
+ const acronym = acronyms.find(
5410
+ (known) => known.toLowerCase() === part.toLowerCase()
5411
+ );
5412
+ return acronym ?? part[0]?.toUpperCase() + part.slice(1);
5413
+ }
5414
+ function associationTargetName(name, plural, project) {
5415
+ return camelize(
5416
+ plural ? singularize(name, project) : name,
5417
+ project?.acronyms ?? []
5418
+ );
5419
+ }
5420
+
5421
+ // src/facts/associations.ts
5422
+ function declaredName2(argument) {
5423
+ if (argument === void 0) {
5424
+ return null;
5425
+ }
5426
+ if (argument.type === "simple_symbol") {
5427
+ return argument.text.slice(1);
5428
+ }
5429
+ return stringLiteralValue(argument);
5430
+ }
5431
+ function targetReference(key, written, nesting) {
5432
+ if (written.startsWith("::")) {
5433
+ return { key, written: written.slice(2), nesting: [] };
5434
+ }
5435
+ return { key, written, nesting };
5436
+ }
5437
+ function targetName(call, calls, name, plural, inflections) {
5438
+ const override = readCallArgs(field(call, "arguments")).keyword[calls.classNameKeyword];
5439
+ if (override === void 0) {
5440
+ return associationTargetName(name, plural, inflections);
5441
+ }
5442
+ return stringLiteralValue(override);
5443
+ }
5444
+ function associationsDeclaredIn(cls, classKey, nesting, calls, inflections) {
5445
+ const body = field(cls, "body");
5446
+ if (body === null || calls.length === 0) {
5447
+ return [];
5448
+ }
5449
+ const found = [];
5450
+ for (const statement of runStatements(body)) {
5451
+ if (statement.type !== "call" || field(statement, "receiver") !== null) {
5452
+ continue;
5453
+ }
5454
+ const method = field(statement, "method")?.text ?? "";
5455
+ for (const declaration of calls) {
5456
+ const plural = declaration.plural.includes(method);
5457
+ if (!plural && !declaration.singular.includes(method)) {
5458
+ continue;
5459
+ }
5460
+ const positional = readCallArgs(field(statement, "arguments")).positional;
5461
+ const name = declaredName2(positional[0]);
5462
+ if (name === null) {
5463
+ continue;
5464
+ }
5465
+ const written = targetName(
5466
+ statement,
5467
+ declaration,
5468
+ name,
5469
+ plural,
5470
+ inflections
5471
+ );
5472
+ if (written === null) {
5473
+ continue;
5474
+ }
5475
+ found.push({
5476
+ classKey,
5477
+ name,
5478
+ target: targetReference(
5479
+ `${classKey}#association:${name}`,
5480
+ written,
5481
+ nesting
5482
+ )
5483
+ });
5484
+ }
5485
+ }
5486
+ return found;
5487
+ }
5488
+
5028
5489
  // src/facts/constants.ts
5029
5490
  function children4(node) {
5030
5491
  return node.namedChildren.filter((child) => child !== null);
@@ -5059,20 +5520,32 @@ function isDeclaration(node, parent) {
5059
5520
  }
5060
5521
  return parent.type === "scope_resolution";
5061
5522
  }
5062
- function collectFileConstants(filePath, root) {
5523
+ function collectFileConstants(filePath, root, associationCalls = [], inflections) {
5063
5524
  const definitions = [];
5064
5525
  const references = [];
5526
+ const associations = [];
5065
5527
  const walk = (node, parent, nesting) => {
5066
5528
  if (NESTING_TYPES.has(node.type)) {
5067
5529
  const name = field(node, "name");
5068
5530
  const written = name === null ? null : writtenName(name);
5069
5531
  if (written !== null) {
5532
+ const classKey = nodeId(filePath, node);
5070
5533
  definitions.push({
5071
5534
  qualifiedName: qualify(nesting, written),
5072
- key: nodeId(filePath, node),
5535
+ key: classKey,
5073
5536
  kind: "declaration"
5074
5537
  });
5075
5538
  const inside = [...nesting, ...written.split("::")];
5539
+ for (const declared of associationsDeclaredIn(
5540
+ node,
5541
+ classKey,
5542
+ inside,
5543
+ associationCalls,
5544
+ inflections
5545
+ )) {
5546
+ associations.push(declared);
5547
+ references.push(declared.target);
5548
+ }
5076
5549
  for (const child of children4(node)) {
5077
5550
  walk(child, node, inside);
5078
5551
  }
@@ -5104,7 +5577,7 @@ function collectFileConstants(filePath, root) {
5104
5577
  }
5105
5578
  };
5106
5579
  walk(root, null, []);
5107
- return { filePath, definitions, references };
5580
+ return { filePath, definitions, references, associations };
5108
5581
  }
5109
5582
  function emitConstantBindings(db, perFile) {
5110
5583
  const byName = /* @__PURE__ */ new Map();
@@ -5128,6 +5601,13 @@ function emitConstantBindings(db, perFile) {
5128
5601
  return found.length === 1 ? found[0].key : null;
5129
5602
  };
5130
5603
  for (const file of files) {
5604
+ for (const declared of file.associations) {
5605
+ db.add("declaresAssociation", [
5606
+ declared.classKey,
5607
+ declared.name,
5608
+ declared.target.key
5609
+ ]);
5610
+ }
5131
5611
  for (const reference of file.references) {
5132
5612
  for (let depth = reference.nesting.length; depth >= 0; depth--) {
5133
5613
  const candidate = qualify(
@@ -5289,6 +5769,35 @@ function requiredRelativePath(node) {
5289
5769
  return positional[0] === void 0 ? null : stringLiteralValue(positional[0]);
5290
5770
  }
5291
5771
 
5772
+ // src/pack.ts
5773
+ function inflectionsIn(packs) {
5774
+ const pooled = {
5775
+ acronyms: [],
5776
+ irregular: [],
5777
+ uncountable: [],
5778
+ singular: []
5779
+ };
5780
+ for (const pack of packs) {
5781
+ pooled.acronyms?.push(...pack.inflections?.acronyms ?? []);
5782
+ pooled.irregular?.push(...pack.inflections?.irregular ?? []);
5783
+ pooled.uncountable?.push(...pack.inflections?.uncountable ?? []);
5784
+ pooled.singular?.push(...pack.inflections?.singular ?? []);
5785
+ }
5786
+ return pooled;
5787
+ }
5788
+ function bodyBlocksIn(packs) {
5789
+ const pooled = /* @__PURE__ */ new Map();
5790
+ for (const pack of packs) {
5791
+ for (const declared of pack.bodyBlocks ?? []) {
5792
+ pooled.set(declared.name, {
5793
+ moduleOnly: declared.moduleOnly ?? false,
5794
+ definesClassMethods: declared.definesClassMethods ?? false
5795
+ });
5796
+ }
5797
+ }
5798
+ return pooled;
5799
+ }
5800
+
5292
5801
  // src/reach/closure.ts
5293
5802
  import {
5294
5803
  functionCallBinding,
@@ -5466,7 +5975,11 @@ function methodOnAncestryOf(qualifiedName, methodName, ctx) {
5466
5975
  if (ancestry === void 0) {
5467
5976
  return stop("outsideRun");
5468
5977
  }
5469
- const found = methodInAncestry(ancestry, methodName);
5978
+ const found = methodInAncestry(ancestry, methodName, {
5979
+ facts: ctx.facts,
5980
+ bodyBlocks: ctx.bodyBlocks,
5981
+ dynamicNames: ctx.dynamicNames
5982
+ });
5470
5983
  if (found.type === "found") {
5471
5984
  return followed(reachedMethod(found.method, found.block, methodName));
5472
5985
  }
@@ -5495,7 +6008,10 @@ function singletonMethodOn(qualifiedName, methodName, ctx) {
5495
6008
  if (block.info.bodyNode === null) {
5496
6009
  continue;
5497
6010
  }
5498
- const found = singletonMethodsByName(block.info.bodyNode).get(methodName);
6011
+ const found = singletonMethodsByName(
6012
+ block.info.bodyNode,
6013
+ ctx.bodyBlocks
6014
+ ).get(methodName);
5499
6015
  if (found !== void 0) {
5500
6016
  return followed(reachedMethod(found, block, methodName));
5501
6017
  }
@@ -5849,7 +6365,7 @@ function parameterName(param) {
5849
6365
 
5850
6366
  // src/reach/context.ts
5851
6367
  var METHOD_TYPES4 = /* @__PURE__ */ new Set(["method", "singleton_method"]);
5852
- async function buildReachContext(files, facts) {
6368
+ async function buildReachContext(files, facts, bodyBlocks = NO_BODY_BLOCKS, dynamicNames = /* @__PURE__ */ new Map()) {
5853
6369
  const blocksByQualifiedName = /* @__PURE__ */ new Map();
5854
6370
  const classes = [];
5855
6371
  const classNames = /* @__PURE__ */ new Map();
@@ -5923,7 +6439,9 @@ async function buildReachContext(files, facts) {
5923
6439
  topLevelMethods,
5924
6440
  facts,
5925
6441
  classNames,
5926
- definitions
6442
+ definitions,
6443
+ bodyBlocks,
6444
+ dynamicNames
5927
6445
  };
5928
6446
  }
5929
6447
  function namedChildren(node) {
@@ -5969,6 +6487,13 @@ function inheritedMethodsIn(packs) {
5969
6487
  }
5970
6488
  return found;
5971
6489
  }
6490
+ function associationCallsIn(packs) {
6491
+ return packs.flatMap(
6492
+ (pack) => (pack.storage ?? []).flatMap(
6493
+ (pattern) => pattern.associations === void 0 ? [] : [pattern.associations]
6494
+ )
6495
+ );
6496
+ }
5972
6497
  function emitStorageFacts(db, packs) {
5973
6498
  for (const pack of packs) {
5974
6499
  for (const pattern of pack.storage ?? []) {
@@ -6023,6 +6548,9 @@ async function extractRubyProject(options) {
6023
6548
  );
6024
6549
  const parsed = [];
6025
6550
  const definitions = /* @__PURE__ */ new Map();
6551
+ const associationCalls = associationCallsIn(options.packs);
6552
+ const bodyBlocks = bodyBlocksIn(options.packs);
6553
+ const inflections = inflectionsIn(options.packs);
6026
6554
  for (const file of options.files) {
6027
6555
  await timer.timeAsync("parse", async () => {
6028
6556
  const root = await cache.get(file);
@@ -6030,14 +6558,16 @@ async function extractRubyProject(options) {
6030
6558
  return;
6031
6559
  }
6032
6560
  parsed.push({ file, root });
6033
- emitValueFacts(db, file, root);
6561
+ emitValueFacts(db, file, root, bodyBlocks);
6034
6562
  for (const [key, method] of methodDefinitionsIn(file, root)) {
6035
6563
  definitions.set(key, method);
6036
6564
  }
6037
- constants.push(collectFileConstants(file, root));
6565
+ constants.push(
6566
+ collectFileConstants(file, root, associationCalls, inflections)
6567
+ );
6038
6568
  });
6039
6569
  }
6040
- timer.time("discover", () => {
6570
+ const dynamicNames = timer.time("discover", () => {
6041
6571
  emitConstantBindings(db, constants);
6042
6572
  const known = new Set(parsed.map(({ file }) => file));
6043
6573
  for (const { file, root } of parsed) {
@@ -6045,6 +6575,10 @@ async function extractRubyProject(options) {
6045
6575
  }
6046
6576
  bindEvaluator(db, { files: parsed, definitions });
6047
6577
  emitStorageFacts(db, options.packs);
6578
+ return readDynamicNames(
6579
+ db,
6580
+ new Map(parsed.map(({ file, root }) => [file, root]))
6581
+ );
6048
6582
  });
6049
6583
  const storagePatterns = options.packs.flatMap((pack) => pack.storage ?? []);
6050
6584
  const loaderPatterns = options.packs.flatMap((pack) => pack.loaders ?? []);
@@ -6052,7 +6586,7 @@ async function extractRubyProject(options) {
6052
6586
  const inheritedMethods = inheritedMethodsIn(options.packs);
6053
6587
  const reachContext = await timer.timeAsync(
6054
6588
  "discover",
6055
- () => buildReachContext(parsed, db)
6589
+ () => buildReachContext(parsed, db, bodyBlocks, dynamicNames)
6056
6590
  );
6057
6591
  const displayPathOf = (file) => options.workspaceRoot !== void 0 ? path4.relative(options.workspaceRoot, file) : file;
6058
6592
  const seeds = [];
@@ -6072,6 +6606,8 @@ async function extractRubyProject(options) {
6072
6606
  cache,
6073
6607
  ...storage === void 0 ? {} : { storage },
6074
6608
  inheritedMethods,
6609
+ bodyBlocks,
6610
+ dynamicNames,
6075
6611
  displayPathOf,
6076
6612
  facts: db,
6077
6613
  onReachSeed: (raw, seed) => seedByRaw.set(raw, seed)
@@ -6143,7 +6679,9 @@ async function extractRubyProject(options) {
6143
6679
  context: reachContext,
6144
6680
  displayPathOf,
6145
6681
  ...storage === void 0 ? {} : { storage },
6146
- inheritedMethods
6682
+ inheritedMethods,
6683
+ bodyBlocks,
6684
+ dynamicNames
6147
6685
  })
6148
6686
  );
6149
6687
  for (const entry of found) {