@suss/adapter-ruby 0.22.4 → 0.23.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
@@ -209,9 +209,21 @@ var PATH_CONVENTIONS = {
209
209
  railsUnderscore: underscoreConstantPath
210
210
  };
211
211
  function resolveConstantFile(root, qualifiedName, convention) {
212
- const relative = PATH_CONVENTIONS[convention](qualifiedName);
213
- const file = path.join(root, `${relative}.rb`);
214
- return fs.existsSync(file) ? file : null;
212
+ const relative = `${PATH_CONVENTIONS[convention](qualifiedName)}.rb`;
213
+ for (const candidate of [root, ...autoloadDirectories(root)]) {
214
+ const file = path.join(candidate, relative);
215
+ if (fs.existsSync(file)) {
216
+ return file;
217
+ }
218
+ }
219
+ return null;
220
+ }
221
+ function autoloadDirectories(root) {
222
+ try {
223
+ return fs.readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => path.join(root, entry.name)).sort();
224
+ } catch {
225
+ return [];
226
+ }
215
227
  }
216
228
 
217
229
  // src/scope.ts
@@ -624,8 +636,15 @@ function isDefaultedAt(node) {
624
636
  return false;
625
637
  }
626
638
 
627
- // src/paths/effects.ts
628
- import { enumerateOrDegrade, sharedGatingConditions } from "@suss/extractor";
639
+ // src/responseStatus.ts
640
+ import {
641
+ absentReading,
642
+ enumerateOrDegrade,
643
+ guardsHoldOn,
644
+ unreadableReading,
645
+ writtenReading
646
+ } from "@suss/extractor";
647
+ import { constantOf, literalOf as literalOf2 } from "@suss/values";
629
648
 
630
649
  // src/paths/bareCalls.ts
631
650
  var PARAMETER_LIST_TYPES = [
@@ -1001,181 +1020,6 @@ function lowerRubyBody(body, terminals, responseCalls = []) {
1001
1020
  };
1002
1021
  }
1003
1022
 
1004
- // src/paths/effects.ts
1005
- var RAISE_NAMES2 = /* @__PURE__ */ new Set(["raise", "fail"]);
1006
- var BLOCK_TYPES = /* @__PURE__ */ new Set([
1007
- "body_statement",
1008
- "then",
1009
- "else",
1010
- "do_block",
1011
- "block"
1012
- ]);
1013
- var LITERAL_ARGS = {
1014
- string: (node) => ({
1015
- kind: "string",
1016
- value: node.text.replace(/^["']|["']$/g, "")
1017
- }),
1018
- integer: (node) => {
1019
- const value = Number.parseInt(node.text, 10);
1020
- return Number.isNaN(value) ? null : { kind: "number", value };
1021
- },
1022
- float: (node) => {
1023
- const value = Number.parseFloat(node.text);
1024
- return Number.isNaN(value) ? null : { kind: "number", value };
1025
- },
1026
- true: () => ({ kind: "boolean", value: true }),
1027
- false: () => ({ kind: "boolean", value: false }),
1028
- simple_symbol: (node) => ({ kind: "string", value: node.text.slice(1) })
1029
- };
1030
- function children(node) {
1031
- return node.namedChildren.filter((child) => child !== null);
1032
- }
1033
- function withoutChainLinks(calls) {
1034
- const isLink = /* @__PURE__ */ new Set();
1035
- for (const call of calls) {
1036
- const receiver = field(call, "receiver");
1037
- if (receiver !== null && receiver.type === "call") {
1038
- isLink.add(receiver.id);
1039
- }
1040
- }
1041
- return calls.filter((call) => !isLink.has(call.id));
1042
- }
1043
- function calleeMethodName(call) {
1044
- if (call.type === "identifier") {
1045
- return call.text;
1046
- }
1047
- const method = field(call, "method");
1048
- if (method !== null) {
1049
- return method.text;
1050
- }
1051
- return field(call, "receiver") !== null ? "call" : children(call)[0]?.text;
1052
- }
1053
- function isPropertyRead(node) {
1054
- if (field(node, "receiver") === null || field(node, "arguments") !== null) {
1055
- return false;
1056
- }
1057
- return calleeMethodName(node) !== "call";
1058
- }
1059
- function isRaise2(node) {
1060
- const method = field(node, "method") ?? children(node)[0] ?? null;
1061
- return field(node, "receiver") === null && method !== null && RAISE_NAMES2.has(method.text);
1062
- }
1063
- var NO_INHERITED_METHODS = /* @__PURE__ */ new Set();
1064
- function isInherited(node, inherited) {
1065
- const method = field(node, "method");
1066
- return field(node, "receiver") === null && method !== null && inherited.has(method.text);
1067
- }
1068
- function isCall(node, locals, inherited) {
1069
- if (node.type === "call") {
1070
- return !isPropertyRead(node) && !isRaise2(node) && !isInherited(node, inherited);
1071
- }
1072
- return isBareMethodCall(node, locals) && !RAISE_NAMES2.has(node.text) && !inherited.has(node.text);
1073
- }
1074
- function collectCalls(node, locals, inherited, found) {
1075
- for (const child of children(node)) {
1076
- if (OWN_BODY_TYPES.has(child.type)) {
1077
- continue;
1078
- }
1079
- if (isCall(child, locals, inherited)) {
1080
- found.push(child);
1081
- }
1082
- collectCalls(child, locals, inherited, found);
1083
- }
1084
- return found;
1085
- }
1086
- function bodyCalls(definitionNode, inherited = NO_INHERITED_METHODS) {
1087
- const body = field(definitionNode, "body");
1088
- if (body === null) {
1089
- return [];
1090
- }
1091
- return collectCalls(body, localNamesIn(definitionNode), inherited, []);
1092
- }
1093
- function calleeText(call) {
1094
- const receiver = field(call, "receiver");
1095
- const method = calleeMethodName(call);
1096
- if (method === void 0) {
1097
- return call.text;
1098
- }
1099
- return receiver === null ? method : `${receiver.text}.${method}`;
1100
- }
1101
- function argOf(node) {
1102
- const literal = LITERAL_ARGS[node.type]?.(node);
1103
- if (literal !== null && literal !== void 0) {
1104
- return literal;
1105
- }
1106
- if (node.type === "call" && !isPropertyRead(node)) {
1107
- return { kind: "call", callee: calleeText(node), args: argsOf(node) };
1108
- }
1109
- return { kind: "identifier", name: node.text };
1110
- }
1111
- function argsOf(call) {
1112
- const args = field(call, "arguments");
1113
- if (args === null) {
1114
- return [];
1115
- }
1116
- return children(args).map(
1117
- (child) => child.type === "pair" ? argOf(field(child, "value") ?? child) : argOf(child)
1118
- );
1119
- }
1120
- function enclosingStatement(call, body) {
1121
- let current = call;
1122
- while (current.parent !== null) {
1123
- if (BLOCK_TYPES.has(current.parent.type) || current.parent.id === body.id) {
1124
- return current;
1125
- }
1126
- current = current.parent;
1127
- }
1128
- return current;
1129
- }
1130
- function invocationEffects(definitionNode, inherited = NO_INHERITED_METHODS) {
1131
- const body = field(definitionNode, "body");
1132
- if (body === null) {
1133
- return [];
1134
- }
1135
- const calls = withoutChainLinks(bodyCalls(definitionNode, inherited));
1136
- if (calls.length === 0) {
1137
- return [];
1138
- }
1139
- const statementOf2 = /* @__PURE__ */ new Map();
1140
- const statements = [];
1141
- for (const call of calls) {
1142
- const statement = enclosingStatement(call, body);
1143
- statementOf2.set(call.id, statement);
1144
- statements.push(statement);
1145
- }
1146
- const lowered = lowerRubyBody(body, statements);
1147
- const enumerated = enumerateOrDegrade(
1148
- {
1149
- statements: lowered.statements,
1150
- terminalsByStmt: lowered.terminalsByStmt
1151
- },
1152
- statements
1153
- );
1154
- return calls.map((call) => {
1155
- const statement = statementOf2.get(call.id);
1156
- const conditions = sharedGatingConditions(
1157
- statement === void 0 ? void 0 : enumerated.byTerminal.get(statement)
1158
- );
1159
- return {
1160
- type: "invocation",
1161
- callee: calleeText(call),
1162
- args: argsOf(call),
1163
- async: false,
1164
- ...conditions.length > 0 ? { preconditions: conditions } : {}
1165
- };
1166
- });
1167
- }
1168
-
1169
- // src/responseStatus.ts
1170
- import {
1171
- absentReading,
1172
- enumerateOrDegrade as enumerateOrDegrade2,
1173
- guardsHoldOn,
1174
- unreadableReading,
1175
- writtenReading
1176
- } from "@suss/extractor";
1177
- import { constantOf, literalOf as literalOf2 } from "@suss/values";
1178
-
1179
1023
  // src/values/evaluator.ts
1180
1024
  import { nodeOfKey } from "@suss/resolution";
1181
1025
  import { Evaluator, force, literalOf } from "@suss/values";
@@ -1244,7 +1088,7 @@ function paramNameOf(param) {
1244
1088
  function declaredParamNames(method) {
1245
1089
  const params = field(method, "parameters");
1246
1090
  const declared = /* @__PURE__ */ new Set();
1247
- for (const param of params === null ? [] : children2(params)) {
1091
+ for (const param of params === null ? [] : children(params)) {
1248
1092
  const name = paramNameOf(param);
1249
1093
  if (name !== null) {
1250
1094
  declared.add(name.text);
@@ -1258,7 +1102,7 @@ function readKey(filePath, node, enclosing) {
1258
1102
  }
1259
1103
  const parent = node.parent;
1260
1104
  if (parent !== null && parent.type === "call" && field(parent, "method")?.id === node.id && field(parent, "receiver") !== null) {
1261
- return isPropertyRead2(parent) ? nodeId(filePath, parent) : nodeId(filePath, node);
1105
+ return isPropertyRead(parent) ? nodeId(filePath, parent) : nodeId(filePath, node);
1262
1106
  }
1263
1107
  if (enclosing !== null && declaredParamNames(enclosing).has(node.text)) {
1264
1108
  return `${nodeId(filePath, enclosing)}#${node.text}`;
@@ -1286,7 +1130,7 @@ var WRITTEN_VALUE_TYPES = /* @__PURE__ */ new Set([
1286
1130
  "parenthesized_statements"
1287
1131
  ]);
1288
1132
  var ARRAY_TYPES = /* @__PURE__ */ new Set(["array", "string_array", "symbol_array"]);
1289
- function children2(node) {
1133
+ function children(node) {
1290
1134
  return node.namedChildren.filter((child) => child !== null);
1291
1135
  }
1292
1136
  function add(emitter, relation, ...tuple) {
@@ -1314,8 +1158,8 @@ function pairKeyText(key) {
1314
1158
  }
1315
1159
  return null;
1316
1160
  }
1317
- function isPropertyRead2(node) {
1318
- return node.type === "call" && field(node, "receiver") !== null && field(node, "arguments") === null && children2(node).every(
1161
+ function isPropertyRead(node) {
1162
+ return node.type === "call" && field(node, "receiver") !== null && field(node, "arguments") === null && children(node).every(
1319
1163
  (child) => child.type !== "do_block" && child.type !== "block"
1320
1164
  );
1321
1165
  }
@@ -1354,7 +1198,7 @@ function emitCall(emitter, call) {
1354
1198
  }
1355
1199
  const args = field(call, "arguments");
1356
1200
  let position = 0;
1357
- for (const argument of args === null ? [] : children2(args)) {
1201
+ for (const argument of args === null ? [] : children(args)) {
1358
1202
  if (argument.type === "pair") {
1359
1203
  const key = field(argument, "key");
1360
1204
  const value = field(argument, "value");
@@ -1384,7 +1228,7 @@ function emitArray(emitter, array) {
1384
1228
  const objectKey = nodeId(emitter.filePath, array);
1385
1229
  add(emitter, "objectValue", objectKey);
1386
1230
  let position = 0;
1387
- for (const element of children2(array)) {
1231
+ for (const element of children(array)) {
1388
1232
  add(
1389
1233
  emitter,
1390
1234
  "holdsProperty",
@@ -1398,7 +1242,7 @@ function emitArray(emitter, array) {
1398
1242
  function emitHash(emitter, hash) {
1399
1243
  const objectKey = nodeId(emitter.filePath, hash);
1400
1244
  add(emitter, "objectValue", objectKey);
1401
- for (const pair of children2(hash)) {
1245
+ for (const pair of children(hash)) {
1402
1246
  if (pair.type !== "pair") {
1403
1247
  continue;
1404
1248
  }
@@ -1412,7 +1256,7 @@ function emitHash(emitter, hash) {
1412
1256
  }
1413
1257
  }
1414
1258
  function walkExpressions(node, visit) {
1415
- for (const child of children2(node)) {
1259
+ for (const child of children(node)) {
1416
1260
  if (OWN_BODY_TYPES.has(child.type)) {
1417
1261
  continue;
1418
1262
  }
@@ -1422,7 +1266,7 @@ function walkExpressions(node, visit) {
1422
1266
  }
1423
1267
  function emitExpressionFacts(emitter, node) {
1424
1268
  walkExpressions(node, (child) => {
1425
- if (isPropertyRead2(child)) {
1269
+ if (isPropertyRead(child)) {
1426
1270
  emitPropertyRead(emitter, child);
1427
1271
  } else if (child.type === "call") {
1428
1272
  emitCall(emitter, child);
@@ -1442,7 +1286,7 @@ function emitExpressionFacts(emitter, node) {
1442
1286
  });
1443
1287
  }
1444
1288
  function implicitReturn(body) {
1445
- const statements = children2(body).filter(
1289
+ const statements = children(body).filter(
1446
1290
  (child) => child.type !== "rescue" && child.type !== "ensure"
1447
1291
  );
1448
1292
  const last = statements[statements.length - 1];
@@ -1454,7 +1298,7 @@ function emitMethodFacts(emitter, method) {
1454
1298
  const params = field(method, "parameters");
1455
1299
  const declared = /* @__PURE__ */ new Set();
1456
1300
  let position = 0;
1457
- for (const param of params === null ? [] : children2(params)) {
1301
+ for (const param of params === null ? [] : children(params)) {
1458
1302
  const paramName = paramNameOf(param);
1459
1303
  if (paramName !== null) {
1460
1304
  declared.add(paramName.text);
@@ -1473,7 +1317,7 @@ function emitMethodFacts(emitter, method) {
1473
1317
  return funcKey;
1474
1318
  }
1475
1319
  const recordNested = (node) => {
1476
- for (const child of children2(node)) {
1320
+ for (const child of children(node)) {
1477
1321
  if (OWN_BODY_TYPES.has(child.type)) {
1478
1322
  add(emitter, "containsFn", funcKey, nodeId(emitter.filePath, child));
1479
1323
  continue;
@@ -1484,8 +1328,8 @@ function emitMethodFacts(emitter, method) {
1484
1328
  recordNested(body);
1485
1329
  walkExpressions(body, (child) => {
1486
1330
  if (child.type === "return") {
1487
- const first = children2(child)[0];
1488
- const returned = first?.type === "argument_list" ? children2(first)[0] : first;
1331
+ const first = children(child)[0];
1332
+ const returned = first?.type === "argument_list" ? children(first)[0] : first;
1489
1333
  if (returned !== void 0) {
1490
1334
  add(inside, "returnsValue", funcKey, valueKey(inside, returned));
1491
1335
  }
@@ -1529,13 +1373,13 @@ function emitClassFacts(emitter, cls) {
1529
1373
  const classKey = nodeId(emitter.filePath, cls);
1530
1374
  add(emitter, "objectValue", classKey);
1531
1375
  const superclass = field(cls, "superclass");
1532
- const base = superclass === null ? null : children2(superclass)[0] ?? null;
1376
+ const base = superclass === null ? null : children(superclass)[0] ?? null;
1533
1377
  if (base !== null) {
1534
1378
  add(emitter, "extends", classKey, valueKey(emitter, base));
1535
1379
  add(emitter, "extendsNamed", classKey, base.text);
1536
1380
  }
1537
1381
  const body = field(cls, "body");
1538
- for (const statement of body === null ? [] : children2(body)) {
1382
+ for (const statement of body === null ? [] : children(body)) {
1539
1383
  if (statement.type === "assignment") {
1540
1384
  const left = field(statement, "left");
1541
1385
  const right = field(statement, "right");
@@ -1577,7 +1421,7 @@ function emitValueFacts(db, filePath, root) {
1577
1421
  add(emitter, "exportsAs", filePath, name.text, key);
1578
1422
  };
1579
1423
  const walk = (node) => {
1580
- for (const child of children2(node)) {
1424
+ for (const child of children(node)) {
1581
1425
  if (NESTING_TYPES.has(child.type)) {
1582
1426
  declaresName(child, emitClassFacts(emitter, child));
1583
1427
  continue;
@@ -1608,7 +1452,7 @@ var OWN_ROOT_TYPES = /* @__PURE__ */ new Set([
1608
1452
  "module",
1609
1453
  "singleton_class"
1610
1454
  ]);
1611
- var BLOCK_TYPES2 = /* @__PURE__ */ new Set(["block", "do_block"]);
1455
+ var BLOCK_TYPES = /* @__PURE__ */ new Set(["block", "do_block"]);
1612
1456
  var METHOD_TYPES2 = /* @__PURE__ */ new Set(["method", "singleton_method"]);
1613
1457
  var NAME_TYPES = /* @__PURE__ */ new Set([
1614
1458
  "identifier",
@@ -2114,7 +1958,7 @@ function isTailExpression(node) {
2114
1958
  if (owner.type === "block" && owner.parent?.type === "lambda") {
2115
1959
  return true;
2116
1960
  }
2117
- if (METHOD_TYPES2.has(owner.type) || BLOCK_TYPES2.has(owner.type)) {
1961
+ if (METHOD_TYPES2.has(owner.type) || BLOCK_TYPES.has(owner.type)) {
2118
1962
  return true;
2119
1963
  }
2120
1964
  if (owner.type === "when") {
@@ -2133,7 +1977,7 @@ function isRoot(node) {
2133
1977
  if (OWN_ROOT_TYPES.has(node.type)) {
2134
1978
  return true;
2135
1979
  }
2136
- return BLOCK_TYPES2.has(node.type) && node.parent?.type === "call";
1980
+ return BLOCK_TYPES.has(node.type) && node.parent?.type === "call";
2137
1981
  }
2138
1982
  function isPathStatement(node, parent) {
2139
1983
  if (node.type === "elsif") {
@@ -2187,7 +2031,7 @@ function functionOf(node) {
2187
2031
  body: bodyOf(block)
2188
2032
  };
2189
2033
  }
2190
- if (BLOCK_TYPES2.has(node.type)) {
2034
+ if (BLOCK_TYPES.has(node.type)) {
2191
2035
  return {
2192
2036
  parameters: parametersOf(field(node, "parameters")),
2193
2037
  body: bodyOf(node)
@@ -2715,7 +2559,10 @@ function conditionOf(condition) {
2715
2559
  source: condition.source
2716
2560
  };
2717
2561
  }
2718
- function branchOf(outcome, pattern, effects, extraEffects) {
2562
+ function branchOf(outcome, pattern, effects, extraEffects, options) {
2563
+ if (outcome.fellThrough === true && options.fallthrough === "handOn") {
2564
+ return handsOnBranch(outcome, effects, extraEffects);
2565
+ }
2719
2566
  return {
2720
2567
  conditions: outcome.conditions,
2721
2568
  terminal: {
@@ -2740,7 +2587,28 @@ function branchOf(outcome, pattern, effects, extraEffects) {
2740
2587
  isDefault: outcome.conditions.length === 0
2741
2588
  };
2742
2589
  }
2743
- function responseBranches(method, pattern, effects, extraEffects) {
2590
+ function handsOnBranch(outcome, effects, extraEffects) {
2591
+ return {
2592
+ conditions: outcome.conditions,
2593
+ terminal: {
2594
+ kind: "delegate",
2595
+ statusCode: null,
2596
+ body: null,
2597
+ exceptionType: null,
2598
+ message: null,
2599
+ component: null,
2600
+ renderTree: null,
2601
+ delegateTarget: null,
2602
+ emitEvent: null,
2603
+ location: outcome.location
2604
+ },
2605
+ effects: effectsReaching(effects, outcome.conditions),
2606
+ ...extraEffects === void 0 ? {} : { extraEffects },
2607
+ location: outcome.location,
2608
+ isDefault: outcome.conditions.length === 0
2609
+ };
2610
+ }
2611
+ function responseBranches(method, pattern, effects, extraEffects, options = {}) {
2744
2612
  const declarations = pattern.responseStatusCalls ?? [];
2745
2613
  const body = field(method, "body");
2746
2614
  if (declarations.length === 0 || body === null) {
@@ -2755,7 +2623,7 @@ function responseBranches(method, pattern, effects, extraEffects) {
2755
2623
  (node) => lowered.terminalHome.has(node) && collectResponseCalls(node, byName, locals, []).length === 0
2756
2624
  );
2757
2625
  const terminals = [...responses, ...bareReturns];
2758
- const enumerated = enumerateOrDegrade2(
2626
+ const enumerated = enumerateOrDegrade(
2759
2627
  {
2760
2628
  statements: lowered.statements,
2761
2629
  terminalsByStmt: lowered.terminalsByStmt
@@ -2779,17 +2647,369 @@ function responseBranches(method, pattern, effects, extraEffects) {
2779
2647
  outcomes.push({
2780
2648
  conditions: path7.map(conditionOf),
2781
2649
  reading: absentReading,
2782
- location: rangeOf(method)
2650
+ location: rangeOf(method),
2651
+ fellThrough: true
2783
2652
  });
2784
2653
  }
2785
2654
  if (outcomes.length === 0) {
2786
2655
  return null;
2787
2656
  }
2788
2657
  return outcomes.map(
2789
- (outcome) => branchOf(outcome, pattern, effects, extraEffects)
2658
+ (outcome) => branchOf(outcome, pattern, effects, extraEffects, options)
2790
2659
  );
2791
2660
  }
2792
2661
 
2662
+ // src/filters.ts
2663
+ function controllerFilters(pattern, ancestry) {
2664
+ const forms = pattern.filters ?? [];
2665
+ if (forms.length === 0) {
2666
+ return [];
2667
+ }
2668
+ const byName = new Map(forms.map((form) => [form.name, form]));
2669
+ const skipNames = /* @__PURE__ */ new Map();
2670
+ for (const form of forms) {
2671
+ if (form.skippedBy !== void 0) {
2672
+ skipNames.set(form.skippedBy, form.name);
2673
+ }
2674
+ }
2675
+ const declared = [];
2676
+ const skips = [];
2677
+ for (const { statement } of inheritedStatements(ancestry)) {
2678
+ const called = calledName(statement);
2679
+ if (called === null) {
2680
+ continue;
2681
+ }
2682
+ const form = byName.get(called);
2683
+ if (form !== void 0) {
2684
+ declared.push(...declarationsOf(statement, form));
2685
+ continue;
2686
+ }
2687
+ const skipped = skipNames.get(called);
2688
+ if (skipped !== void 0) {
2689
+ skips.push(...skipsOf(statement, skipped));
2690
+ }
2691
+ }
2692
+ const resolved = [];
2693
+ for (const declaration of applySkips(declared, skips)) {
2694
+ const found = methodInAncestry(ancestry, declaration.methodName);
2695
+ if (found.type !== "found") {
2696
+ continue;
2697
+ }
2698
+ resolved.push({
2699
+ filter: declaration.filter,
2700
+ methodName: declaration.methodName,
2701
+ method: found.method,
2702
+ file: found.block.file,
2703
+ enclosingQualifiedName: found.block.info.qualifiedName,
2704
+ only: declaration.only,
2705
+ except: declaration.except
2706
+ });
2707
+ }
2708
+ return [
2709
+ ...resolved.filter((one) => one.filter.onThrow !== true),
2710
+ ...resolved.filter((one) => one.filter.onThrow === true)
2711
+ ];
2712
+ }
2713
+ function filterCoversAction(filter, actionName) {
2714
+ if (filter.except.has(actionName)) {
2715
+ return false;
2716
+ }
2717
+ return filter.only === null || filter.only.has(actionName);
2718
+ }
2719
+ function filterReference(filter, displayPath) {
2720
+ return {
2721
+ file: displayPath,
2722
+ name: filter.methodName,
2723
+ ...filter.filter.onThrow === true ? { onThrow: true } : {}
2724
+ };
2725
+ }
2726
+ function filterUnit(filter, pattern, displayPath, body) {
2727
+ const range = rangeOf(filter.method);
2728
+ const branches = responseBranches(
2729
+ filter.method,
2730
+ pattern,
2731
+ body.effects ?? [],
2732
+ body.extraEffects,
2733
+ { fallthrough: "handOn" }
2734
+ );
2735
+ return {
2736
+ identity: {
2737
+ name: filter.methodName,
2738
+ nameKind: "binding",
2739
+ kind: "middleware",
2740
+ file: displayPath,
2741
+ range,
2742
+ span: spanOf(filter.method),
2743
+ exportName: filter.methodName,
2744
+ exportPath: [filter.enclosingQualifiedName, filter.methodName]
2745
+ },
2746
+ boundaryBinding: null,
2747
+ parameters: [],
2748
+ branches: branches ?? [handsOn(range, body)],
2749
+ bodyContent: body.bodyContent ?? "absent",
2750
+ dependencyCalls: [],
2751
+ declaredContract: null
2752
+ };
2753
+ }
2754
+ function handsOn(range, body) {
2755
+ return {
2756
+ conditions: [],
2757
+ terminal: {
2758
+ kind: "delegate",
2759
+ statusCode: null,
2760
+ body: null,
2761
+ exceptionType: null,
2762
+ message: null,
2763
+ component: null,
2764
+ renderTree: null,
2765
+ delegateTarget: null,
2766
+ emitEvent: null,
2767
+ location: range
2768
+ },
2769
+ effects: body.effects ?? [],
2770
+ ...body.extraEffects === void 0 ? {} : { extraEffects: body.extraEffects },
2771
+ location: range,
2772
+ isDefault: true
2773
+ };
2774
+ }
2775
+ function calledName(statement) {
2776
+ if (statement.type !== "call" || field(statement, "receiver") !== null) {
2777
+ return null;
2778
+ }
2779
+ return field(statement, "method")?.text ?? null;
2780
+ }
2781
+ function declarationsOf(statement, filter) {
2782
+ const args = readCallArgs(field(statement, "arguments"));
2783
+ const keywords = filter.actionKeywords;
2784
+ const only = keywords === void 0 ? null : namesUnder(args.keyword[keywords.include]);
2785
+ const except = keywords === void 0 ? null : namesUnder(args.keyword[keywords.exclude]);
2786
+ return methodNamesOf(args, filter).map((methodName) => ({
2787
+ filter,
2788
+ methodName,
2789
+ only,
2790
+ except: except ?? /* @__PURE__ */ new Set()
2791
+ }));
2792
+ }
2793
+ function methodNamesOf(args, filter) {
2794
+ if (filter.methodFrom === "withKeyword") {
2795
+ const named2 = args.keyword.with;
2796
+ const value = named2 === void 0 ? null : symbolValue(named2);
2797
+ return value === null ? [] : [value];
2798
+ }
2799
+ return args.positional.map((arg) => symbolValue(arg)).filter((name) => name !== null);
2800
+ }
2801
+ function skipsOf(statement, filterName) {
2802
+ const args = readCallArgs(field(statement, "arguments"));
2803
+ const actions = namesUnder(args.keyword.only);
2804
+ return args.positional.map((arg) => symbolValue(arg)).filter((name) => name !== null).map((methodName) => ({ filterName, methodName, actions }));
2805
+ }
2806
+ function applySkips(declared, skips) {
2807
+ const left = [];
2808
+ for (const declaration of declared) {
2809
+ const reaching = skips.filter(
2810
+ (skip) => skip.filterName === declaration.filter.name && skip.methodName === declaration.methodName
2811
+ );
2812
+ if (reaching.some((skip) => skip.actions === null)) {
2813
+ continue;
2814
+ }
2815
+ const except = new Set(declaration.except);
2816
+ for (const skip of reaching) {
2817
+ for (const action of skip.actions ?? []) {
2818
+ except.add(action);
2819
+ }
2820
+ }
2821
+ left.push({ ...declaration, except });
2822
+ }
2823
+ return left;
2824
+ }
2825
+ function namesUnder(node) {
2826
+ if (node === void 0) {
2827
+ return null;
2828
+ }
2829
+ const single = symbolValue(node);
2830
+ if (single !== null) {
2831
+ return /* @__PURE__ */ new Set([single]);
2832
+ }
2833
+ if (node.type !== "array" && node.type !== "symbol_array") {
2834
+ return null;
2835
+ }
2836
+ const names = /* @__PURE__ */ new Set();
2837
+ for (const child of bodyStatements(node)) {
2838
+ const bare = child.type === "bare_symbol" ? child.text : null;
2839
+ const name = symbolValue(child) ?? bare;
2840
+ if (name !== null) {
2841
+ names.add(name);
2842
+ }
2843
+ }
2844
+ return names;
2845
+ }
2846
+
2847
+ // src/paths/effects.ts
2848
+ import { enumerateOrDegrade as enumerateOrDegrade2, sharedGatingConditions } from "@suss/extractor";
2849
+ var RAISE_NAMES2 = /* @__PURE__ */ new Set(["raise", "fail"]);
2850
+ var BLOCK_TYPES2 = /* @__PURE__ */ new Set([
2851
+ "body_statement",
2852
+ "then",
2853
+ "else",
2854
+ "do_block",
2855
+ "block"
2856
+ ]);
2857
+ var LITERAL_ARGS = {
2858
+ string: (node) => ({
2859
+ kind: "string",
2860
+ value: node.text.replace(/^["']|["']$/g, "")
2861
+ }),
2862
+ integer: (node) => {
2863
+ const value = Number.parseInt(node.text, 10);
2864
+ return Number.isNaN(value) ? null : { kind: "number", value };
2865
+ },
2866
+ float: (node) => {
2867
+ const value = Number.parseFloat(node.text);
2868
+ return Number.isNaN(value) ? null : { kind: "number", value };
2869
+ },
2870
+ true: () => ({ kind: "boolean", value: true }),
2871
+ false: () => ({ kind: "boolean", value: false }),
2872
+ simple_symbol: (node) => ({ kind: "string", value: node.text.slice(1) })
2873
+ };
2874
+ function children2(node) {
2875
+ return node.namedChildren.filter((child) => child !== null);
2876
+ }
2877
+ function withoutChainLinks(calls) {
2878
+ const isLink = /* @__PURE__ */ new Set();
2879
+ for (const call of calls) {
2880
+ const receiver = field(call, "receiver");
2881
+ if (receiver !== null && receiver.type === "call") {
2882
+ isLink.add(receiver.id);
2883
+ }
2884
+ }
2885
+ return calls.filter((call) => !isLink.has(call.id));
2886
+ }
2887
+ function calleeMethodName(call) {
2888
+ if (call.type === "identifier") {
2889
+ return call.text;
2890
+ }
2891
+ const method = field(call, "method");
2892
+ if (method !== null) {
2893
+ return method.text;
2894
+ }
2895
+ return field(call, "receiver") !== null ? "call" : children2(call)[0]?.text;
2896
+ }
2897
+ function isPropertyRead2(node) {
2898
+ if (field(node, "receiver") === null || field(node, "arguments") !== null) {
2899
+ return false;
2900
+ }
2901
+ return calleeMethodName(node) !== "call";
2902
+ }
2903
+ function isRaise2(node) {
2904
+ const method = field(node, "method") ?? children2(node)[0] ?? null;
2905
+ return field(node, "receiver") === null && method !== null && RAISE_NAMES2.has(method.text);
2906
+ }
2907
+ var NO_INHERITED_METHODS = /* @__PURE__ */ new Set();
2908
+ function isInherited(node, inherited) {
2909
+ const method = field(node, "method");
2910
+ return field(node, "receiver") === null && method !== null && inherited.has(method.text);
2911
+ }
2912
+ function isCall(node, locals, inherited) {
2913
+ if (node.type === "call") {
2914
+ return !isPropertyRead2(node) && !isRaise2(node) && !isInherited(node, inherited);
2915
+ }
2916
+ return isBareMethodCall(node, locals) && !RAISE_NAMES2.has(node.text) && !inherited.has(node.text);
2917
+ }
2918
+ function collectCalls(node, locals, inherited, found) {
2919
+ for (const child of children2(node)) {
2920
+ if (OWN_BODY_TYPES.has(child.type)) {
2921
+ continue;
2922
+ }
2923
+ if (isCall(child, locals, inherited)) {
2924
+ found.push(child);
2925
+ }
2926
+ collectCalls(child, locals, inherited, found);
2927
+ }
2928
+ return found;
2929
+ }
2930
+ function bodyCalls(definitionNode, inherited = NO_INHERITED_METHODS) {
2931
+ const body = field(definitionNode, "body");
2932
+ if (body === null) {
2933
+ return [];
2934
+ }
2935
+ return collectCalls(body, localNamesIn(definitionNode), inherited, []);
2936
+ }
2937
+ function calleeText(call) {
2938
+ const receiver = field(call, "receiver");
2939
+ const method = calleeMethodName(call);
2940
+ if (method === void 0) {
2941
+ return call.text;
2942
+ }
2943
+ return receiver === null ? method : `${receiver.text}.${method}`;
2944
+ }
2945
+ function argOf(node) {
2946
+ const literal = LITERAL_ARGS[node.type]?.(node);
2947
+ if (literal !== null && literal !== void 0) {
2948
+ return literal;
2949
+ }
2950
+ if (node.type === "call" && !isPropertyRead2(node)) {
2951
+ return { kind: "call", callee: calleeText(node), args: argsOf(node) };
2952
+ }
2953
+ return { kind: "identifier", name: node.text };
2954
+ }
2955
+ function argsOf(call) {
2956
+ const args = field(call, "arguments");
2957
+ if (args === null) {
2958
+ return [];
2959
+ }
2960
+ return children2(args).map(
2961
+ (child) => child.type === "pair" ? argOf(field(child, "value") ?? child) : argOf(child)
2962
+ );
2963
+ }
2964
+ function enclosingStatement(call, body) {
2965
+ let current = call;
2966
+ while (current.parent !== null) {
2967
+ if (BLOCK_TYPES2.has(current.parent.type) || current.parent.id === body.id) {
2968
+ return current;
2969
+ }
2970
+ current = current.parent;
2971
+ }
2972
+ return current;
2973
+ }
2974
+ function invocationEffects(definitionNode, inherited = NO_INHERITED_METHODS) {
2975
+ const body = field(definitionNode, "body");
2976
+ if (body === null) {
2977
+ return [];
2978
+ }
2979
+ const calls = withoutChainLinks(bodyCalls(definitionNode, inherited));
2980
+ if (calls.length === 0) {
2981
+ return [];
2982
+ }
2983
+ const statementOf2 = /* @__PURE__ */ new Map();
2984
+ const statements = [];
2985
+ for (const call of calls) {
2986
+ const statement = enclosingStatement(call, body);
2987
+ statementOf2.set(call.id, statement);
2988
+ statements.push(statement);
2989
+ }
2990
+ const lowered = lowerRubyBody(body, statements);
2991
+ const enumerated = enumerateOrDegrade2(
2992
+ {
2993
+ statements: lowered.statements,
2994
+ terminalsByStmt: lowered.terminalsByStmt
2995
+ },
2996
+ statements
2997
+ );
2998
+ return calls.map((call) => {
2999
+ const statement = statementOf2.get(call.id);
3000
+ const conditions = sharedGatingConditions(
3001
+ statement === void 0 ? void 0 : enumerated.byTerminal.get(statement)
3002
+ );
3003
+ return {
3004
+ type: "invocation",
3005
+ callee: calleeText(call),
3006
+ args: argsOf(call),
3007
+ async: false,
3008
+ ...conditions.length > 0 ? { preconditions: conditions } : {}
3009
+ };
3010
+ });
3011
+ }
3012
+
2793
3013
  // src/storage.ts
2794
3014
  import { deriveOnDemand as deriveOnDemand2, evaluate as evaluate2 } from "@suss/datalog";
2795
3015
  import { storageBinding } from "@suss/ir-core";
@@ -3093,7 +3313,23 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
3093
3313
  if (!reachesConfiguredBase(ancestry, info.qualifiedName, pattern.baseClassNames)) {
3094
3314
  return [];
3095
3315
  }
3316
+ const filters = controllerFilters(pattern, ancestry);
3096
3317
  const units = [];
3318
+ for (const filter of filters) {
3319
+ const displayPath = options.displayPathOf?.(filter.file) ?? filter.file;
3320
+ const raw = filterUnit(
3321
+ filter,
3322
+ pattern,
3323
+ displayPath,
3324
+ bodyOfMethod(filter.method, options)
3325
+ );
3326
+ units.push(raw);
3327
+ options.onReachSeed?.(raw, {
3328
+ file: filter.file,
3329
+ node: filter.method,
3330
+ enclosingQualifiedName: filter.enclosingQualifiedName
3331
+ });
3332
+ }
3097
3333
  for (const block of ownBlocks) {
3098
3334
  if (block.info.bodyNode === null) {
3099
3335
  continue;
@@ -3105,6 +3341,12 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
3105
3341
  if ((visibility.get(actionName) ?? "public") !== "public") {
3106
3342
  continue;
3107
3343
  }
3344
+ const around = filters.filter((filter) => filterCoversAction(filter, actionName)).map(
3345
+ (filter) => filterReference(
3346
+ filter,
3347
+ options.displayPathOf?.(filter.file) ?? filter.file
3348
+ )
3349
+ );
3108
3350
  const raw = buildControllerActionUnit(
3109
3351
  pack,
3110
3352
  pattern,
@@ -3112,7 +3354,8 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
3112
3354
  actionName,
3113
3355
  method,
3114
3356
  options.filePath,
3115
- options
3357
+ options,
3358
+ around
3116
3359
  );
3117
3360
  units.push(raw);
3118
3361
  options.onReachSeed?.(raw, {
@@ -3124,7 +3367,7 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
3124
3367
  }
3125
3368
  return units;
3126
3369
  }
3127
- function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actionName, method, filePath, bodyRead) {
3370
+ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actionName, method, filePath, bodyRead, wrappers = []) {
3128
3371
  const range = rangeOf(method);
3129
3372
  const route = pattern.routeFor(controllerQualifiedName, actionName);
3130
3373
  const body = bodyOfMethod(method, bodyRead);
@@ -3179,7 +3422,8 @@ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actio
3179
3422
  ],
3180
3423
  bodyContent: body.bodyContent ?? "absent",
3181
3424
  dependencyCalls: [],
3182
- declaredContract: null
3425
+ declaredContract: null,
3426
+ ...wrappers.length > 0 ? { wrappers: [...wrappers] } : {}
3183
3427
  };
3184
3428
  }
3185
3429
  function routingGapUnit(pattern, gaps) {
@@ -3786,6 +4030,7 @@ import {
3786
4030
  import { Database as Database2 } from "@suss/datalog";
3787
4031
  import {
3788
4032
  assembleSummary as assembleSummary2,
4033
+ composeWrappers,
3789
4034
  createCacheLayer,
3790
4035
  createTimer,
3791
4036
  moduleInitStructure,
@@ -4375,6 +4620,14 @@ function inheritedMethodsIn(packs) {
4375
4620
  }
4376
4621
  return found;
4377
4622
  }
4623
+ function alreadyDiscovered(seen, raw) {
4624
+ const key = `${raw.identity.file}::${raw.identity.name}::${raw.identity.range.start}`;
4625
+ if (seen.has(key)) {
4626
+ return true;
4627
+ }
4628
+ seen.add(key);
4629
+ return false;
4630
+ }
4378
4631
  async function extractRubyProject(options) {
4379
4632
  const timer = options.onTiming !== void 0 ? createTimer() : noopTimer();
4380
4633
  const cacheDir = adapterStamp.declineWhenRunFromSource(
@@ -4436,6 +4689,7 @@ async function extractRubyProject(options) {
4436
4689
  const displayPathOf = (file) => options.workspaceRoot !== void 0 ? path4.relative(options.workspaceRoot, file) : file;
4437
4690
  const seeds = [];
4438
4691
  const summariesBySeed = /* @__PURE__ */ new Map();
4692
+ const discovered = /* @__PURE__ */ new Set();
4439
4693
  for (const { file, root } of parsed) {
4440
4694
  const displayPath = displayPathOf(file);
4441
4695
  const seedByRaw = /* @__PURE__ */ new Map();
@@ -4448,10 +4702,14 @@ async function extractRubyProject(options) {
4448
4702
  cache,
4449
4703
  ...storagePatterns.length > 0 ? { storage: { facts: db, patterns: storagePatterns } } : {},
4450
4704
  inheritedMethods,
4705
+ displayPathOf,
4451
4706
  onReachSeed: (raw, seed) => seedByRaw.set(raw, seed)
4452
4707
  })
4453
4708
  );
4454
4709
  for (const raw of rawUnits) {
4710
+ if (alreadyDiscovered(discovered, raw)) {
4711
+ continue;
4712
+ }
4455
4713
  const summary = timer.time(
4456
4714
  "summarize",
4457
4715
  () => assembleSummary2(raw, { gapHandling: "permissive" })
@@ -4554,12 +4812,18 @@ async function extractRubyProject(options) {
4554
4812
  }
4555
4813
  disambiguateSummaryIds(summaries);
4556
4814
  linkCallsToSummaries(summaries);
4815
+ const composed = timer.time(
4816
+ "summarize",
4817
+ () => composeWrappers(summaries, {
4818
+ gapHandling: options.gapHandling ?? "permissive"
4819
+ })
4820
+ );
4557
4821
  await timer.timeAsync("cache.write", async () => {
4558
- if (cacheDir === null || summaries.length === 0) {
4822
+ if (cacheDir === null || composed.length === 0) {
4559
4823
  return;
4560
4824
  }
4561
4825
  try {
4562
- await extractionCache.write(cacheInput, summaries);
4826
+ await extractionCache.write(cacheInput, composed);
4563
4827
  } catch {
4564
4828
  }
4565
4829
  });
@@ -4568,11 +4832,11 @@ async function extractRubyProject(options) {
4568
4832
  packs: options.packs,
4569
4833
  tallies,
4570
4834
  filesWalked: options.files.length,
4571
- summaries
4835
+ summaries: composed
4572
4836
  })
4573
4837
  );
4574
4838
  options.onTiming?.(timer.report());
4575
- return { summaries, facts: db };
4839
+ return { summaries: composed, facts: db };
4576
4840
  }
4577
4841
  function fileDependenciesOf(db, displayPathOf) {
4578
4842
  const byFile = /* @__PURE__ */ new Map();