@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.cjs CHANGED
@@ -51,6 +51,7 @@ __export(index_exports, {
51
51
  inheritedStatements: () => inheritedStatements,
52
52
  methodDefinitionsIn: () => methodDefinitionsIn,
53
53
  methodInAncestry: () => methodInAncestry,
54
+ nestedStatements: () => nestedStatements,
54
55
  nodeId: () => nodeId,
55
56
  parseRuby: () => parseRuby,
56
57
  parseRubySync: () => parseRubySync,
@@ -94,6 +95,45 @@ function field(node, name) {
94
95
  function bodyStatements(body) {
95
96
  return body.namedChildren.filter((child) => child !== null);
96
97
  }
98
+ var BRANCH_FIELDS = {
99
+ if: ["consequence", "alternative"],
100
+ unless: ["consequence", "alternative"],
101
+ elsif: ["consequence", "alternative"],
102
+ if_modifier: ["body"],
103
+ unless_modifier: ["body"],
104
+ while: ["body"],
105
+ until: ["body"],
106
+ while_modifier: ["body"],
107
+ until_modifier: ["body"],
108
+ when: ["body"],
109
+ rescue: ["body"]
110
+ };
111
+ var STATEMENT_LISTS = /* @__PURE__ */ new Set(["then", "else", "do", "ensure", "begin"]);
112
+ var CLAUSE_TYPES = /* @__PURE__ */ new Set(["elsif", "else", "when", "rescue", "ensure"]);
113
+ function nestedStatements(stmt) {
114
+ const branchFields = BRANCH_FIELDS[stmt.type];
115
+ if (branchFields !== void 0) {
116
+ return branchFields.flatMap((name) => {
117
+ const branch = field(stmt, name);
118
+ return branch === null ? [] : statementsRunBy(branch);
119
+ });
120
+ }
121
+ if (STATEMENT_LISTS.has(stmt.type)) {
122
+ return bodyStatements(stmt).flatMap(
123
+ (child) => CLAUSE_TYPES.has(child.type) ? nestedStatements(child) : [child]
124
+ );
125
+ }
126
+ if (stmt.type === "case") {
127
+ return bodyStatements(stmt).filter((child) => CLAUSE_TYPES.has(child.type)).flatMap(nestedStatements);
128
+ }
129
+ return [];
130
+ }
131
+ function statementsRunBy(branch) {
132
+ if (STATEMENT_LISTS.has(branch.type) || CLAUSE_TYPES.has(branch.type)) {
133
+ return nestedStatements(branch);
134
+ }
135
+ return [branch];
136
+ }
97
137
  var OWN_BODY_TYPES = /* @__PURE__ */ new Set([
98
138
  "method",
99
139
  "singleton_method",
@@ -157,8 +197,8 @@ var VISIBILITY_KEYWORDS = /* @__PURE__ */ new Set([
157
197
  "protected",
158
198
  "public"
159
199
  ]);
160
- function visibilityKeyword(text) {
161
- return VISIBILITY_KEYWORDS.has(text) ? text : null;
200
+ function visibilityKeyword(text2) {
201
+ return VISIBILITY_KEYWORDS.has(text2) ? text2 : null;
162
202
  }
163
203
  function setVisibility(visibility, name, keyword) {
164
204
  if (keyword === "public") {
@@ -281,14 +321,31 @@ var NodeMap = class extends import_extractor.IdMap {
281
321
  // src/constantPath.ts
282
322
  var import_node_fs = __toESM(require("fs"), 1);
283
323
  var import_node_path = __toESM(require("path"), 1);
284
- function underscoreConstantPath(qualifiedName) {
285
- 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();
324
+ function underscoreConstantPath(qualifiedName, acronyms = []) {
325
+ 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();
326
+ }
327
+ function lowerAcronyms(word, acronyms) {
328
+ if (acronyms.length === 0) {
329
+ return word;
330
+ }
331
+ const alternatives = acronyms.map(escapeRegExp).join("|");
332
+ const acronymRegex = new RegExp(
333
+ `(?:(?<=([A-Za-z\\d]))|\\b)(${alternatives})(?=\\b|[^a-z])`,
334
+ "g"
335
+ );
336
+ return word.replace(
337
+ acronymRegex,
338
+ (_match, before, acronym) => `${before === void 0 ? "" : "_"}${acronym.toLowerCase()}`
339
+ );
340
+ }
341
+ function escapeRegExp(text2) {
342
+ return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
286
343
  }
287
344
  var PATH_CONVENTIONS = {
288
345
  railsUnderscore: underscoreConstantPath
289
346
  };
290
- function resolveConstantFile(root, qualifiedName, convention) {
291
- const relative = `${PATH_CONVENTIONS[convention](qualifiedName)}.rb`;
347
+ function resolveConstantFile(root, qualifiedName, convention, acronyms = []) {
348
+ const relative = `${PATH_CONVENTIONS[convention](qualifiedName, acronyms)}.rb`;
292
349
  for (const candidate of [root, ...autoloadDirectories(root)]) {
293
350
  const file = import_node_path.default.join(candidate, relative);
294
351
  if (import_node_fs.default.existsSync(file)) {
@@ -308,7 +365,7 @@ function autoloadDirectories(root) {
308
365
  // src/scope.ts
309
366
  function qualifyConstantRef(node, nesting) {
310
367
  if (node.type === "scope_resolution") {
311
- return node.text;
368
+ return compoundName(node);
312
369
  }
313
370
  if (node.type === "constant") {
314
371
  const innermost = nesting[0] ?? null;
@@ -318,13 +375,16 @@ function qualifyConstantRef(node, nesting) {
318
375
  }
319
376
  function constantRefCandidates(node, nesting) {
320
377
  if (node.type === "scope_resolution") {
321
- return [node.text];
378
+ return [compoundName(node)];
322
379
  }
323
380
  if (node.type === "constant") {
324
381
  return [...nesting.map((level) => `${level}::${node.text}`), node.text];
325
382
  }
326
383
  return [];
327
384
  }
385
+ function compoundName(node) {
386
+ return node.text.replace(/^::/, "");
387
+ }
328
388
  function shadowingClassFor(node, nesting, knownClasses) {
329
389
  if (node.type !== "constant") {
330
390
  return null;
@@ -359,12 +419,19 @@ function walkBody(statements, nesting, visit) {
359
419
  }
360
420
  }
361
421
  function ownIdentity(nameNode, outerNesting) {
362
- const qualifiedName = qualifyConstantRef(nameNode, outerNesting);
422
+ const qualifiedName = definedName(nameNode, outerNesting);
363
423
  if (qualifiedName === null) {
364
424
  return null;
365
425
  }
366
426
  return { qualifiedName, bodyNesting: [qualifiedName, ...outerNesting] };
367
427
  }
428
+ function definedName(nameNode, outerNesting) {
429
+ const innermost = outerNesting[0] ?? null;
430
+ if (nameNode.type === "scope_resolution" && innermost !== null && !nameNode.text.startsWith("::")) {
431
+ return `${innermost}::${nameNode.text}`;
432
+ }
433
+ return qualifyConstantRef(nameNode, outerNesting);
434
+ }
368
435
  function visitClass(node, nesting, visit) {
369
436
  const nameNode = field(node, "name");
370
437
  const identity = nameNode !== null ? ownIdentity(nameNode, nesting) : null;
@@ -428,7 +495,8 @@ async function reachDefinition(qualifiedName, lookup) {
428
495
  const filePath = resolveConstantFile(
429
496
  lookup.root,
430
497
  qualifiedName,
431
- lookup.pathConvention
498
+ lookup.pathConvention,
499
+ lookup.acronyms
432
500
  );
433
501
  if (filePath === null) {
434
502
  return null;
@@ -470,7 +538,10 @@ async function chainOf(self, lookup, active) {
470
538
  async function superclassChain(self, lookup, active) {
471
539
  const candidates = superclassCandidatesOf(self.blocks);
472
540
  for (const candidate of candidates) {
473
- if (lookup.ancestryRootClassNames.includes(candidate) || active.has(candidate)) {
541
+ if (lookup.ancestryRootClassNames.includes(candidate)) {
542
+ return [{ type: "root", name: candidate }];
543
+ }
544
+ if (active.has(candidate)) {
474
545
  return [];
475
546
  }
476
547
  const blocks = await definitionOf(candidate, lookup);
@@ -548,6 +619,9 @@ function superclassCandidatesOf(blocks) {
548
619
  }
549
620
  function methodInAncestry(ancestry, name) {
550
621
  for (const entry of ancestry) {
622
+ if (entry.type === "root") {
623
+ return { type: "none" };
624
+ }
551
625
  if (entry.type === "unfollowed") {
552
626
  return {
553
627
  type: "unsettled",
@@ -850,7 +924,7 @@ var Lowerer = class {
850
924
  return [nested];
851
925
  }
852
926
  /**
853
- * `unless ok` runs its body when `ok` is false, so it reads as an `if` on
927
+ * `unless ok` runs its body when `ok` is false, so it lowers to an `if` on
854
928
  * the same test with the two arms the other way around. Without the swap
855
929
  * a summary would say a branch runs on the test that skips it.
856
930
  */
@@ -866,7 +940,7 @@ var Lowerer = class {
866
940
  exitKind: exitKindOf(node)
867
941
  };
868
942
  }
869
- /** `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. */
943
+ /** `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. */
870
944
  lowerIfModifier(node) {
871
945
  const body = field(node, "body");
872
946
  const gated = body === null ? [] : [this.lower(body)];
@@ -2620,11 +2694,18 @@ function bindEvaluator(db, nodes) {
2620
2694
  )
2621
2695
  );
2622
2696
  }
2623
- function evaluatedValue(node, db) {
2624
- return (0, import_values2.force)(evaluatorFor(node, db).evaluate(node));
2697
+ function evaluatedValue(node, db, bindings) {
2698
+ const evaluator = evaluatorFor(node, db);
2699
+ if (bindings === void 0) {
2700
+ return (0, import_values2.force)(evaluator.evaluate(node));
2701
+ }
2702
+ const supplied = new Map(
2703
+ [...bindings].map(([name, value]) => [name, (0, import_values2.text)(value)])
2704
+ );
2705
+ return (0, import_values2.force)(evaluator.evaluate(node, { bindings: supplied }));
2625
2706
  }
2626
- function stringValueOf(node, db) {
2627
- return (0, import_values2.literalOf)(evaluatedValue(node, db));
2707
+ function stringValueOf(node, db, bindings) {
2708
+ return (0, import_values2.literalOf)(evaluatedValue(node, db, bindings));
2628
2709
  }
2629
2710
  function methodDefinitionsIn(file, root) {
2630
2711
  const found = /* @__PURE__ */ new Map();
@@ -3044,8 +3125,11 @@ function receiverPrefix(receiver, method, root, pattern, options) {
3044
3125
  function trimmed(path7) {
3045
3126
  return path7 === "/" ? "" : path7.replace(/\/+$/, "");
3046
3127
  }
3047
- function namesConstant(receiver, constantName) {
3048
- return (receiver.type === "constant" || receiver.type === "scope_resolution") && receiver.text === constantName;
3128
+ function namesConstant(receiver, constantName2) {
3129
+ if (receiver.type === "constant") {
3130
+ return receiver.text === constantName2;
3131
+ }
3132
+ return receiver.type === "scope_resolution" && compoundName(receiver) === constantName2;
3049
3133
  }
3050
3134
  function builtBy(name, method, root, pattern) {
3051
3135
  const builders = pattern.receiverBuilders ?? [];
@@ -3282,8 +3366,7 @@ function controllerFilters(pattern, ancestry) {
3282
3366
  skipNames.set(form.skippedBy, form.name);
3283
3367
  }
3284
3368
  }
3285
- const declared = [];
3286
- const skips = [];
3369
+ let declared = [];
3287
3370
  for (const { statement } of inheritedStatements(ancestry)) {
3288
3371
  const called = calledName(statement);
3289
3372
  if (called === null) {
@@ -3291,16 +3374,21 @@ function controllerFilters(pattern, ancestry) {
3291
3374
  }
3292
3375
  const form = byName.get(called);
3293
3376
  if (form !== void 0) {
3294
- declared.push(...declarationsOf(statement, form));
3377
+ for (const declaration of declarationsOf(statement, form)) {
3378
+ declared = [
3379
+ ...declared.filter((earlier) => !sameFilter(earlier, declaration)),
3380
+ declaration
3381
+ ];
3382
+ }
3295
3383
  continue;
3296
3384
  }
3297
3385
  const skipped = skipNames.get(called);
3298
3386
  if (skipped !== void 0) {
3299
- skips.push(...skipsOf(statement, skipped));
3387
+ declared = applySkips(declared, skipsOf(statement, skipped));
3300
3388
  }
3301
3389
  }
3302
3390
  const resolved = [];
3303
- for (const declaration of applySkips(declared, skips)) {
3391
+ for (const declaration of declared) {
3304
3392
  const found = methodInAncestry(ancestry, declaration.methodName);
3305
3393
  if (found.type !== "found") {
3306
3394
  continue;
@@ -3413,6 +3501,9 @@ function skipsOf(statement, filterName) {
3413
3501
  const actions = namesUnder(args.keyword.only);
3414
3502
  return args.positional.map((arg) => symbolValue(arg)).filter((name) => name !== null).map((methodName) => ({ filterName, methodName, actions }));
3415
3503
  }
3504
+ function sameFilter(one, other) {
3505
+ return one.filter.name === other.filter.name && one.methodName === other.methodName;
3506
+ }
3416
3507
  function applySkips(declared, skips) {
3417
3508
  const left = [];
3418
3509
  for (const declaration of declared) {
@@ -3477,6 +3568,12 @@ function rootConstant(node) {
3477
3568
  }
3478
3569
  return rootConstant(receiver);
3479
3570
  }
3571
+ function isConstant(node) {
3572
+ return node.type === "constant" || node.type === "scope_resolution";
3573
+ }
3574
+ function constantName(constant2) {
3575
+ return constant2.type === "constant" ? constant2.text : compoundName(constant2);
3576
+ }
3480
3577
  var ANCESTRY_PROGRAM = (0, import_datalog2.deriveOnDemand)(
3481
3578
  [...import_resolution3.RESOLUTION_RULES, ...import_resolution3.RESOLUTION_QUESTIONS],
3482
3579
  import_resolution3.ANSWER_RELATIONS
@@ -3486,13 +3583,22 @@ function reachesBase(facts, classKey, bases) {
3486
3583
  (0, import_datalog2.evaluate)(facts, ANCESTRY_PROGRAM.rules);
3487
3584
  return facts.lookup("wantedBaseName", 0, classKey).some((row) => bases.includes(String(row[1])));
3488
3585
  }
3489
- function classBehind(facts, constant2) {
3490
- const wanted = `#${constant2.text}`;
3586
+ function classBehind(facts, file, constant2) {
3587
+ const key = constant2.type === "constant" ? `${file}#${constant2.text}` : nodeId(file, constant2);
3491
3588
  const bound = new Set(
3492
- facts.facts("binds").filter((row) => String(row[0]).endsWith(wanted)).map((row) => String(row[1]))
3589
+ facts.lookup("binds", 0, key).map((row) => String(row[1]))
3493
3590
  );
3494
3591
  return bound.size === 1 ? [...bound][0] : void 0;
3495
3592
  }
3593
+ function modelPattern(constant2, file, options) {
3594
+ const classKey = classBehind(options.facts, file, constant2);
3595
+ if (classKey === void 0) {
3596
+ return void 0;
3597
+ }
3598
+ return options.patterns.find(
3599
+ (candidate) => reachesBase(options.facts, classKey, candidate.baseClasses)
3600
+ );
3601
+ }
3496
3602
  function selectorOf(node) {
3497
3603
  const picked = [];
3498
3604
  const walk = (current) => {
@@ -3514,7 +3620,87 @@ function selectorOf(node) {
3514
3620
  walk(node);
3515
3621
  return [...new Set(picked)];
3516
3622
  }
3517
- function storageEffects(calls, options) {
3623
+ function storageEffect(call, model, pattern, kind, selector) {
3624
+ const operation = methodOf(call);
3625
+ return {
3626
+ type: "interaction",
3627
+ binding: (0, import_ir_core.storageBinding)({
3628
+ recognition: "ruby-storage",
3629
+ storageSystem: pattern.storageSystem,
3630
+ scope: "default",
3631
+ container: constantName(model)
3632
+ }),
3633
+ callee: call.text,
3634
+ interaction: {
3635
+ class: "storage-access",
3636
+ kind,
3637
+ fields: [],
3638
+ operation,
3639
+ ...selector.length > 0 ? { selector } : {}
3640
+ }
3641
+ };
3642
+ }
3643
+ function modelCallEffects(call, file, options) {
3644
+ const constant2 = rootConstant(call);
3645
+ if (constant2 === null) {
3646
+ return [];
3647
+ }
3648
+ const pattern = modelPattern(constant2, file, options);
3649
+ if (pattern === void 0) {
3650
+ return [];
3651
+ }
3652
+ const kind = pattern.writes.includes(methodOf(call)) ? "write" : "read";
3653
+ return [storageEffect(call, constant2, pattern, kind, selectorOf(call))];
3654
+ }
3655
+ function isLoader(node, loader) {
3656
+ if (node === null) {
3657
+ return false;
3658
+ }
3659
+ if (node.type === "identifier") {
3660
+ return node.text === loader.loader;
3661
+ }
3662
+ return node.type === "call" && receiverOf(node) === null && methodOf(node) === loader.loader;
3663
+ }
3664
+ function loaderPick(call, loader) {
3665
+ const method = methodOf(call);
3666
+ const receiver = receiverOf(call);
3667
+ if (receiver === null) {
3668
+ return loader.shortcuts.includes(method) ? call : null;
3669
+ }
3670
+ if (!loader.reads.includes(method) || receiver.type !== "call") {
3671
+ return null;
3672
+ }
3673
+ const picks = methodOf(receiver) === loader.pick && isLoader(receiverOf(receiver), loader);
3674
+ return picks ? receiver : null;
3675
+ }
3676
+ function loaderCallEffects(call, file, options) {
3677
+ const effects = [];
3678
+ for (const loader of options.loaders ?? []) {
3679
+ const pick = loaderPick(call, loader);
3680
+ if (pick === null) {
3681
+ continue;
3682
+ }
3683
+ const args = field(pick, "arguments");
3684
+ for (const argument of args === null ? [] : children3(args)) {
3685
+ if (!isConstant(argument)) {
3686
+ continue;
3687
+ }
3688
+ const pattern = modelPattern(argument, file, options);
3689
+ if (pattern !== void 0) {
3690
+ effects.push(storageEffect(call, argument, pattern, "read", []));
3691
+ }
3692
+ }
3693
+ }
3694
+ return effects;
3695
+ }
3696
+ function effectsOfCall(call, file, options) {
3697
+ const onModel = modelCallEffects(call, file, options);
3698
+ return onModel.length > 0 ? onModel : loaderCallEffects(call, file, options);
3699
+ }
3700
+ function storageClaims(call, file, options) {
3701
+ return options.patterns.length > 0 && effectsOfCall(call, file, options).length > 0;
3702
+ }
3703
+ function storageEffects(calls, file, options) {
3518
3704
  if (options.patterns.length === 0) {
3519
3705
  return [];
3520
3706
  }
@@ -3530,39 +3716,7 @@ function storageEffects(calls, options) {
3530
3716
  if (partOfOne.has(call.id)) {
3531
3717
  continue;
3532
3718
  }
3533
- const constant2 = rootConstant(call);
3534
- if (constant2 === null) {
3535
- continue;
3536
- }
3537
- const classKey = classBehind(options.facts, constant2);
3538
- if (classKey === void 0) {
3539
- continue;
3540
- }
3541
- const pattern = options.patterns.find(
3542
- (candidate) => reachesBase(options.facts, classKey, candidate.baseClasses)
3543
- );
3544
- if (pattern === void 0) {
3545
- continue;
3546
- }
3547
- const operation = methodOf(call);
3548
- const picked = selectorOf(call);
3549
- effects.push({
3550
- type: "interaction",
3551
- binding: (0, import_ir_core.storageBinding)({
3552
- recognition: "ruby-storage",
3553
- storageSystem: pattern.storageSystem,
3554
- scope: pattern.baseClasses[0] ?? "",
3555
- container: constant2.text
3556
- }),
3557
- callee: call.text,
3558
- interaction: {
3559
- class: "storage-access",
3560
- kind: pattern.writes.includes(operation) ? "write" : "read",
3561
- fields: [],
3562
- operation,
3563
- ...picked.length > 0 ? { selector: picked } : {}
3564
- }
3565
- });
3719
+ effects.push(...effectsOfCall(call, file, options));
3566
3720
  }
3567
3721
  return effects;
3568
3722
  }
@@ -3604,7 +3758,7 @@ function typeShapeFromNode(node, ctx) {
3604
3758
  };
3605
3759
  }
3606
3760
  if (node.type === "scope_resolution") {
3607
- const qualified = node.text;
3761
+ const qualified = compoundName(node);
3608
3762
  const scalar = ctx.scalars[scalarLookupName(qualified, ctx.scalarNamePrefixes)];
3609
3763
  if (scalar !== void 0) {
3610
3764
  return scalar;
@@ -3641,6 +3795,7 @@ function fieldReadContext(pattern, cache, fileBlocks, bodyRead) {
3641
3795
  lookup: {
3642
3796
  root: pattern.root,
3643
3797
  pathConvention: pattern.pathConvention,
3798
+ acronyms: pattern.acronyms ?? [],
3644
3799
  ancestryRootClassNames: pattern.ancestryRootClassNames,
3645
3800
  parsedFile: (absPath) => cache.get(absPath),
3646
3801
  localDefinition: (name) => sameFileBlocks(name, fileBlocks)
@@ -3755,12 +3910,16 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
3755
3910
  const lookup = {
3756
3911
  root: pattern.root,
3757
3912
  pathConvention: pattern.pathConvention,
3913
+ acronyms: pattern.acronyms ?? [],
3758
3914
  ancestryRootClassNames: pattern.ancestryRootClassNames,
3759
3915
  parsedFile: (absPath) => options.cache.get(absPath),
3760
3916
  localDefinition: (name) => sameFileBlocks(name, fileBlocks)
3761
3917
  };
3762
3918
  const ancestry = await ancestryOf(info.qualifiedName, ownBlocks, lookup);
3763
- if (!reachesConfiguredBase(ancestry, info.qualifiedName, pattern.baseClassNames)) {
3919
+ if (!reachesConfiguredBase(ancestry, info.qualifiedName, [
3920
+ ...pattern.baseClassNames,
3921
+ ...pattern.ancestryRootClassNames
3922
+ ])) {
3764
3923
  return [];
3765
3924
  }
3766
3925
  const filters = controllerFilters(pattern, ancestry);
@@ -3771,7 +3930,7 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
3771
3930
  filter,
3772
3931
  pattern,
3773
3932
  displayPath,
3774
- bodyOfMethod(filter.method, options)
3933
+ bodyOfMethod(filter.method, filter.file, options)
3775
3934
  );
3776
3935
  units.push(raw);
3777
3936
  options.onReachSeed?.(raw, {
@@ -3780,47 +3939,77 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
3780
3939
  enclosingQualifiedName: filter.enclosingQualifiedName
3781
3940
  });
3782
3941
  }
3783
- for (const block of ownBlocks) {
3784
- if (block.info.bodyNode === null) {
3942
+ const emitAction = (actionName, method, block) => {
3943
+ const around = filters.filter((filter) => filterCoversAction(filter, actionName)).map(
3944
+ (filter) => filterReference(
3945
+ filter,
3946
+ options.displayPathOf?.(filter.file) ?? filter.file
3947
+ )
3948
+ );
3949
+ const raw = buildControllerActionUnit(
3950
+ pack,
3951
+ pattern,
3952
+ info.qualifiedName,
3953
+ actionName,
3954
+ method,
3955
+ {
3956
+ display: block.file === (options.absoluteFile ?? options.filePath) ? options.filePath : options.displayPathOf?.(block.file) ?? block.file,
3957
+ absolute: block.file
3958
+ },
3959
+ options,
3960
+ around
3961
+ );
3962
+ units.push(raw);
3963
+ options.onReachSeed?.(raw, {
3964
+ file: block.file,
3965
+ node: method,
3966
+ enclosingQualifiedName: block.info.qualifiedName
3967
+ });
3968
+ };
3969
+ const own = /* @__PURE__ */ new Set();
3970
+ for (const [actionName, method, block] of publicInstanceMethods(ownBlocks)) {
3971
+ own.add(actionName);
3972
+ emitAction(actionName, method, block);
3973
+ }
3974
+ const seen = new Set(own);
3975
+ for (const entry of ancestry) {
3976
+ if (entry.type !== "bodies" || entry.name === info.qualifiedName) {
3785
3977
  continue;
3786
3978
  }
3787
- const visibility = instanceMethodVisibility(block.info.bodyNode);
3788
- for (const [actionName, method] of instanceMethodsByName(
3789
- block.info.bodyNode
3979
+ for (const [actionName, method, block] of publicInstanceMethods(
3980
+ entry.blocks
3790
3981
  )) {
3791
- if ((visibility.get(actionName) ?? "public") !== "public") {
3982
+ if (seen.has(actionName)) {
3792
3983
  continue;
3793
3984
  }
3794
- const around = filters.filter((filter) => filterCoversAction(filter, actionName)).map(
3795
- (filter) => filterReference(
3796
- filter,
3797
- options.displayPathOf?.(filter.file) ?? filter.file
3798
- )
3799
- );
3800
- const raw = buildControllerActionUnit(
3801
- pack,
3802
- pattern,
3803
- info.qualifiedName,
3804
- actionName,
3805
- method,
3806
- options.filePath,
3807
- options,
3808
- around
3809
- );
3810
- units.push(raw);
3811
- options.onReachSeed?.(raw, {
3812
- file: block.file,
3813
- node: method,
3814
- enclosingQualifiedName: info.qualifiedName
3815
- });
3985
+ seen.add(actionName);
3986
+ if (pattern.routeFor(info.qualifiedName, actionName) === null) {
3987
+ continue;
3988
+ }
3989
+ emitAction(actionName, method, block);
3816
3990
  }
3817
3991
  }
3818
3992
  return units;
3819
3993
  }
3820
- function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actionName, method, filePath, bodyRead, wrappers = []) {
3994
+ function publicInstanceMethods(blocks) {
3995
+ const found = [];
3996
+ for (const block of blocks) {
3997
+ if (block.info.bodyNode === null) {
3998
+ continue;
3999
+ }
4000
+ const visibility = instanceMethodVisibility(block.info.bodyNode);
4001
+ for (const [name, method] of instanceMethodsByName(block.info.bodyNode)) {
4002
+ if ((visibility.get(name) ?? "public") === "public") {
4003
+ found.push([name, method, block]);
4004
+ }
4005
+ }
4006
+ }
4007
+ return found;
4008
+ }
4009
+ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actionName, method, file, bodyRead, wrappers = []) {
3821
4010
  const range = rangeOf(method);
3822
4011
  const route = pattern.routeFor(controllerQualifiedName, actionName);
3823
- const body = bodyOfMethod(method, bodyRead);
4012
+ const body = bodyOfMethod(method, file.absolute, bodyRead);
3824
4013
  const perResponse = responseBranches(
3825
4014
  method,
3826
4015
  pattern,
@@ -3832,7 +4021,7 @@ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actio
3832
4021
  name: actionName,
3833
4022
  nameKind: "binding",
3834
4023
  kind: "handler",
3835
- file: filePath,
4024
+ file: file.display,
3836
4025
  range,
3837
4026
  span: spanOf(method),
3838
4027
  exportName: actionName,
@@ -3951,12 +4140,12 @@ function branchesFor(body, range) {
3951
4140
  }
3952
4141
  ];
3953
4142
  }
3954
- function bodyOfMethod(method, bodyRead = {}) {
4143
+ function bodyOfMethod(method, file, bodyRead = {}) {
3955
4144
  const effects = invocationEffects(method, bodyRead.inheritedMethods);
3956
4145
  const storage = bodyRead.storage;
3957
4146
  const extra = [
3958
4147
  ...envReadEffects(method),
3959
- ...storage === void 0 ? [] : storageEffects(callsUnder2(method), storage)
4148
+ ...storage === void 0 ? [] : storageEffects(callsUnder2(method), file, storage)
3960
4149
  ];
3961
4150
  return {
3962
4151
  bodyContent: methodHasStatements(method) ? "statements" : "empty",
@@ -3987,7 +4176,7 @@ function methodNotSettled(reason, range) {
3987
4176
  function bodyFromLookup(found, range, subject, nothingThere, bodyRead) {
3988
4177
  const table = {
3989
4178
  found: (lookup) => ({
3990
- ...bodyOfMethod(lookup.method, bodyRead),
4179
+ ...bodyOfMethod(lookup.method, lookup.block.file, bodyRead),
3991
4180
  reachSeed: {
3992
4181
  file: lookup.block.file,
3993
4182
  node: lookup.method,
@@ -4351,11 +4540,10 @@ function collectFileConstants(filePath, root) {
4351
4540
  if ((node.type === "constant" || node.type === "scope_resolution") && !isDeclaration(node, parent)) {
4352
4541
  const written = writtenName(node);
4353
4542
  if (written !== null) {
4354
- references.push({
4355
- key: node.type === "constant" ? `${filePath}#${node.text}` : nodeId(filePath, node),
4356
- written,
4357
- nesting
4358
- });
4543
+ const key = node.type === "constant" ? `${filePath}#${node.text}` : nodeId(filePath, node);
4544
+ references.push(
4545
+ written.startsWith("::") ? { key, written: written.slice(2), nesting: [] } : { key, written, nesting }
4546
+ );
4359
4547
  }
4360
4548
  }
4361
4549
  for (const child of children4(node)) {
@@ -4916,7 +5104,8 @@ async function scanBody(source, ctx, options) {
4916
5104
  );
4917
5105
  if (outcome.kind === "stopped") {
4918
5106
  const stopKey = `${outcome.reason}:${callee}`;
4919
- if (!seen.has(stopKey) && (0, import_behavioral_ir4.worthRecording)(outcome.reason)) {
5107
+ const claimed = options.storage !== void 0 && storageClaims(call, source.file, options.storage);
5108
+ if (!seen.has(stopKey) && !claimed && (0, import_behavioral_ir4.worthRecording)(outcome.reason)) {
4920
5109
  seen.add(stopKey);
4921
5110
  stops.push({ callee, reason: outcome.reason });
4922
5111
  }
@@ -4947,7 +5136,7 @@ async function scanBody(source, ctx, options) {
4947
5136
  }
4948
5137
  function libraryUnit(target, options) {
4949
5138
  const { file, node, name, exportPath } = target;
4950
- const body = bodyOfMethod(node, options);
5139
+ const body = bodyOfMethod(node, file, options);
4951
5140
  const range = rangeOf(node);
4952
5141
  return {
4953
5142
  identity: {
@@ -5040,7 +5229,8 @@ function inheritedMethodsIn(packs) {
5040
5229
  return found;
5041
5230
  }
5042
5231
  function alreadyDiscovered(seen, raw) {
5043
- const key = `${raw.identity.file}::${raw.identity.name}::${raw.identity.range.start}`;
5232
+ const reported = raw.identity.exportPath?.join(".") ?? raw.identity.name;
5233
+ const key = `${raw.identity.file}::${reported}::${raw.identity.range.start}`;
5044
5234
  if (seen.has(key)) {
5045
5235
  return true;
5046
5236
  }
@@ -5104,6 +5294,8 @@ async function extractRubyProject(options) {
5104
5294
  bindEvaluator(db, { files: parsed, definitions });
5105
5295
  });
5106
5296
  const storagePatterns = options.packs.flatMap((pack) => pack.storage ?? []);
5297
+ const loaderPatterns = options.packs.flatMap((pack) => pack.loaders ?? []);
5298
+ const storage = storagePatterns.length > 0 ? { facts: db, patterns: storagePatterns, loaders: loaderPatterns } : void 0;
5107
5299
  const inheritedMethods = inheritedMethodsIn(options.packs);
5108
5300
  const displayPathOf = (file) => options.workspaceRoot !== void 0 ? import_node_path4.default.relative(options.workspaceRoot, file) : file;
5109
5301
  const seeds = [];
@@ -5119,7 +5311,7 @@ async function extractRubyProject(options) {
5119
5311
  filePath: displayPath,
5120
5312
  absoluteFile: file,
5121
5313
  cache,
5122
- ...storagePatterns.length > 0 ? { storage: { facts: db, patterns: storagePatterns } } : {},
5314
+ ...storage === void 0 ? {} : { storage },
5123
5315
  inheritedMethods,
5124
5316
  displayPathOf,
5125
5317
  facts: db,
@@ -5198,7 +5390,7 @@ async function extractRubyProject(options) {
5198
5390
  () => reachedFunctions(seeds, {
5199
5391
  files: parsed,
5200
5392
  displayPathOf,
5201
- ...storagePatterns.length > 0 ? { storage: { facts: db, patterns: storagePatterns } } : {},
5393
+ ...storage === void 0 ? {} : { storage },
5202
5394
  inheritedMethods
5203
5395
  })
5204
5396
  );
@@ -5337,8 +5529,8 @@ function displayNameOf(node) {
5337
5529
  if (node.type === "class" || node.type === "module") {
5338
5530
  return field(node, "name")?.text ?? node.text;
5339
5531
  }
5340
- const text = node.text.split("\n")[0].trim();
5341
- return text.length > 40 ? `${text.slice(0, 40)}...` : text;
5532
+ const text2 = node.text.split("\n")[0].trim();
5533
+ return text2.length > 40 ? `${text2.slice(0, 40)}...` : text2;
5342
5534
  }
5343
5535
  function width(node) {
5344
5536
  return node.endIndex - node.startIndex;
@@ -5365,14 +5557,14 @@ var RubyWhySession = class {
5365
5557
  * The smallest expression on `line` of `file` whose text is exactly
5366
5558
  * `text`, or null.
5367
5559
  */
5368
- findExpression(file, line, text) {
5560
+ findExpression(file, line, text2) {
5369
5561
  const root = this.rootOf(file);
5370
5562
  if (root === null) {
5371
5563
  return null;
5372
5564
  }
5373
5565
  let found = null;
5374
5566
  const visit = (node) => {
5375
- if (node.startPosition.row + 1 === line && node.text === text) {
5567
+ if (node.startPosition.row + 1 === line && node.text === text2) {
5376
5568
  if (found === null || width(node) <= width(found)) {
5377
5569
  found = node;
5378
5570
  }
@@ -5543,6 +5735,7 @@ async function rubyStubEvidence(options) {
5543
5735
  inheritedStatements,
5544
5736
  methodDefinitionsIn,
5545
5737
  methodInAncestry,
5738
+ nestedStatements,
5546
5739
  nodeId,
5547
5740
  parseRuby,
5548
5741
  parseRubySync,