@suss/adapter-ruby 0.26.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)) {
@@ -362,12 +419,19 @@ function walkBody(statements, nesting, visit) {
362
419
  }
363
420
  }
364
421
  function ownIdentity(nameNode, outerNesting) {
365
- const qualifiedName = qualifyConstantRef(nameNode, outerNesting);
422
+ const qualifiedName = definedName(nameNode, outerNesting);
366
423
  if (qualifiedName === null) {
367
424
  return null;
368
425
  }
369
426
  return { qualifiedName, bodyNesting: [qualifiedName, ...outerNesting] };
370
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
+ }
371
435
  function visitClass(node, nesting, visit) {
372
436
  const nameNode = field(node, "name");
373
437
  const identity = nameNode !== null ? ownIdentity(nameNode, nesting) : null;
@@ -431,7 +495,8 @@ async function reachDefinition(qualifiedName, lookup) {
431
495
  const filePath = resolveConstantFile(
432
496
  lookup.root,
433
497
  qualifiedName,
434
- lookup.pathConvention
498
+ lookup.pathConvention,
499
+ lookup.acronyms
435
500
  );
436
501
  if (filePath === null) {
437
502
  return null;
@@ -473,7 +538,10 @@ async function chainOf(self, lookup, active) {
473
538
  async function superclassChain(self, lookup, active) {
474
539
  const candidates = superclassCandidatesOf(self.blocks);
475
540
  for (const candidate of candidates) {
476
- 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)) {
477
545
  return [];
478
546
  }
479
547
  const blocks = await definitionOf(candidate, lookup);
@@ -551,6 +619,9 @@ function superclassCandidatesOf(blocks) {
551
619
  }
552
620
  function methodInAncestry(ancestry, name) {
553
621
  for (const entry of ancestry) {
622
+ if (entry.type === "root") {
623
+ return { type: "none" };
624
+ }
554
625
  if (entry.type === "unfollowed") {
555
626
  return {
556
627
  type: "unsettled",
@@ -2623,11 +2694,18 @@ function bindEvaluator(db, nodes) {
2623
2694
  )
2624
2695
  );
2625
2696
  }
2626
- function evaluatedValue(node, db) {
2627
- 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 }));
2628
2706
  }
2629
- function stringValueOf(node, db) {
2630
- return (0, import_values2.literalOf)(evaluatedValue(node, db));
2707
+ function stringValueOf(node, db, bindings) {
2708
+ return (0, import_values2.literalOf)(evaluatedValue(node, db, bindings));
2631
2709
  }
2632
2710
  function methodDefinitionsIn(file, root) {
2633
2711
  const found = /* @__PURE__ */ new Map();
@@ -3288,8 +3366,7 @@ function controllerFilters(pattern, ancestry) {
3288
3366
  skipNames.set(form.skippedBy, form.name);
3289
3367
  }
3290
3368
  }
3291
- const declared = [];
3292
- const skips = [];
3369
+ let declared = [];
3293
3370
  for (const { statement } of inheritedStatements(ancestry)) {
3294
3371
  const called = calledName(statement);
3295
3372
  if (called === null) {
@@ -3297,16 +3374,21 @@ function controllerFilters(pattern, ancestry) {
3297
3374
  }
3298
3375
  const form = byName.get(called);
3299
3376
  if (form !== void 0) {
3300
- 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
+ }
3301
3383
  continue;
3302
3384
  }
3303
3385
  const skipped = skipNames.get(called);
3304
3386
  if (skipped !== void 0) {
3305
- skips.push(...skipsOf(statement, skipped));
3387
+ declared = applySkips(declared, skipsOf(statement, skipped));
3306
3388
  }
3307
3389
  }
3308
3390
  const resolved = [];
3309
- for (const declaration of applySkips(declared, skips)) {
3391
+ for (const declaration of declared) {
3310
3392
  const found = methodInAncestry(ancestry, declaration.methodName);
3311
3393
  if (found.type !== "found") {
3312
3394
  continue;
@@ -3419,6 +3501,9 @@ function skipsOf(statement, filterName) {
3419
3501
  const actions = namesUnder(args.keyword.only);
3420
3502
  return args.positional.map((arg) => symbolValue(arg)).filter((name) => name !== null).map((methodName) => ({ filterName, methodName, actions }));
3421
3503
  }
3504
+ function sameFilter(one, other) {
3505
+ return one.filter.name === other.filter.name && one.methodName === other.methodName;
3506
+ }
3422
3507
  function applySkips(declared, skips) {
3423
3508
  const left = [];
3424
3509
  for (const declaration of declared) {
@@ -3542,7 +3627,7 @@ function storageEffect(call, model, pattern, kind, selector) {
3542
3627
  binding: (0, import_ir_core.storageBinding)({
3543
3628
  recognition: "ruby-storage",
3544
3629
  storageSystem: pattern.storageSystem,
3545
- scope: pattern.baseClasses[0] ?? "",
3630
+ scope: "default",
3546
3631
  container: constantName(model)
3547
3632
  }),
3548
3633
  callee: call.text,
@@ -3710,6 +3795,7 @@ function fieldReadContext(pattern, cache, fileBlocks, bodyRead) {
3710
3795
  lookup: {
3711
3796
  root: pattern.root,
3712
3797
  pathConvention: pattern.pathConvention,
3798
+ acronyms: pattern.acronyms ?? [],
3713
3799
  ancestryRootClassNames: pattern.ancestryRootClassNames,
3714
3800
  parsedFile: (absPath) => cache.get(absPath),
3715
3801
  localDefinition: (name) => sameFileBlocks(name, fileBlocks)
@@ -3824,12 +3910,16 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
3824
3910
  const lookup = {
3825
3911
  root: pattern.root,
3826
3912
  pathConvention: pattern.pathConvention,
3913
+ acronyms: pattern.acronyms ?? [],
3827
3914
  ancestryRootClassNames: pattern.ancestryRootClassNames,
3828
3915
  parsedFile: (absPath) => options.cache.get(absPath),
3829
3916
  localDefinition: (name) => sameFileBlocks(name, fileBlocks)
3830
3917
  };
3831
3918
  const ancestry = await ancestryOf(info.qualifiedName, ownBlocks, lookup);
3832
- if (!reachesConfiguredBase(ancestry, info.qualifiedName, pattern.baseClassNames)) {
3919
+ if (!reachesConfiguredBase(ancestry, info.qualifiedName, [
3920
+ ...pattern.baseClassNames,
3921
+ ...pattern.ancestryRootClassNames
3922
+ ])) {
3833
3923
  return [];
3834
3924
  }
3835
3925
  const filters = controllerFilters(pattern, ancestry);
@@ -3849,43 +3939,73 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
3849
3939
  enclosingQualifiedName: filter.enclosingQualifiedName
3850
3940
  });
3851
3941
  }
3852
- for (const block of ownBlocks) {
3853
- 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) {
3854
3977
  continue;
3855
3978
  }
3856
- const visibility = instanceMethodVisibility(block.info.bodyNode);
3857
- for (const [actionName, method] of instanceMethodsByName(
3858
- block.info.bodyNode
3979
+ for (const [actionName, method, block] of publicInstanceMethods(
3980
+ entry.blocks
3859
3981
  )) {
3860
- if ((visibility.get(actionName) ?? "public") !== "public") {
3982
+ if (seen.has(actionName)) {
3861
3983
  continue;
3862
3984
  }
3863
- const around = filters.filter((filter) => filterCoversAction(filter, actionName)).map(
3864
- (filter) => filterReference(
3865
- filter,
3866
- options.displayPathOf?.(filter.file) ?? filter.file
3867
- )
3868
- );
3869
- const raw = buildControllerActionUnit(
3870
- pack,
3871
- pattern,
3872
- info.qualifiedName,
3873
- actionName,
3874
- method,
3875
- { display: options.filePath, absolute: block.file },
3876
- options,
3877
- around
3878
- );
3879
- units.push(raw);
3880
- options.onReachSeed?.(raw, {
3881
- file: block.file,
3882
- node: method,
3883
- enclosingQualifiedName: info.qualifiedName
3884
- });
3985
+ seen.add(actionName);
3986
+ if (pattern.routeFor(info.qualifiedName, actionName) === null) {
3987
+ continue;
3988
+ }
3989
+ emitAction(actionName, method, block);
3885
3990
  }
3886
3991
  }
3887
3992
  return units;
3888
3993
  }
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
+ }
3889
4009
  function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actionName, method, file, bodyRead, wrappers = []) {
3890
4010
  const range = rangeOf(method);
3891
4011
  const route = pattern.routeFor(controllerQualifiedName, actionName);
@@ -5109,7 +5229,8 @@ function inheritedMethodsIn(packs) {
5109
5229
  return found;
5110
5230
  }
5111
5231
  function alreadyDiscovered(seen, raw) {
5112
- 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}`;
5113
5234
  if (seen.has(key)) {
5114
5235
  return true;
5115
5236
  }
@@ -5408,8 +5529,8 @@ function displayNameOf(node) {
5408
5529
  if (node.type === "class" || node.type === "module") {
5409
5530
  return field(node, "name")?.text ?? node.text;
5410
5531
  }
5411
- const text = node.text.split("\n")[0].trim();
5412
- 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;
5413
5534
  }
5414
5535
  function width(node) {
5415
5536
  return node.endIndex - node.startIndex;
@@ -5436,14 +5557,14 @@ var RubyWhySession = class {
5436
5557
  * The smallest expression on `line` of `file` whose text is exactly
5437
5558
  * `text`, or null.
5438
5559
  */
5439
- findExpression(file, line, text) {
5560
+ findExpression(file, line, text2) {
5440
5561
  const root = this.rootOf(file);
5441
5562
  if (root === null) {
5442
5563
  return null;
5443
5564
  }
5444
5565
  let found = null;
5445
5566
  const visit = (node) => {
5446
- if (node.startPosition.row + 1 === line && node.text === text) {
5567
+ if (node.startPosition.row + 1 === line && node.text === text2) {
5447
5568
  if (found === null || width(node) <= width(found)) {
5448
5569
  found = node;
5449
5570
  }
@@ -5614,6 +5735,7 @@ async function rubyStubEvidence(options) {
5614
5735
  inheritedStatements,
5615
5736
  methodDefinitionsIn,
5616
5737
  methodInAncestry,
5738
+ nestedStatements,
5617
5739
  nodeId,
5618
5740
  parseRuby,
5619
5741
  parseRubySync,
package/dist/index.d.cts CHANGED
@@ -31,6 +31,13 @@ interface Range {
31
31
  }
32
32
  declare function field(node: Node, name: string): Node | null;
33
33
  declare function bodyStatements(body: Node): Node[];
34
+ /**
35
+ * Every statement written directly inside a branching statement's
36
+ * branches, across its `elsif`, `else`, `when`, `rescue` and `ensure`
37
+ * clauses. A statement inside a nested branching statement is not
38
+ * included; the caller descends into that one itself.
39
+ */
40
+ declare function nestedStatements(stmt: Node): Node[];
34
41
  /** A call whose block is the thing being configured rather than a place statements run. */
35
42
  type BlockConfigures = (call: Node) => boolean;
36
43
  /**
@@ -68,8 +75,14 @@ declare function readCallArgs(argumentList: Node | null): CallArgs;
68
75
  */
69
76
  /** A pack picks a convention by name, and the code for each one lives here. */
70
77
  type ConstantPathConvention = "railsUnderscore";
71
- /** Ported from ActiveSupport's own `String#underscore`, which is what Rails autoloading runs a constant path through. */
72
- declare function underscoreConstantPath(qualifiedName: string): string;
78
+ /**
79
+ * Ported from ActiveSupport's own `String#underscore`, which is what
80
+ * Rails autoloading runs a constant path through. An acronym the
81
+ * project registers with the inflector is one word to it, so with
82
+ * `ActivityPub` registered the path is `activitypub`, not
83
+ * `activity_pub`.
84
+ */
85
+ declare function underscoreConstantPath(qualifiedName: string, acronyms?: readonly string[]): string;
73
86
  /**
74
87
  * Null when there is no file at that path. Rails autoloads from every
75
88
  * directory directly under `app`, so `ApplicationController` is
@@ -77,14 +90,14 @@ declare function underscoreConstantPath(qualifiedName: string): string;
77
90
  * `app/application_controller.rb`; those directories are tried after
78
91
  * the root itself, in name order, and no other spelling is tried.
79
92
  */
80
- declare function resolveConstantFile(root: string, qualifiedName: string, convention: ConstantPathConvention): string | null;
93
+ declare function resolveConstantFile(root: string, qualifiedName: string, convention: ConstantPathConvention, acronyms?: readonly string[]): string | null;
81
94
 
82
95
  /**
83
96
  * Resolves Ruby constant paths against lexical nesting.
84
97
  *
85
- * A compound path written out, like `Data::Record`, means the same thing
86
- * wherever it appears, so its text is the qualified name. A bare name is
87
- * relative to the nesting it is written inside. `require` is never resolved,
98
+ * A compound path referenced, like `Data::Record`, is read as absolute. A
99
+ * bare name is relative to the nesting it is written inside, and so is a
100
+ * compound name `class` or `module` opens. `require` is never resolved,
88
101
  * because Rails autoloads by naming convention rather than through a load graph
89
102
  * a static reader could follow, so a constant that nesting alone cannot qualify
90
103
  * keeps whatever name the source wrote and callers treat it as unresolved.
@@ -141,7 +154,13 @@ interface ReachedBody {
141
154
  /** Absolute path of the file the block is written in. */
142
155
  file: string;
143
156
  }
144
- /** One ancestor: every block reopening its name, or the name alone when nothing reached it. Blocks stay together because Ruby treats a reopened class as one place in the chain. */
157
+ /**
158
+ * One ancestor: every block reopening its name, the name alone when
159
+ * nothing reached it, or the library's own class the walk stopped at.
160
+ * Blocks stay together because Ruby treats a reopened class as one
161
+ * place in the chain. The root is kept by name so a caller can tell a
162
+ * chain that ended at the library from one that ended at nothing.
163
+ */
145
164
  type AncestorEntry = {
146
165
  type: "bodies";
147
166
  name: string;
@@ -149,6 +168,9 @@ type AncestorEntry = {
149
168
  } | {
150
169
  type: "unfollowed";
151
170
  name: string;
171
+ } | {
172
+ type: "root";
173
+ name: string;
152
174
  };
153
175
  /** A class and everything it inherits from, in Ruby's own method-lookup order. */
154
176
  type Ancestry = readonly AncestorEntry[];
@@ -157,6 +179,8 @@ interface AncestorLookup {
157
179
  /** Directory the constant-to-path convention resolves an ancestor's name against. */
158
180
  root: string;
159
181
  pathConvention: ConstantPathConvention;
182
+ /** Acronyms the convention keeps as one word, `ActivityPub` to `activitypub`. */
183
+ acronyms?: readonly string[];
160
184
  /**
161
185
  * The library's own classes a project's chain ends at. Reaching one
162
186
  * ends a walk with nothing left unfollowed.
@@ -324,15 +348,17 @@ interface RbLoaderPattern {
324
348
  shortcuts: string[];
325
349
  }
326
350
  type RubyDiscoveryPattern = GraphqlObjectFields | ControllerActions;
327
- /** A class whose ancestry reaches one of `baseClassNames` is a controller, and every instance method it defines directly is one of its actions, bound by `routeFor` to the method and path a project's own routing gives it, or discovered with no boundary binding when `routeFor` finds none. */
351
+ /** A class whose ancestry reaches one of `baseClassNames` or `ancestryRootClassNames` is a controller, and every instance method it defines directly is one of its actions, bound by `routeFor` to the method and path a project's own routing gives it, or discovered with no boundary binding when `routeFor` finds none. */
328
352
  interface ControllerActions {
329
353
  type: "controllerActions";
330
- /** The library's own base a project's controllers extend, `ActionController::Base` say. */
354
+ /** A base a project's controllers extend, `ApplicationController` say. The base itself is not a controller. */
331
355
  baseClassNames: string[];
332
356
  /** The directory a bare superclass name is looked up under, the project's own layout. */
333
357
  root: string;
334
358
  pathConvention: ConstantPathConvention;
335
- /** The library's own classes a project's controller chain ends at. */
359
+ /** Acronyms the project registers with the inflector, which the path convention keeps as one word: `ActivityPub` is `activitypub`, not `activity_pub`. */
360
+ acronyms?: string[];
361
+ /** The library's own classes a project's controller chain ends at. A class extending one directly is a controller too. */
336
362
  ancestryRootClassNames: string[];
337
363
  /** Status code a wire response gets when the action does not say otherwise. */
338
364
  defaultStatusCode: number;
@@ -412,6 +438,8 @@ interface GraphqlObjectFields {
412
438
  /** The directory a wiring keyword's referenced class is looked up under. That is the project's own layout, not the library's. */
413
439
  root: string;
414
440
  pathConvention: ConstantPathConvention;
441
+ /** Acronyms the project registers with the inflector, which the path convention keeps as one word. */
442
+ acronyms?: string[];
415
443
  /** The DSL call declaring one schema field in an object type's body. */
416
444
  fieldCallName: string;
417
445
  /** The DSL call declaring a referenced class's own return type. */
@@ -754,10 +782,12 @@ interface ProjectNodes {
754
782
  }
755
783
  /** Register the parsed project, so reads through `db` can follow the facts back to nodes. */
756
784
  declare function bindEvaluator(db: Database, nodes: ProjectNodes): void;
785
+ /** Strings to read for the parameters of the block or method `node` is written in, the way a caller would supply them. */
786
+ type ParameterBindings = ReadonlyMap<string, string>;
757
787
  /** The abstract value `node` comes down to, through the facts when `db` was bound. */
758
- declare function evaluatedValue(node: Node, db?: Database): Value;
788
+ declare function evaluatedValue(node: Node, db?: Database, bindings?: ParameterBindings): Value;
759
789
  /** The one string `node` comes down to, or null when it does not settle on one. */
760
- declare function stringValueOf(node: Node, db?: Database): string | null;
790
+ declare function stringValueOf(node: Node, db?: Database, bindings?: ParameterBindings): string | null;
761
791
  /** Every method a file defines, keyed the way the facts key it, for `bindEvaluator`. */
762
792
  declare function methodDefinitionsIn(file: string, root: Node): Map<string, Node>;
763
793
 
@@ -771,4 +801,4 @@ declare function methodDefinitionsIn(file: string, root: Node): Map<string, Node
771
801
  */
772
802
  declare const ADAPTER_VERSION = "0.1.0";
773
803
 
774
- export { ADAPTER_VERSION, type AncestorEntry, type AncestorLookup, type Ancestry, type CallArgs, type ClassInfo, type ConstantPathConvention, type ControllerActions, type DiscoveryOptions, type EvaluatedFile, type ExtractRubyOptions, type ExtractRubyResult, type FileCache, type FileConstants, type GraphqlObjectFields, type GraphqlTypeNameConvention, type MethodLookup, type ProjectNodes, type Range, type RbStatusCall, type RbStoragePattern, type ReachedBody, type RubyDiscoveryPattern, type RubyExtendsSite, type RubyPack, type RubyRequireSite, type RubyStubEvidence, type RubyStubEvidenceOptions, type RubyValueHandle, RubyWhySession, type RubyWhySessionOptions, type TypeReadContext, ancestryOf, bindEvaluator, bodyStatements, booleanLiteralValue, collectFileConstants, createFileCache, discoverUnits, emitConstantBindings, emitEntryFact, emitValueFacts, evaluatedValue, extractRubyProject, field, findRubyFiles, graphqlTypeNameFromQualified, hashKeySymbolName, inheritedStatements, methodDefinitionsIn, methodInAncestry, nodeId, parseRuby, parseRubySync, preloadRubyGrammar, qualifyConstantRef, reachDefinition, readCallArgs, readKey, resolveConstantFile, rubyStubEvidence, runStatements, stringLiteralValue, stringValueOf, symbolValue, typeShapeFromNode, underscoreConstantPath, unitKey, walkClasses, walkDefinitions };
804
+ export { ADAPTER_VERSION, type AncestorEntry, type AncestorLookup, type Ancestry, type CallArgs, type ClassInfo, type ConstantPathConvention, type ControllerActions, type DiscoveryOptions, type EvaluatedFile, type ExtractRubyOptions, type ExtractRubyResult, type FileCache, type FileConstants, type GraphqlObjectFields, type GraphqlTypeNameConvention, type MethodLookup, type ParameterBindings, type ProjectNodes, type Range, type RbStatusCall, type RbStoragePattern, type ReachedBody, type RubyDiscoveryPattern, type RubyExtendsSite, type RubyPack, type RubyRequireSite, type RubyStubEvidence, type RubyStubEvidenceOptions, type RubyValueHandle, RubyWhySession, type RubyWhySessionOptions, type TypeReadContext, ancestryOf, bindEvaluator, bodyStatements, booleanLiteralValue, collectFileConstants, createFileCache, discoverUnits, emitConstantBindings, emitEntryFact, emitValueFacts, evaluatedValue, extractRubyProject, field, findRubyFiles, graphqlTypeNameFromQualified, hashKeySymbolName, inheritedStatements, methodDefinitionsIn, methodInAncestry, nestedStatements, nodeId, parseRuby, parseRubySync, preloadRubyGrammar, qualifyConstantRef, reachDefinition, readCallArgs, readKey, resolveConstantFile, rubyStubEvidence, runStatements, stringLiteralValue, stringValueOf, symbolValue, typeShapeFromNode, underscoreConstantPath, unitKey, walkClasses, walkDefinitions };
package/dist/index.d.ts CHANGED
@@ -31,6 +31,13 @@ interface Range {
31
31
  }
32
32
  declare function field(node: Node, name: string): Node | null;
33
33
  declare function bodyStatements(body: Node): Node[];
34
+ /**
35
+ * Every statement written directly inside a branching statement's
36
+ * branches, across its `elsif`, `else`, `when`, `rescue` and `ensure`
37
+ * clauses. A statement inside a nested branching statement is not
38
+ * included; the caller descends into that one itself.
39
+ */
40
+ declare function nestedStatements(stmt: Node): Node[];
34
41
  /** A call whose block is the thing being configured rather than a place statements run. */
35
42
  type BlockConfigures = (call: Node) => boolean;
36
43
  /**
@@ -68,8 +75,14 @@ declare function readCallArgs(argumentList: Node | null): CallArgs;
68
75
  */
69
76
  /** A pack picks a convention by name, and the code for each one lives here. */
70
77
  type ConstantPathConvention = "railsUnderscore";
71
- /** Ported from ActiveSupport's own `String#underscore`, which is what Rails autoloading runs a constant path through. */
72
- declare function underscoreConstantPath(qualifiedName: string): string;
78
+ /**
79
+ * Ported from ActiveSupport's own `String#underscore`, which is what
80
+ * Rails autoloading runs a constant path through. An acronym the
81
+ * project registers with the inflector is one word to it, so with
82
+ * `ActivityPub` registered the path is `activitypub`, not
83
+ * `activity_pub`.
84
+ */
85
+ declare function underscoreConstantPath(qualifiedName: string, acronyms?: readonly string[]): string;
73
86
  /**
74
87
  * Null when there is no file at that path. Rails autoloads from every
75
88
  * directory directly under `app`, so `ApplicationController` is
@@ -77,14 +90,14 @@ declare function underscoreConstantPath(qualifiedName: string): string;
77
90
  * `app/application_controller.rb`; those directories are tried after
78
91
  * the root itself, in name order, and no other spelling is tried.
79
92
  */
80
- declare function resolveConstantFile(root: string, qualifiedName: string, convention: ConstantPathConvention): string | null;
93
+ declare function resolveConstantFile(root: string, qualifiedName: string, convention: ConstantPathConvention, acronyms?: readonly string[]): string | null;
81
94
 
82
95
  /**
83
96
  * Resolves Ruby constant paths against lexical nesting.
84
97
  *
85
- * A compound path written out, like `Data::Record`, means the same thing
86
- * wherever it appears, so its text is the qualified name. A bare name is
87
- * relative to the nesting it is written inside. `require` is never resolved,
98
+ * A compound path referenced, like `Data::Record`, is read as absolute. A
99
+ * bare name is relative to the nesting it is written inside, and so is a
100
+ * compound name `class` or `module` opens. `require` is never resolved,
88
101
  * because Rails autoloads by naming convention rather than through a load graph
89
102
  * a static reader could follow, so a constant that nesting alone cannot qualify
90
103
  * keeps whatever name the source wrote and callers treat it as unresolved.
@@ -141,7 +154,13 @@ interface ReachedBody {
141
154
  /** Absolute path of the file the block is written in. */
142
155
  file: string;
143
156
  }
144
- /** One ancestor: every block reopening its name, or the name alone when nothing reached it. Blocks stay together because Ruby treats a reopened class as one place in the chain. */
157
+ /**
158
+ * One ancestor: every block reopening its name, the name alone when
159
+ * nothing reached it, or the library's own class the walk stopped at.
160
+ * Blocks stay together because Ruby treats a reopened class as one
161
+ * place in the chain. The root is kept by name so a caller can tell a
162
+ * chain that ended at the library from one that ended at nothing.
163
+ */
145
164
  type AncestorEntry = {
146
165
  type: "bodies";
147
166
  name: string;
@@ -149,6 +168,9 @@ type AncestorEntry = {
149
168
  } | {
150
169
  type: "unfollowed";
151
170
  name: string;
171
+ } | {
172
+ type: "root";
173
+ name: string;
152
174
  };
153
175
  /** A class and everything it inherits from, in Ruby's own method-lookup order. */
154
176
  type Ancestry = readonly AncestorEntry[];
@@ -157,6 +179,8 @@ interface AncestorLookup {
157
179
  /** Directory the constant-to-path convention resolves an ancestor's name against. */
158
180
  root: string;
159
181
  pathConvention: ConstantPathConvention;
182
+ /** Acronyms the convention keeps as one word, `ActivityPub` to `activitypub`. */
183
+ acronyms?: readonly string[];
160
184
  /**
161
185
  * The library's own classes a project's chain ends at. Reaching one
162
186
  * ends a walk with nothing left unfollowed.
@@ -324,15 +348,17 @@ interface RbLoaderPattern {
324
348
  shortcuts: string[];
325
349
  }
326
350
  type RubyDiscoveryPattern = GraphqlObjectFields | ControllerActions;
327
- /** A class whose ancestry reaches one of `baseClassNames` is a controller, and every instance method it defines directly is one of its actions, bound by `routeFor` to the method and path a project's own routing gives it, or discovered with no boundary binding when `routeFor` finds none. */
351
+ /** A class whose ancestry reaches one of `baseClassNames` or `ancestryRootClassNames` is a controller, and every instance method it defines directly is one of its actions, bound by `routeFor` to the method and path a project's own routing gives it, or discovered with no boundary binding when `routeFor` finds none. */
328
352
  interface ControllerActions {
329
353
  type: "controllerActions";
330
- /** The library's own base a project's controllers extend, `ActionController::Base` say. */
354
+ /** A base a project's controllers extend, `ApplicationController` say. The base itself is not a controller. */
331
355
  baseClassNames: string[];
332
356
  /** The directory a bare superclass name is looked up under, the project's own layout. */
333
357
  root: string;
334
358
  pathConvention: ConstantPathConvention;
335
- /** The library's own classes a project's controller chain ends at. */
359
+ /** Acronyms the project registers with the inflector, which the path convention keeps as one word: `ActivityPub` is `activitypub`, not `activity_pub`. */
360
+ acronyms?: string[];
361
+ /** The library's own classes a project's controller chain ends at. A class extending one directly is a controller too. */
336
362
  ancestryRootClassNames: string[];
337
363
  /** Status code a wire response gets when the action does not say otherwise. */
338
364
  defaultStatusCode: number;
@@ -412,6 +438,8 @@ interface GraphqlObjectFields {
412
438
  /** The directory a wiring keyword's referenced class is looked up under. That is the project's own layout, not the library's. */
413
439
  root: string;
414
440
  pathConvention: ConstantPathConvention;
441
+ /** Acronyms the project registers with the inflector, which the path convention keeps as one word. */
442
+ acronyms?: string[];
415
443
  /** The DSL call declaring one schema field in an object type's body. */
416
444
  fieldCallName: string;
417
445
  /** The DSL call declaring a referenced class's own return type. */
@@ -754,10 +782,12 @@ interface ProjectNodes {
754
782
  }
755
783
  /** Register the parsed project, so reads through `db` can follow the facts back to nodes. */
756
784
  declare function bindEvaluator(db: Database, nodes: ProjectNodes): void;
785
+ /** Strings to read for the parameters of the block or method `node` is written in, the way a caller would supply them. */
786
+ type ParameterBindings = ReadonlyMap<string, string>;
757
787
  /** The abstract value `node` comes down to, through the facts when `db` was bound. */
758
- declare function evaluatedValue(node: Node, db?: Database): Value;
788
+ declare function evaluatedValue(node: Node, db?: Database, bindings?: ParameterBindings): Value;
759
789
  /** The one string `node` comes down to, or null when it does not settle on one. */
760
- declare function stringValueOf(node: Node, db?: Database): string | null;
790
+ declare function stringValueOf(node: Node, db?: Database, bindings?: ParameterBindings): string | null;
761
791
  /** Every method a file defines, keyed the way the facts key it, for `bindEvaluator`. */
762
792
  declare function methodDefinitionsIn(file: string, root: Node): Map<string, Node>;
763
793
 
@@ -771,4 +801,4 @@ declare function methodDefinitionsIn(file: string, root: Node): Map<string, Node
771
801
  */
772
802
  declare const ADAPTER_VERSION = "0.1.0";
773
803
 
774
- export { ADAPTER_VERSION, type AncestorEntry, type AncestorLookup, type Ancestry, type CallArgs, type ClassInfo, type ConstantPathConvention, type ControllerActions, type DiscoveryOptions, type EvaluatedFile, type ExtractRubyOptions, type ExtractRubyResult, type FileCache, type FileConstants, type GraphqlObjectFields, type GraphqlTypeNameConvention, type MethodLookup, type ProjectNodes, type Range, type RbStatusCall, type RbStoragePattern, type ReachedBody, type RubyDiscoveryPattern, type RubyExtendsSite, type RubyPack, type RubyRequireSite, type RubyStubEvidence, type RubyStubEvidenceOptions, type RubyValueHandle, RubyWhySession, type RubyWhySessionOptions, type TypeReadContext, ancestryOf, bindEvaluator, bodyStatements, booleanLiteralValue, collectFileConstants, createFileCache, discoverUnits, emitConstantBindings, emitEntryFact, emitValueFacts, evaluatedValue, extractRubyProject, field, findRubyFiles, graphqlTypeNameFromQualified, hashKeySymbolName, inheritedStatements, methodDefinitionsIn, methodInAncestry, nodeId, parseRuby, parseRubySync, preloadRubyGrammar, qualifyConstantRef, reachDefinition, readCallArgs, readKey, resolveConstantFile, rubyStubEvidence, runStatements, stringLiteralValue, stringValueOf, symbolValue, typeShapeFromNode, underscoreConstantPath, unitKey, walkClasses, walkDefinitions };
804
+ export { ADAPTER_VERSION, type AncestorEntry, type AncestorLookup, type Ancestry, type CallArgs, type ClassInfo, type ConstantPathConvention, type ControllerActions, type DiscoveryOptions, type EvaluatedFile, type ExtractRubyOptions, type ExtractRubyResult, type FileCache, type FileConstants, type GraphqlObjectFields, type GraphqlTypeNameConvention, type MethodLookup, type ParameterBindings, type ProjectNodes, type Range, type RbStatusCall, type RbStoragePattern, type ReachedBody, type RubyDiscoveryPattern, type RubyExtendsSite, type RubyPack, type RubyRequireSite, type RubyStubEvidence, type RubyStubEvidenceOptions, type RubyValueHandle, RubyWhySession, type RubyWhySessionOptions, type TypeReadContext, ancestryOf, bindEvaluator, bodyStatements, booleanLiteralValue, collectFileConstants, createFileCache, discoverUnits, emitConstantBindings, emitEntryFact, emitValueFacts, evaluatedValue, extractRubyProject, field, findRubyFiles, graphqlTypeNameFromQualified, hashKeySymbolName, inheritedStatements, methodDefinitionsIn, methodInAncestry, nestedStatements, nodeId, parseRuby, parseRubySync, preloadRubyGrammar, qualifyConstantRef, reachDefinition, readCallArgs, readKey, resolveConstantFile, rubyStubEvidence, runStatements, stringLiteralValue, stringValueOf, symbolValue, typeShapeFromNode, underscoreConstantPath, unitKey, walkClasses, walkDefinitions };
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)) {
@@ -283,12 +339,19 @@ function walkBody(statements, nesting, visit) {
283
339
  }
284
340
  }
285
341
  function ownIdentity(nameNode, outerNesting) {
286
- const qualifiedName = qualifyConstantRef(nameNode, outerNesting);
342
+ const qualifiedName = definedName(nameNode, outerNesting);
287
343
  if (qualifiedName === null) {
288
344
  return null;
289
345
  }
290
346
  return { qualifiedName, bodyNesting: [qualifiedName, ...outerNesting] };
291
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
+ }
292
355
  function visitClass(node, nesting, visit) {
293
356
  const nameNode = field(node, "name");
294
357
  const identity = nameNode !== null ? ownIdentity(nameNode, nesting) : null;
@@ -352,7 +415,8 @@ async function reachDefinition(qualifiedName, lookup) {
352
415
  const filePath = resolveConstantFile(
353
416
  lookup.root,
354
417
  qualifiedName,
355
- lookup.pathConvention
418
+ lookup.pathConvention,
419
+ lookup.acronyms
356
420
  );
357
421
  if (filePath === null) {
358
422
  return null;
@@ -394,7 +458,10 @@ async function chainOf(self, lookup, active) {
394
458
  async function superclassChain(self, lookup, active) {
395
459
  const candidates = superclassCandidatesOf(self.blocks);
396
460
  for (const candidate of candidates) {
397
- 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)) {
398
465
  return [];
399
466
  }
400
467
  const blocks = await definitionOf(candidate, lookup);
@@ -472,6 +539,9 @@ function superclassCandidatesOf(blocks) {
472
539
  }
473
540
  function methodInAncestry(ancestry, name) {
474
541
  for (const entry of ancestry) {
542
+ if (entry.type === "root") {
543
+ return { type: "none" };
544
+ }
475
545
  if (entry.type === "unfollowed") {
476
546
  return {
477
547
  type: "unsettled",
@@ -1207,7 +1277,7 @@ function calledMethod(node) {
1207
1277
 
1208
1278
  // src/values/evaluator.ts
1209
1279
  import { nodeOfKey } from "@suss/resolution";
1210
- import { Evaluator, force, literalOf as literalOf2 } from "@suss/values";
1280
+ import { Evaluator, force, literalOf as literalOf2, text } from "@suss/values";
1211
1281
 
1212
1282
  // src/facts/resolve.ts
1213
1283
  import {
@@ -2585,11 +2655,18 @@ function bindEvaluator(db, nodes) {
2585
2655
  )
2586
2656
  );
2587
2657
  }
2588
- function evaluatedValue(node, db) {
2589
- 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 }));
2590
2667
  }
2591
- function stringValueOf(node, db) {
2592
- return literalOf2(evaluatedValue(node, db));
2668
+ function stringValueOf(node, db, bindings) {
2669
+ return literalOf2(evaluatedValue(node, db, bindings));
2593
2670
  }
2594
2671
  function methodDefinitionsIn(file, root) {
2595
2672
  const found = /* @__PURE__ */ new Map();
@@ -3250,8 +3327,7 @@ function controllerFilters(pattern, ancestry) {
3250
3327
  skipNames.set(form.skippedBy, form.name);
3251
3328
  }
3252
3329
  }
3253
- const declared = [];
3254
- const skips = [];
3330
+ let declared = [];
3255
3331
  for (const { statement } of inheritedStatements(ancestry)) {
3256
3332
  const called = calledName(statement);
3257
3333
  if (called === null) {
@@ -3259,16 +3335,21 @@ function controllerFilters(pattern, ancestry) {
3259
3335
  }
3260
3336
  const form = byName.get(called);
3261
3337
  if (form !== void 0) {
3262
- 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
+ }
3263
3344
  continue;
3264
3345
  }
3265
3346
  const skipped = skipNames.get(called);
3266
3347
  if (skipped !== void 0) {
3267
- skips.push(...skipsOf(statement, skipped));
3348
+ declared = applySkips(declared, skipsOf(statement, skipped));
3268
3349
  }
3269
3350
  }
3270
3351
  const resolved = [];
3271
- for (const declaration of applySkips(declared, skips)) {
3352
+ for (const declaration of declared) {
3272
3353
  const found = methodInAncestry(ancestry, declaration.methodName);
3273
3354
  if (found.type !== "found") {
3274
3355
  continue;
@@ -3381,6 +3462,9 @@ function skipsOf(statement, filterName) {
3381
3462
  const actions = namesUnder(args.keyword.only);
3382
3463
  return args.positional.map((arg) => symbolValue(arg)).filter((name) => name !== null).map((methodName) => ({ filterName, methodName, actions }));
3383
3464
  }
3465
+ function sameFilter(one, other) {
3466
+ return one.filter.name === other.filter.name && one.methodName === other.methodName;
3467
+ }
3384
3468
  function applySkips(declared, skips) {
3385
3469
  const left = [];
3386
3470
  for (const declaration of declared) {
@@ -3508,7 +3592,7 @@ function storageEffect(call, model, pattern, kind, selector) {
3508
3592
  binding: storageBinding({
3509
3593
  recognition: "ruby-storage",
3510
3594
  storageSystem: pattern.storageSystem,
3511
- scope: pattern.baseClasses[0] ?? "",
3595
+ scope: "default",
3512
3596
  container: constantName(model)
3513
3597
  }),
3514
3598
  callee: call.text,
@@ -3676,6 +3760,7 @@ function fieldReadContext(pattern, cache, fileBlocks, bodyRead) {
3676
3760
  lookup: {
3677
3761
  root: pattern.root,
3678
3762
  pathConvention: pattern.pathConvention,
3763
+ acronyms: pattern.acronyms ?? [],
3679
3764
  ancestryRootClassNames: pattern.ancestryRootClassNames,
3680
3765
  parsedFile: (absPath) => cache.get(absPath),
3681
3766
  localDefinition: (name) => sameFileBlocks(name, fileBlocks)
@@ -3790,12 +3875,16 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
3790
3875
  const lookup = {
3791
3876
  root: pattern.root,
3792
3877
  pathConvention: pattern.pathConvention,
3878
+ acronyms: pattern.acronyms ?? [],
3793
3879
  ancestryRootClassNames: pattern.ancestryRootClassNames,
3794
3880
  parsedFile: (absPath) => options.cache.get(absPath),
3795
3881
  localDefinition: (name) => sameFileBlocks(name, fileBlocks)
3796
3882
  };
3797
3883
  const ancestry = await ancestryOf(info.qualifiedName, ownBlocks, lookup);
3798
- if (!reachesConfiguredBase(ancestry, info.qualifiedName, pattern.baseClassNames)) {
3884
+ if (!reachesConfiguredBase(ancestry, info.qualifiedName, [
3885
+ ...pattern.baseClassNames,
3886
+ ...pattern.ancestryRootClassNames
3887
+ ])) {
3799
3888
  return [];
3800
3889
  }
3801
3890
  const filters = controllerFilters(pattern, ancestry);
@@ -3815,43 +3904,73 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
3815
3904
  enclosingQualifiedName: filter.enclosingQualifiedName
3816
3905
  });
3817
3906
  }
3818
- for (const block of ownBlocks) {
3819
- 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) {
3820
3942
  continue;
3821
3943
  }
3822
- const visibility = instanceMethodVisibility(block.info.bodyNode);
3823
- for (const [actionName, method] of instanceMethodsByName(
3824
- block.info.bodyNode
3944
+ for (const [actionName, method, block] of publicInstanceMethods(
3945
+ entry.blocks
3825
3946
  )) {
3826
- if ((visibility.get(actionName) ?? "public") !== "public") {
3947
+ if (seen.has(actionName)) {
3827
3948
  continue;
3828
3949
  }
3829
- const around = filters.filter((filter) => filterCoversAction(filter, actionName)).map(
3830
- (filter) => filterReference(
3831
- filter,
3832
- options.displayPathOf?.(filter.file) ?? filter.file
3833
- )
3834
- );
3835
- const raw = buildControllerActionUnit(
3836
- pack,
3837
- pattern,
3838
- info.qualifiedName,
3839
- actionName,
3840
- method,
3841
- { display: options.filePath, absolute: block.file },
3842
- options,
3843
- around
3844
- );
3845
- units.push(raw);
3846
- options.onReachSeed?.(raw, {
3847
- file: block.file,
3848
- node: method,
3849
- enclosingQualifiedName: info.qualifiedName
3850
- });
3950
+ seen.add(actionName);
3951
+ if (pattern.routeFor(info.qualifiedName, actionName) === null) {
3952
+ continue;
3953
+ }
3954
+ emitAction(actionName, method, block);
3851
3955
  }
3852
3956
  }
3853
3957
  return units;
3854
3958
  }
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
+ }
3855
3974
  function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actionName, method, file, bodyRead, wrappers = []) {
3856
3975
  const range = rangeOf(method);
3857
3976
  const route = pattern.routeFor(controllerQualifiedName, actionName);
@@ -5106,7 +5225,8 @@ function inheritedMethodsIn(packs) {
5106
5225
  return found;
5107
5226
  }
5108
5227
  function alreadyDiscovered(seen, raw) {
5109
- 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}`;
5110
5230
  if (seen.has(key)) {
5111
5231
  return true;
5112
5232
  }
@@ -5405,8 +5525,8 @@ function displayNameOf(node) {
5405
5525
  if (node.type === "class" || node.type === "module") {
5406
5526
  return field(node, "name")?.text ?? node.text;
5407
5527
  }
5408
- const text = node.text.split("\n")[0].trim();
5409
- 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;
5410
5530
  }
5411
5531
  function width(node) {
5412
5532
  return node.endIndex - node.startIndex;
@@ -5433,14 +5553,14 @@ var RubyWhySession = class {
5433
5553
  * The smallest expression on `line` of `file` whose text is exactly
5434
5554
  * `text`, or null.
5435
5555
  */
5436
- findExpression(file, line, text) {
5556
+ findExpression(file, line, text2) {
5437
5557
  const root = this.rootOf(file);
5438
5558
  if (root === null) {
5439
5559
  return null;
5440
5560
  }
5441
5561
  let found = null;
5442
5562
  const visit = (node) => {
5443
- if (node.startPosition.row + 1 === line && node.text === text) {
5563
+ if (node.startPosition.row + 1 === line && node.text === text2) {
5444
5564
  if (found === null || width(node) <= width(found)) {
5445
5565
  found = node;
5446
5566
  }
@@ -5610,6 +5730,7 @@ export {
5610
5730
  inheritedStatements,
5611
5731
  methodDefinitionsIn,
5612
5732
  methodInAncestry,
5733
+ nestedStatements,
5613
5734
  nodeId,
5614
5735
  parseRuby,
5615
5736
  parseRubySync,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suss/adapter-ruby",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "type": "module",
5
5
  "description": "Read a Ruby codebase with suss and check what each Rails action or graphql-ruby resolver returns against the code that calls it.",
6
6
  "keywords": [
@@ -49,12 +49,12 @@
49
49
  "typecheck": "tsc --noEmit"
50
50
  },
51
51
  "dependencies": {
52
- "@suss/behavioral-ir": "0.26.0",
53
- "@suss/datalog": "0.26.0",
54
- "@suss/extractor": "0.26.0",
55
- "@suss/ir-core": "0.26.0",
56
- "@suss/resolution": "0.26.0",
57
- "@suss/values": "0.26.0",
52
+ "@suss/behavioral-ir": "0.27.0",
53
+ "@suss/datalog": "0.27.0",
54
+ "@suss/extractor": "0.27.0",
55
+ "@suss/ir-core": "0.27.0",
56
+ "@suss/resolution": "0.27.0",
57
+ "@suss/values": "0.27.0",
58
58
  "web-tree-sitter": "^0.26.11"
59
59
  },
60
60
  "devDependencies": {