@suss/adapter-ruby 0.25.0 → 0.27.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
@@ -15,6 +15,45 @@ function field(node, name) {
15
15
  function bodyStatements(body) {
16
16
  return body.namedChildren.filter((child) => child !== null);
17
17
  }
18
+ var BRANCH_FIELDS = {
19
+ if: ["consequence", "alternative"],
20
+ unless: ["consequence", "alternative"],
21
+ elsif: ["consequence", "alternative"],
22
+ if_modifier: ["body"],
23
+ unless_modifier: ["body"],
24
+ while: ["body"],
25
+ until: ["body"],
26
+ while_modifier: ["body"],
27
+ until_modifier: ["body"],
28
+ when: ["body"],
29
+ rescue: ["body"]
30
+ };
31
+ var STATEMENT_LISTS = /* @__PURE__ */ new Set(["then", "else", "do", "ensure", "begin"]);
32
+ var CLAUSE_TYPES = /* @__PURE__ */ new Set(["elsif", "else", "when", "rescue", "ensure"]);
33
+ function nestedStatements(stmt) {
34
+ const branchFields = BRANCH_FIELDS[stmt.type];
35
+ if (branchFields !== void 0) {
36
+ return branchFields.flatMap((name) => {
37
+ const branch = field(stmt, name);
38
+ return branch === null ? [] : statementsRunBy(branch);
39
+ });
40
+ }
41
+ if (STATEMENT_LISTS.has(stmt.type)) {
42
+ return bodyStatements(stmt).flatMap(
43
+ (child) => CLAUSE_TYPES.has(child.type) ? nestedStatements(child) : [child]
44
+ );
45
+ }
46
+ if (stmt.type === "case") {
47
+ return bodyStatements(stmt).filter((child) => CLAUSE_TYPES.has(child.type)).flatMap(nestedStatements);
48
+ }
49
+ return [];
50
+ }
51
+ function statementsRunBy(branch) {
52
+ if (STATEMENT_LISTS.has(branch.type) || CLAUSE_TYPES.has(branch.type)) {
53
+ return nestedStatements(branch);
54
+ }
55
+ return [branch];
56
+ }
18
57
  var OWN_BODY_TYPES = /* @__PURE__ */ new Set([
19
58
  "method",
20
59
  "singleton_method",
@@ -78,8 +117,8 @@ var VISIBILITY_KEYWORDS = /* @__PURE__ */ new Set([
78
117
  "protected",
79
118
  "public"
80
119
  ]);
81
- function visibilityKeyword(text) {
82
- return VISIBILITY_KEYWORDS.has(text) ? text : null;
120
+ function visibilityKeyword(text2) {
121
+ return VISIBILITY_KEYWORDS.has(text2) ? text2 : null;
83
122
  }
84
123
  function setVisibility(visibility, name, keyword) {
85
124
  if (keyword === "public") {
@@ -202,14 +241,31 @@ var NodeMap = class extends IdMap {
202
241
  // src/constantPath.ts
203
242
  import fs from "fs";
204
243
  import path from "path";
205
- function underscoreConstantPath(qualifiedName) {
206
- return qualifiedName.replaceAll("::", "/").replace(/([A-Z\d]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z\d])([A-Z])/g, "$1_$2").replaceAll("-", "_").toLowerCase();
244
+ function underscoreConstantPath(qualifiedName, acronyms = []) {
245
+ return lowerAcronyms(qualifiedName.replaceAll("::", "/"), acronyms).replace(/([A-Z\d]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z\d])([A-Z])/g, "$1_$2").replaceAll("-", "_").toLowerCase();
246
+ }
247
+ function lowerAcronyms(word, acronyms) {
248
+ if (acronyms.length === 0) {
249
+ return word;
250
+ }
251
+ const alternatives = acronyms.map(escapeRegExp).join("|");
252
+ const acronymRegex = new RegExp(
253
+ `(?:(?<=([A-Za-z\\d]))|\\b)(${alternatives})(?=\\b|[^a-z])`,
254
+ "g"
255
+ );
256
+ return word.replace(
257
+ acronymRegex,
258
+ (_match, before, acronym) => `${before === void 0 ? "" : "_"}${acronym.toLowerCase()}`
259
+ );
260
+ }
261
+ function escapeRegExp(text2) {
262
+ return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
207
263
  }
208
264
  var PATH_CONVENTIONS = {
209
265
  railsUnderscore: underscoreConstantPath
210
266
  };
211
- function resolveConstantFile(root, qualifiedName, convention) {
212
- const relative = `${PATH_CONVENTIONS[convention](qualifiedName)}.rb`;
267
+ function resolveConstantFile(root, qualifiedName, convention, acronyms = []) {
268
+ const relative = `${PATH_CONVENTIONS[convention](qualifiedName, acronyms)}.rb`;
213
269
  for (const candidate of [root, ...autoloadDirectories(root)]) {
214
270
  const file = path.join(candidate, relative);
215
271
  if (fs.existsSync(file)) {
@@ -229,7 +285,7 @@ function autoloadDirectories(root) {
229
285
  // src/scope.ts
230
286
  function qualifyConstantRef(node, nesting) {
231
287
  if (node.type === "scope_resolution") {
232
- return node.text;
288
+ return compoundName(node);
233
289
  }
234
290
  if (node.type === "constant") {
235
291
  const innermost = nesting[0] ?? null;
@@ -239,13 +295,16 @@ function qualifyConstantRef(node, nesting) {
239
295
  }
240
296
  function constantRefCandidates(node, nesting) {
241
297
  if (node.type === "scope_resolution") {
242
- return [node.text];
298
+ return [compoundName(node)];
243
299
  }
244
300
  if (node.type === "constant") {
245
301
  return [...nesting.map((level) => `${level}::${node.text}`), node.text];
246
302
  }
247
303
  return [];
248
304
  }
305
+ function compoundName(node) {
306
+ return node.text.replace(/^::/, "");
307
+ }
249
308
  function shadowingClassFor(node, nesting, knownClasses) {
250
309
  if (node.type !== "constant") {
251
310
  return null;
@@ -280,12 +339,19 @@ function walkBody(statements, nesting, visit) {
280
339
  }
281
340
  }
282
341
  function ownIdentity(nameNode, outerNesting) {
283
- const qualifiedName = qualifyConstantRef(nameNode, outerNesting);
342
+ const qualifiedName = definedName(nameNode, outerNesting);
284
343
  if (qualifiedName === null) {
285
344
  return null;
286
345
  }
287
346
  return { qualifiedName, bodyNesting: [qualifiedName, ...outerNesting] };
288
347
  }
348
+ function definedName(nameNode, outerNesting) {
349
+ const innermost = outerNesting[0] ?? null;
350
+ if (nameNode.type === "scope_resolution" && innermost !== null && !nameNode.text.startsWith("::")) {
351
+ return `${innermost}::${nameNode.text}`;
352
+ }
353
+ return qualifyConstantRef(nameNode, outerNesting);
354
+ }
289
355
  function visitClass(node, nesting, visit) {
290
356
  const nameNode = field(node, "name");
291
357
  const identity = nameNode !== null ? ownIdentity(nameNode, nesting) : null;
@@ -349,7 +415,8 @@ async function reachDefinition(qualifiedName, lookup) {
349
415
  const filePath = resolveConstantFile(
350
416
  lookup.root,
351
417
  qualifiedName,
352
- lookup.pathConvention
418
+ lookup.pathConvention,
419
+ lookup.acronyms
353
420
  );
354
421
  if (filePath === null) {
355
422
  return null;
@@ -391,7 +458,10 @@ async function chainOf(self, lookup, active) {
391
458
  async function superclassChain(self, lookup, active) {
392
459
  const candidates = superclassCandidatesOf(self.blocks);
393
460
  for (const candidate of candidates) {
394
- if (lookup.ancestryRootClassNames.includes(candidate) || active.has(candidate)) {
461
+ if (lookup.ancestryRootClassNames.includes(candidate)) {
462
+ return [{ type: "root", name: candidate }];
463
+ }
464
+ if (active.has(candidate)) {
395
465
  return [];
396
466
  }
397
467
  const blocks = await definitionOf(candidate, lookup);
@@ -469,6 +539,9 @@ function superclassCandidatesOf(blocks) {
469
539
  }
470
540
  function methodInAncestry(ancestry, name) {
471
541
  for (const entry of ancestry) {
542
+ if (entry.type === "root") {
543
+ return { type: "none" };
544
+ }
472
545
  if (entry.type === "unfollowed") {
473
546
  return {
474
547
  type: "unsettled",
@@ -775,7 +848,7 @@ var Lowerer = class {
775
848
  return [nested];
776
849
  }
777
850
  /**
778
- * `unless ok` runs its body when `ok` is false, so it reads as an `if` on
851
+ * `unless ok` runs its body when `ok` is false, so it lowers to an `if` on
779
852
  * the same test with the two arms the other way around. Without the swap
780
853
  * a summary would say a branch runs on the test that skips it.
781
854
  */
@@ -791,7 +864,7 @@ var Lowerer = class {
791
864
  exitKind: exitKindOf(node)
792
865
  };
793
866
  }
794
- /** `render :gone if expired?` gates one statement on one test, so it reads as an if with one arm, on whichever side the test puts it. */
867
+ /** `render :gone if expired?` gates one statement on one test, so it lowers to an if with one arm, on whichever side the test puts it. */
795
868
  lowerIfModifier(node) {
796
869
  const body = field(node, "body");
797
870
  const gated = body === null ? [] : [this.lower(body)];
@@ -1204,7 +1277,7 @@ function calledMethod(node) {
1204
1277
 
1205
1278
  // src/values/evaluator.ts
1206
1279
  import { nodeOfKey } from "@suss/resolution";
1207
- import { Evaluator, force, literalOf as literalOf2 } from "@suss/values";
1280
+ import { Evaluator, force, literalOf as literalOf2, text } from "@suss/values";
1208
1281
 
1209
1282
  // src/facts/resolve.ts
1210
1283
  import {
@@ -2582,11 +2655,18 @@ function bindEvaluator(db, nodes) {
2582
2655
  )
2583
2656
  );
2584
2657
  }
2585
- function evaluatedValue(node, db) {
2586
- return force(evaluatorFor(node, db).evaluate(node));
2658
+ function evaluatedValue(node, db, bindings) {
2659
+ const evaluator = evaluatorFor(node, db);
2660
+ if (bindings === void 0) {
2661
+ return force(evaluator.evaluate(node));
2662
+ }
2663
+ const supplied = new Map(
2664
+ [...bindings].map(([name, value]) => [name, text(value)])
2665
+ );
2666
+ return force(evaluator.evaluate(node, { bindings: supplied }));
2587
2667
  }
2588
- function stringValueOf(node, db) {
2589
- return literalOf2(evaluatedValue(node, db));
2668
+ function stringValueOf(node, db, bindings) {
2669
+ return literalOf2(evaluatedValue(node, db, bindings));
2590
2670
  }
2591
2671
  function methodDefinitionsIn(file, root) {
2592
2672
  const found = /* @__PURE__ */ new Map();
@@ -3006,8 +3086,11 @@ function receiverPrefix(receiver, method, root, pattern, options) {
3006
3086
  function trimmed(path7) {
3007
3087
  return path7 === "/" ? "" : path7.replace(/\/+$/, "");
3008
3088
  }
3009
- function namesConstant(receiver, constantName) {
3010
- return (receiver.type === "constant" || receiver.type === "scope_resolution") && receiver.text === constantName;
3089
+ function namesConstant(receiver, constantName2) {
3090
+ if (receiver.type === "constant") {
3091
+ return receiver.text === constantName2;
3092
+ }
3093
+ return receiver.type === "scope_resolution" && compoundName(receiver) === constantName2;
3011
3094
  }
3012
3095
  function builtBy(name, method, root, pattern) {
3013
3096
  const builders = pattern.receiverBuilders ?? [];
@@ -3244,8 +3327,7 @@ function controllerFilters(pattern, ancestry) {
3244
3327
  skipNames.set(form.skippedBy, form.name);
3245
3328
  }
3246
3329
  }
3247
- const declared = [];
3248
- const skips = [];
3330
+ let declared = [];
3249
3331
  for (const { statement } of inheritedStatements(ancestry)) {
3250
3332
  const called = calledName(statement);
3251
3333
  if (called === null) {
@@ -3253,16 +3335,21 @@ function controllerFilters(pattern, ancestry) {
3253
3335
  }
3254
3336
  const form = byName.get(called);
3255
3337
  if (form !== void 0) {
3256
- declared.push(...declarationsOf(statement, form));
3338
+ for (const declaration of declarationsOf(statement, form)) {
3339
+ declared = [
3340
+ ...declared.filter((earlier) => !sameFilter(earlier, declaration)),
3341
+ declaration
3342
+ ];
3343
+ }
3257
3344
  continue;
3258
3345
  }
3259
3346
  const skipped = skipNames.get(called);
3260
3347
  if (skipped !== void 0) {
3261
- skips.push(...skipsOf(statement, skipped));
3348
+ declared = applySkips(declared, skipsOf(statement, skipped));
3262
3349
  }
3263
3350
  }
3264
3351
  const resolved = [];
3265
- for (const declaration of applySkips(declared, skips)) {
3352
+ for (const declaration of declared) {
3266
3353
  const found = methodInAncestry(ancestry, declaration.methodName);
3267
3354
  if (found.type !== "found") {
3268
3355
  continue;
@@ -3375,6 +3462,9 @@ function skipsOf(statement, filterName) {
3375
3462
  const actions = namesUnder(args.keyword.only);
3376
3463
  return args.positional.map((arg) => symbolValue(arg)).filter((name) => name !== null).map((methodName) => ({ filterName, methodName, actions }));
3377
3464
  }
3465
+ function sameFilter(one, other) {
3466
+ return one.filter.name === other.filter.name && one.methodName === other.methodName;
3467
+ }
3378
3468
  function applySkips(declared, skips) {
3379
3469
  const left = [];
3380
3470
  for (const declaration of declared) {
@@ -3443,6 +3533,12 @@ function rootConstant(node) {
3443
3533
  }
3444
3534
  return rootConstant(receiver);
3445
3535
  }
3536
+ function isConstant(node) {
3537
+ return node.type === "constant" || node.type === "scope_resolution";
3538
+ }
3539
+ function constantName(constant2) {
3540
+ return constant2.type === "constant" ? constant2.text : compoundName(constant2);
3541
+ }
3446
3542
  var ANCESTRY_PROGRAM = deriveOnDemand2(
3447
3543
  [...RESOLUTION_RULES2, ...RESOLUTION_QUESTIONS2],
3448
3544
  ANSWER_RELATIONS2
@@ -3452,13 +3548,22 @@ function reachesBase(facts, classKey, bases) {
3452
3548
  evaluate2(facts, ANCESTRY_PROGRAM.rules);
3453
3549
  return facts.lookup("wantedBaseName", 0, classKey).some((row) => bases.includes(String(row[1])));
3454
3550
  }
3455
- function classBehind(facts, constant2) {
3456
- const wanted = `#${constant2.text}`;
3551
+ function classBehind(facts, file, constant2) {
3552
+ const key = constant2.type === "constant" ? `${file}#${constant2.text}` : nodeId(file, constant2);
3457
3553
  const bound = new Set(
3458
- facts.facts("binds").filter((row) => String(row[0]).endsWith(wanted)).map((row) => String(row[1]))
3554
+ facts.lookup("binds", 0, key).map((row) => String(row[1]))
3459
3555
  );
3460
3556
  return bound.size === 1 ? [...bound][0] : void 0;
3461
3557
  }
3558
+ function modelPattern(constant2, file, options) {
3559
+ const classKey = classBehind(options.facts, file, constant2);
3560
+ if (classKey === void 0) {
3561
+ return void 0;
3562
+ }
3563
+ return options.patterns.find(
3564
+ (candidate) => reachesBase(options.facts, classKey, candidate.baseClasses)
3565
+ );
3566
+ }
3462
3567
  function selectorOf(node) {
3463
3568
  const picked = [];
3464
3569
  const walk = (current) => {
@@ -3480,7 +3585,87 @@ function selectorOf(node) {
3480
3585
  walk(node);
3481
3586
  return [...new Set(picked)];
3482
3587
  }
3483
- function storageEffects(calls, options) {
3588
+ function storageEffect(call, model, pattern, kind, selector) {
3589
+ const operation = methodOf(call);
3590
+ return {
3591
+ type: "interaction",
3592
+ binding: storageBinding({
3593
+ recognition: "ruby-storage",
3594
+ storageSystem: pattern.storageSystem,
3595
+ scope: "default",
3596
+ container: constantName(model)
3597
+ }),
3598
+ callee: call.text,
3599
+ interaction: {
3600
+ class: "storage-access",
3601
+ kind,
3602
+ fields: [],
3603
+ operation,
3604
+ ...selector.length > 0 ? { selector } : {}
3605
+ }
3606
+ };
3607
+ }
3608
+ function modelCallEffects(call, file, options) {
3609
+ const constant2 = rootConstant(call);
3610
+ if (constant2 === null) {
3611
+ return [];
3612
+ }
3613
+ const pattern = modelPattern(constant2, file, options);
3614
+ if (pattern === void 0) {
3615
+ return [];
3616
+ }
3617
+ const kind = pattern.writes.includes(methodOf(call)) ? "write" : "read";
3618
+ return [storageEffect(call, constant2, pattern, kind, selectorOf(call))];
3619
+ }
3620
+ function isLoader(node, loader) {
3621
+ if (node === null) {
3622
+ return false;
3623
+ }
3624
+ if (node.type === "identifier") {
3625
+ return node.text === loader.loader;
3626
+ }
3627
+ return node.type === "call" && receiverOf(node) === null && methodOf(node) === loader.loader;
3628
+ }
3629
+ function loaderPick(call, loader) {
3630
+ const method = methodOf(call);
3631
+ const receiver = receiverOf(call);
3632
+ if (receiver === null) {
3633
+ return loader.shortcuts.includes(method) ? call : null;
3634
+ }
3635
+ if (!loader.reads.includes(method) || receiver.type !== "call") {
3636
+ return null;
3637
+ }
3638
+ const picks = methodOf(receiver) === loader.pick && isLoader(receiverOf(receiver), loader);
3639
+ return picks ? receiver : null;
3640
+ }
3641
+ function loaderCallEffects(call, file, options) {
3642
+ const effects = [];
3643
+ for (const loader of options.loaders ?? []) {
3644
+ const pick = loaderPick(call, loader);
3645
+ if (pick === null) {
3646
+ continue;
3647
+ }
3648
+ const args = field(pick, "arguments");
3649
+ for (const argument of args === null ? [] : children3(args)) {
3650
+ if (!isConstant(argument)) {
3651
+ continue;
3652
+ }
3653
+ const pattern = modelPattern(argument, file, options);
3654
+ if (pattern !== void 0) {
3655
+ effects.push(storageEffect(call, argument, pattern, "read", []));
3656
+ }
3657
+ }
3658
+ }
3659
+ return effects;
3660
+ }
3661
+ function effectsOfCall(call, file, options) {
3662
+ const onModel = modelCallEffects(call, file, options);
3663
+ return onModel.length > 0 ? onModel : loaderCallEffects(call, file, options);
3664
+ }
3665
+ function storageClaims(call, file, options) {
3666
+ return options.patterns.length > 0 && effectsOfCall(call, file, options).length > 0;
3667
+ }
3668
+ function storageEffects(calls, file, options) {
3484
3669
  if (options.patterns.length === 0) {
3485
3670
  return [];
3486
3671
  }
@@ -3496,39 +3681,7 @@ function storageEffects(calls, options) {
3496
3681
  if (partOfOne.has(call.id)) {
3497
3682
  continue;
3498
3683
  }
3499
- const constant2 = rootConstant(call);
3500
- if (constant2 === null) {
3501
- continue;
3502
- }
3503
- const classKey = classBehind(options.facts, constant2);
3504
- if (classKey === void 0) {
3505
- continue;
3506
- }
3507
- const pattern = options.patterns.find(
3508
- (candidate) => reachesBase(options.facts, classKey, candidate.baseClasses)
3509
- );
3510
- if (pattern === void 0) {
3511
- continue;
3512
- }
3513
- const operation = methodOf(call);
3514
- const picked = selectorOf(call);
3515
- effects.push({
3516
- type: "interaction",
3517
- binding: storageBinding({
3518
- recognition: "ruby-storage",
3519
- storageSystem: pattern.storageSystem,
3520
- scope: pattern.baseClasses[0] ?? "",
3521
- container: constant2.text
3522
- }),
3523
- callee: call.text,
3524
- interaction: {
3525
- class: "storage-access",
3526
- kind: pattern.writes.includes(operation) ? "write" : "read",
3527
- fields: [],
3528
- operation,
3529
- ...picked.length > 0 ? { selector: picked } : {}
3530
- }
3531
- });
3684
+ effects.push(...effectsOfCall(call, file, options));
3532
3685
  }
3533
3686
  return effects;
3534
3687
  }
@@ -3570,7 +3723,7 @@ function typeShapeFromNode(node, ctx) {
3570
3723
  };
3571
3724
  }
3572
3725
  if (node.type === "scope_resolution") {
3573
- const qualified = node.text;
3726
+ const qualified = compoundName(node);
3574
3727
  const scalar = ctx.scalars[scalarLookupName(qualified, ctx.scalarNamePrefixes)];
3575
3728
  if (scalar !== void 0) {
3576
3729
  return scalar;
@@ -3607,6 +3760,7 @@ function fieldReadContext(pattern, cache, fileBlocks, bodyRead) {
3607
3760
  lookup: {
3608
3761
  root: pattern.root,
3609
3762
  pathConvention: pattern.pathConvention,
3763
+ acronyms: pattern.acronyms ?? [],
3610
3764
  ancestryRootClassNames: pattern.ancestryRootClassNames,
3611
3765
  parsedFile: (absPath) => cache.get(absPath),
3612
3766
  localDefinition: (name) => sameFileBlocks(name, fileBlocks)
@@ -3721,12 +3875,16 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
3721
3875
  const lookup = {
3722
3876
  root: pattern.root,
3723
3877
  pathConvention: pattern.pathConvention,
3878
+ acronyms: pattern.acronyms ?? [],
3724
3879
  ancestryRootClassNames: pattern.ancestryRootClassNames,
3725
3880
  parsedFile: (absPath) => options.cache.get(absPath),
3726
3881
  localDefinition: (name) => sameFileBlocks(name, fileBlocks)
3727
3882
  };
3728
3883
  const ancestry = await ancestryOf(info.qualifiedName, ownBlocks, lookup);
3729
- if (!reachesConfiguredBase(ancestry, info.qualifiedName, pattern.baseClassNames)) {
3884
+ if (!reachesConfiguredBase(ancestry, info.qualifiedName, [
3885
+ ...pattern.baseClassNames,
3886
+ ...pattern.ancestryRootClassNames
3887
+ ])) {
3730
3888
  return [];
3731
3889
  }
3732
3890
  const filters = controllerFilters(pattern, ancestry);
@@ -3737,7 +3895,7 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
3737
3895
  filter,
3738
3896
  pattern,
3739
3897
  displayPath,
3740
- bodyOfMethod(filter.method, options)
3898
+ bodyOfMethod(filter.method, filter.file, options)
3741
3899
  );
3742
3900
  units.push(raw);
3743
3901
  options.onReachSeed?.(raw, {
@@ -3746,47 +3904,77 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
3746
3904
  enclosingQualifiedName: filter.enclosingQualifiedName
3747
3905
  });
3748
3906
  }
3749
- for (const block of ownBlocks) {
3750
- if (block.info.bodyNode === null) {
3907
+ const emitAction = (actionName, method, block) => {
3908
+ const around = filters.filter((filter) => filterCoversAction(filter, actionName)).map(
3909
+ (filter) => filterReference(
3910
+ filter,
3911
+ options.displayPathOf?.(filter.file) ?? filter.file
3912
+ )
3913
+ );
3914
+ const raw = buildControllerActionUnit(
3915
+ pack,
3916
+ pattern,
3917
+ info.qualifiedName,
3918
+ actionName,
3919
+ method,
3920
+ {
3921
+ display: block.file === (options.absoluteFile ?? options.filePath) ? options.filePath : options.displayPathOf?.(block.file) ?? block.file,
3922
+ absolute: block.file
3923
+ },
3924
+ options,
3925
+ around
3926
+ );
3927
+ units.push(raw);
3928
+ options.onReachSeed?.(raw, {
3929
+ file: block.file,
3930
+ node: method,
3931
+ enclosingQualifiedName: block.info.qualifiedName
3932
+ });
3933
+ };
3934
+ const own = /* @__PURE__ */ new Set();
3935
+ for (const [actionName, method, block] of publicInstanceMethods(ownBlocks)) {
3936
+ own.add(actionName);
3937
+ emitAction(actionName, method, block);
3938
+ }
3939
+ const seen = new Set(own);
3940
+ for (const entry of ancestry) {
3941
+ if (entry.type !== "bodies" || entry.name === info.qualifiedName) {
3751
3942
  continue;
3752
3943
  }
3753
- const visibility = instanceMethodVisibility(block.info.bodyNode);
3754
- for (const [actionName, method] of instanceMethodsByName(
3755
- block.info.bodyNode
3944
+ for (const [actionName, method, block] of publicInstanceMethods(
3945
+ entry.blocks
3756
3946
  )) {
3757
- if ((visibility.get(actionName) ?? "public") !== "public") {
3947
+ if (seen.has(actionName)) {
3758
3948
  continue;
3759
3949
  }
3760
- const around = filters.filter((filter) => filterCoversAction(filter, actionName)).map(
3761
- (filter) => filterReference(
3762
- filter,
3763
- options.displayPathOf?.(filter.file) ?? filter.file
3764
- )
3765
- );
3766
- const raw = buildControllerActionUnit(
3767
- pack,
3768
- pattern,
3769
- info.qualifiedName,
3770
- actionName,
3771
- method,
3772
- options.filePath,
3773
- options,
3774
- around
3775
- );
3776
- units.push(raw);
3777
- options.onReachSeed?.(raw, {
3778
- file: block.file,
3779
- node: method,
3780
- enclosingQualifiedName: info.qualifiedName
3781
- });
3950
+ seen.add(actionName);
3951
+ if (pattern.routeFor(info.qualifiedName, actionName) === null) {
3952
+ continue;
3953
+ }
3954
+ emitAction(actionName, method, block);
3782
3955
  }
3783
3956
  }
3784
3957
  return units;
3785
3958
  }
3786
- function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actionName, method, filePath, bodyRead, wrappers = []) {
3959
+ function publicInstanceMethods(blocks) {
3960
+ const found = [];
3961
+ for (const block of blocks) {
3962
+ if (block.info.bodyNode === null) {
3963
+ continue;
3964
+ }
3965
+ const visibility = instanceMethodVisibility(block.info.bodyNode);
3966
+ for (const [name, method] of instanceMethodsByName(block.info.bodyNode)) {
3967
+ if ((visibility.get(name) ?? "public") === "public") {
3968
+ found.push([name, method, block]);
3969
+ }
3970
+ }
3971
+ }
3972
+ return found;
3973
+ }
3974
+ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actionName, method, file, bodyRead, wrappers = []) {
3787
3975
  const range = rangeOf(method);
3788
3976
  const route = pattern.routeFor(controllerQualifiedName, actionName);
3789
- const body = bodyOfMethod(method, bodyRead);
3977
+ const body = bodyOfMethod(method, file.absolute, bodyRead);
3790
3978
  const perResponse = responseBranches(
3791
3979
  method,
3792
3980
  pattern,
@@ -3798,7 +3986,7 @@ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actio
3798
3986
  name: actionName,
3799
3987
  nameKind: "binding",
3800
3988
  kind: "handler",
3801
- file: filePath,
3989
+ file: file.display,
3802
3990
  range,
3803
3991
  span: spanOf(method),
3804
3992
  exportName: actionName,
@@ -3917,12 +4105,12 @@ function branchesFor(body, range) {
3917
4105
  }
3918
4106
  ];
3919
4107
  }
3920
- function bodyOfMethod(method, bodyRead = {}) {
4108
+ function bodyOfMethod(method, file, bodyRead = {}) {
3921
4109
  const effects = invocationEffects(method, bodyRead.inheritedMethods);
3922
4110
  const storage = bodyRead.storage;
3923
4111
  const extra = [
3924
4112
  ...envReadEffects(method),
3925
- ...storage === void 0 ? [] : storageEffects(callsUnder2(method), storage)
4113
+ ...storage === void 0 ? [] : storageEffects(callsUnder2(method), file, storage)
3926
4114
  ];
3927
4115
  return {
3928
4116
  bodyContent: methodHasStatements(method) ? "statements" : "empty",
@@ -3953,7 +4141,7 @@ function methodNotSettled(reason, range) {
3953
4141
  function bodyFromLookup(found, range, subject, nothingThere, bodyRead) {
3954
4142
  const table = {
3955
4143
  found: (lookup) => ({
3956
- ...bodyOfMethod(lookup.method, bodyRead),
4144
+ ...bodyOfMethod(lookup.method, lookup.block.file, bodyRead),
3957
4145
  reachSeed: {
3958
4146
  file: lookup.block.file,
3959
4147
  node: lookup.method,
@@ -4317,11 +4505,10 @@ function collectFileConstants(filePath, root) {
4317
4505
  if ((node.type === "constant" || node.type === "scope_resolution") && !isDeclaration(node, parent)) {
4318
4506
  const written = writtenName(node);
4319
4507
  if (written !== null) {
4320
- references.push({
4321
- key: node.type === "constant" ? `${filePath}#${node.text}` : nodeId(filePath, node),
4322
- written,
4323
- nesting
4324
- });
4508
+ const key = node.type === "constant" ? `${filePath}#${node.text}` : nodeId(filePath, node);
4509
+ references.push(
4510
+ written.startsWith("::") ? { key, written: written.slice(2), nesting: [] } : { key, written, nesting }
4511
+ );
4325
4512
  }
4326
4513
  }
4327
4514
  for (const child of children4(node)) {
@@ -4913,7 +5100,8 @@ async function scanBody(source, ctx, options) {
4913
5100
  );
4914
5101
  if (outcome.kind === "stopped") {
4915
5102
  const stopKey = `${outcome.reason}:${callee}`;
4916
- if (!seen.has(stopKey) && worthRecording(outcome.reason)) {
5103
+ const claimed = options.storage !== void 0 && storageClaims(call, source.file, options.storage);
5104
+ if (!seen.has(stopKey) && !claimed && worthRecording(outcome.reason)) {
4917
5105
  seen.add(stopKey);
4918
5106
  stops.push({ callee, reason: outcome.reason });
4919
5107
  }
@@ -4944,7 +5132,7 @@ async function scanBody(source, ctx, options) {
4944
5132
  }
4945
5133
  function libraryUnit(target, options) {
4946
5134
  const { file, node, name, exportPath } = target;
4947
- const body = bodyOfMethod(node, options);
5135
+ const body = bodyOfMethod(node, file, options);
4948
5136
  const range = rangeOf(node);
4949
5137
  return {
4950
5138
  identity: {
@@ -5037,7 +5225,8 @@ function inheritedMethodsIn(packs) {
5037
5225
  return found;
5038
5226
  }
5039
5227
  function alreadyDiscovered(seen, raw) {
5040
- const key = `${raw.identity.file}::${raw.identity.name}::${raw.identity.range.start}`;
5228
+ const reported = raw.identity.exportPath?.join(".") ?? raw.identity.name;
5229
+ const key = `${raw.identity.file}::${reported}::${raw.identity.range.start}`;
5041
5230
  if (seen.has(key)) {
5042
5231
  return true;
5043
5232
  }
@@ -5101,6 +5290,8 @@ async function extractRubyProject(options) {
5101
5290
  bindEvaluator(db, { files: parsed, definitions });
5102
5291
  });
5103
5292
  const storagePatterns = options.packs.flatMap((pack) => pack.storage ?? []);
5293
+ const loaderPatterns = options.packs.flatMap((pack) => pack.loaders ?? []);
5294
+ const storage = storagePatterns.length > 0 ? { facts: db, patterns: storagePatterns, loaders: loaderPatterns } : void 0;
5104
5295
  const inheritedMethods = inheritedMethodsIn(options.packs);
5105
5296
  const displayPathOf = (file) => options.workspaceRoot !== void 0 ? path4.relative(options.workspaceRoot, file) : file;
5106
5297
  const seeds = [];
@@ -5116,7 +5307,7 @@ async function extractRubyProject(options) {
5116
5307
  filePath: displayPath,
5117
5308
  absoluteFile: file,
5118
5309
  cache,
5119
- ...storagePatterns.length > 0 ? { storage: { facts: db, patterns: storagePatterns } } : {},
5310
+ ...storage === void 0 ? {} : { storage },
5120
5311
  inheritedMethods,
5121
5312
  displayPathOf,
5122
5313
  facts: db,
@@ -5195,7 +5386,7 @@ async function extractRubyProject(options) {
5195
5386
  () => reachedFunctions(seeds, {
5196
5387
  files: parsed,
5197
5388
  displayPathOf,
5198
- ...storagePatterns.length > 0 ? { storage: { facts: db, patterns: storagePatterns } } : {},
5389
+ ...storage === void 0 ? {} : { storage },
5199
5390
  inheritedMethods
5200
5391
  })
5201
5392
  );
@@ -5334,8 +5525,8 @@ function displayNameOf(node) {
5334
5525
  if (node.type === "class" || node.type === "module") {
5335
5526
  return field(node, "name")?.text ?? node.text;
5336
5527
  }
5337
- const text = node.text.split("\n")[0].trim();
5338
- return text.length > 40 ? `${text.slice(0, 40)}...` : text;
5528
+ const text2 = node.text.split("\n")[0].trim();
5529
+ return text2.length > 40 ? `${text2.slice(0, 40)}...` : text2;
5339
5530
  }
5340
5531
  function width(node) {
5341
5532
  return node.endIndex - node.startIndex;
@@ -5362,14 +5553,14 @@ var RubyWhySession = class {
5362
5553
  * The smallest expression on `line` of `file` whose text is exactly
5363
5554
  * `text`, or null.
5364
5555
  */
5365
- findExpression(file, line, text) {
5556
+ findExpression(file, line, text2) {
5366
5557
  const root = this.rootOf(file);
5367
5558
  if (root === null) {
5368
5559
  return null;
5369
5560
  }
5370
5561
  let found = null;
5371
5562
  const visit = (node) => {
5372
- if (node.startPosition.row + 1 === line && node.text === text) {
5563
+ if (node.startPosition.row + 1 === line && node.text === text2) {
5373
5564
  if (found === null || width(node) <= width(found)) {
5374
5565
  found = node;
5375
5566
  }
@@ -5539,6 +5730,7 @@ export {
5539
5730
  inheritedStatements,
5540
5731
  methodDefinitionsIn,
5541
5732
  methodInAncestry,
5733
+ nestedStatements,
5542
5734
  nodeId,
5543
5735
  parseRuby,
5544
5736
  parseRubySync,