@suss/adapter-ruby 0.22.3 → 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.cjs CHANGED
@@ -288,9 +288,21 @@ var PATH_CONVENTIONS = {
288
288
  railsUnderscore: underscoreConstantPath
289
289
  };
290
290
  function resolveConstantFile(root, qualifiedName, convention) {
291
- const relative = PATH_CONVENTIONS[convention](qualifiedName);
292
- const file = import_node_path.default.join(root, `${relative}.rb`);
293
- return import_node_fs.default.existsSync(file) ? file : null;
291
+ const relative = `${PATH_CONVENTIONS[convention](qualifiedName)}.rb`;
292
+ for (const candidate of [root, ...autoloadDirectories(root)]) {
293
+ const file = import_node_path.default.join(candidate, relative);
294
+ if (import_node_fs.default.existsSync(file)) {
295
+ return file;
296
+ }
297
+ }
298
+ return null;
299
+ }
300
+ function autoloadDirectories(root) {
301
+ try {
302
+ return import_node_fs.default.readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => import_node_path.default.join(root, entry.name)).sort();
303
+ } catch {
304
+ return [];
305
+ }
294
306
  }
295
307
 
296
308
  // src/scope.ts
@@ -699,8 +711,9 @@ function isDefaultedAt(node) {
699
711
  return false;
700
712
  }
701
713
 
702
- // src/paths/effects.ts
714
+ // src/responseStatus.ts
703
715
  var import_extractor3 = require("@suss/extractor");
716
+ var import_values4 = require("@suss/values");
704
717
 
705
718
  // src/paths/bareCalls.ts
706
719
  var PARAMETER_LIST_TYPES = [
@@ -1076,175 +1089,6 @@ function lowerRubyBody(body, terminals, responseCalls = []) {
1076
1089
  };
1077
1090
  }
1078
1091
 
1079
- // src/paths/effects.ts
1080
- var RAISE_NAMES2 = /* @__PURE__ */ new Set(["raise", "fail"]);
1081
- var BLOCK_TYPES = /* @__PURE__ */ new Set([
1082
- "body_statement",
1083
- "then",
1084
- "else",
1085
- "do_block",
1086
- "block"
1087
- ]);
1088
- var LITERAL_ARGS = {
1089
- string: (node) => ({
1090
- kind: "string",
1091
- value: node.text.replace(/^["']|["']$/g, "")
1092
- }),
1093
- integer: (node) => {
1094
- const value = Number.parseInt(node.text, 10);
1095
- return Number.isNaN(value) ? null : { kind: "number", value };
1096
- },
1097
- float: (node) => {
1098
- const value = Number.parseFloat(node.text);
1099
- return Number.isNaN(value) ? null : { kind: "number", value };
1100
- },
1101
- true: () => ({ kind: "boolean", value: true }),
1102
- false: () => ({ kind: "boolean", value: false }),
1103
- simple_symbol: (node) => ({ kind: "string", value: node.text.slice(1) })
1104
- };
1105
- function children(node) {
1106
- return node.namedChildren.filter((child) => child !== null);
1107
- }
1108
- function withoutChainLinks(calls) {
1109
- const isLink = /* @__PURE__ */ new Set();
1110
- for (const call of calls) {
1111
- const receiver = field(call, "receiver");
1112
- if (receiver !== null && receiver.type === "call") {
1113
- isLink.add(receiver.id);
1114
- }
1115
- }
1116
- return calls.filter((call) => !isLink.has(call.id));
1117
- }
1118
- function calleeMethodName(call) {
1119
- if (call.type === "identifier") {
1120
- return call.text;
1121
- }
1122
- const method = field(call, "method");
1123
- if (method !== null) {
1124
- return method.text;
1125
- }
1126
- return field(call, "receiver") !== null ? "call" : children(call)[0]?.text;
1127
- }
1128
- function isPropertyRead(node) {
1129
- if (field(node, "receiver") === null || field(node, "arguments") !== null) {
1130
- return false;
1131
- }
1132
- return calleeMethodName(node) !== "call";
1133
- }
1134
- function isRaise2(node) {
1135
- const method = field(node, "method") ?? children(node)[0] ?? null;
1136
- return field(node, "receiver") === null && method !== null && RAISE_NAMES2.has(method.text);
1137
- }
1138
- var NO_INHERITED_METHODS = /* @__PURE__ */ new Set();
1139
- function isInherited(node, inherited) {
1140
- const method = field(node, "method");
1141
- return field(node, "receiver") === null && method !== null && inherited.has(method.text);
1142
- }
1143
- function isCall(node, locals, inherited) {
1144
- if (node.type === "call") {
1145
- return !isPropertyRead(node) && !isRaise2(node) && !isInherited(node, inherited);
1146
- }
1147
- return isBareMethodCall(node, locals) && !RAISE_NAMES2.has(node.text) && !inherited.has(node.text);
1148
- }
1149
- function collectCalls(node, locals, inherited, found) {
1150
- for (const child of children(node)) {
1151
- if (OWN_BODY_TYPES.has(child.type)) {
1152
- continue;
1153
- }
1154
- if (isCall(child, locals, inherited)) {
1155
- found.push(child);
1156
- }
1157
- collectCalls(child, locals, inherited, found);
1158
- }
1159
- return found;
1160
- }
1161
- function bodyCalls(definitionNode, inherited = NO_INHERITED_METHODS) {
1162
- const body = field(definitionNode, "body");
1163
- if (body === null) {
1164
- return [];
1165
- }
1166
- return collectCalls(body, localNamesIn(definitionNode), inherited, []);
1167
- }
1168
- function calleeText(call) {
1169
- const receiver = field(call, "receiver");
1170
- const method = calleeMethodName(call);
1171
- if (method === void 0) {
1172
- return call.text;
1173
- }
1174
- return receiver === null ? method : `${receiver.text}.${method}`;
1175
- }
1176
- function argOf(node) {
1177
- const literal = LITERAL_ARGS[node.type]?.(node);
1178
- if (literal !== null && literal !== void 0) {
1179
- return literal;
1180
- }
1181
- if (node.type === "call" && !isPropertyRead(node)) {
1182
- return { kind: "call", callee: calleeText(node), args: argsOf(node) };
1183
- }
1184
- return { kind: "identifier", name: node.text };
1185
- }
1186
- function argsOf(call) {
1187
- const args = field(call, "arguments");
1188
- if (args === null) {
1189
- return [];
1190
- }
1191
- return children(args).map(
1192
- (child) => child.type === "pair" ? argOf(field(child, "value") ?? child) : argOf(child)
1193
- );
1194
- }
1195
- function enclosingStatement(call, body) {
1196
- let current = call;
1197
- while (current.parent !== null) {
1198
- if (BLOCK_TYPES.has(current.parent.type) || current.parent.id === body.id) {
1199
- return current;
1200
- }
1201
- current = current.parent;
1202
- }
1203
- return current;
1204
- }
1205
- function invocationEffects(definitionNode, inherited = NO_INHERITED_METHODS) {
1206
- const body = field(definitionNode, "body");
1207
- if (body === null) {
1208
- return [];
1209
- }
1210
- const calls = withoutChainLinks(bodyCalls(definitionNode, inherited));
1211
- if (calls.length === 0) {
1212
- return [];
1213
- }
1214
- const statementOf2 = /* @__PURE__ */ new Map();
1215
- const statements = [];
1216
- for (const call of calls) {
1217
- const statement = enclosingStatement(call, body);
1218
- statementOf2.set(call.id, statement);
1219
- statements.push(statement);
1220
- }
1221
- const lowered = lowerRubyBody(body, statements);
1222
- const enumerated = (0, import_extractor3.enumerateOrDegrade)(
1223
- {
1224
- statements: lowered.statements,
1225
- terminalsByStmt: lowered.terminalsByStmt
1226
- },
1227
- statements
1228
- );
1229
- return calls.map((call) => {
1230
- const statement = statementOf2.get(call.id);
1231
- const conditions = (0, import_extractor3.sharedGatingConditions)(
1232
- statement === void 0 ? void 0 : enumerated.byTerminal.get(statement)
1233
- );
1234
- return {
1235
- type: "invocation",
1236
- callee: calleeText(call),
1237
- args: argsOf(call),
1238
- async: false,
1239
- ...conditions.length > 0 ? { preconditions: conditions } : {}
1240
- };
1241
- });
1242
- }
1243
-
1244
- // src/responseStatus.ts
1245
- var import_extractor4 = require("@suss/extractor");
1246
- var import_values4 = require("@suss/values");
1247
-
1248
1092
  // src/values/evaluator.ts
1249
1093
  var import_resolution2 = require("@suss/resolution");
1250
1094
  var import_values2 = require("@suss/values");
@@ -1300,7 +1144,7 @@ function paramNameOf(param) {
1300
1144
  function declaredParamNames(method) {
1301
1145
  const params = field(method, "parameters");
1302
1146
  const declared = /* @__PURE__ */ new Set();
1303
- for (const param of params === null ? [] : children2(params)) {
1147
+ for (const param of params === null ? [] : children(params)) {
1304
1148
  const name = paramNameOf(param);
1305
1149
  if (name !== null) {
1306
1150
  declared.add(name.text);
@@ -1314,7 +1158,7 @@ function readKey(filePath, node, enclosing) {
1314
1158
  }
1315
1159
  const parent = node.parent;
1316
1160
  if (parent !== null && parent.type === "call" && field(parent, "method")?.id === node.id && field(parent, "receiver") !== null) {
1317
- return isPropertyRead2(parent) ? nodeId(filePath, parent) : nodeId(filePath, node);
1161
+ return isPropertyRead(parent) ? nodeId(filePath, parent) : nodeId(filePath, node);
1318
1162
  }
1319
1163
  if (enclosing !== null && declaredParamNames(enclosing).has(node.text)) {
1320
1164
  return `${nodeId(filePath, enclosing)}#${node.text}`;
@@ -1342,7 +1186,7 @@ var WRITTEN_VALUE_TYPES = /* @__PURE__ */ new Set([
1342
1186
  "parenthesized_statements"
1343
1187
  ]);
1344
1188
  var ARRAY_TYPES = /* @__PURE__ */ new Set(["array", "string_array", "symbol_array"]);
1345
- function children2(node) {
1189
+ function children(node) {
1346
1190
  return node.namedChildren.filter((child) => child !== null);
1347
1191
  }
1348
1192
  function add(emitter, relation, ...tuple) {
@@ -1370,8 +1214,8 @@ function pairKeyText(key) {
1370
1214
  }
1371
1215
  return null;
1372
1216
  }
1373
- function isPropertyRead2(node) {
1374
- return node.type === "call" && field(node, "receiver") !== null && field(node, "arguments") === null && children2(node).every(
1217
+ function isPropertyRead(node) {
1218
+ return node.type === "call" && field(node, "receiver") !== null && field(node, "arguments") === null && children(node).every(
1375
1219
  (child) => child.type !== "do_block" && child.type !== "block"
1376
1220
  );
1377
1221
  }
@@ -1410,7 +1254,7 @@ function emitCall(emitter, call) {
1410
1254
  }
1411
1255
  const args = field(call, "arguments");
1412
1256
  let position = 0;
1413
- for (const argument of args === null ? [] : children2(args)) {
1257
+ for (const argument of args === null ? [] : children(args)) {
1414
1258
  if (argument.type === "pair") {
1415
1259
  const key = field(argument, "key");
1416
1260
  const value = field(argument, "value");
@@ -1440,7 +1284,7 @@ function emitArray(emitter, array) {
1440
1284
  const objectKey = nodeId(emitter.filePath, array);
1441
1285
  add(emitter, "objectValue", objectKey);
1442
1286
  let position = 0;
1443
- for (const element of children2(array)) {
1287
+ for (const element of children(array)) {
1444
1288
  add(
1445
1289
  emitter,
1446
1290
  "holdsProperty",
@@ -1454,7 +1298,7 @@ function emitArray(emitter, array) {
1454
1298
  function emitHash(emitter, hash) {
1455
1299
  const objectKey = nodeId(emitter.filePath, hash);
1456
1300
  add(emitter, "objectValue", objectKey);
1457
- for (const pair of children2(hash)) {
1301
+ for (const pair of children(hash)) {
1458
1302
  if (pair.type !== "pair") {
1459
1303
  continue;
1460
1304
  }
@@ -1468,7 +1312,7 @@ function emitHash(emitter, hash) {
1468
1312
  }
1469
1313
  }
1470
1314
  function walkExpressions(node, visit) {
1471
- for (const child of children2(node)) {
1315
+ for (const child of children(node)) {
1472
1316
  if (OWN_BODY_TYPES.has(child.type)) {
1473
1317
  continue;
1474
1318
  }
@@ -1478,7 +1322,7 @@ function walkExpressions(node, visit) {
1478
1322
  }
1479
1323
  function emitExpressionFacts(emitter, node) {
1480
1324
  walkExpressions(node, (child) => {
1481
- if (isPropertyRead2(child)) {
1325
+ if (isPropertyRead(child)) {
1482
1326
  emitPropertyRead(emitter, child);
1483
1327
  } else if (child.type === "call") {
1484
1328
  emitCall(emitter, child);
@@ -1498,7 +1342,7 @@ function emitExpressionFacts(emitter, node) {
1498
1342
  });
1499
1343
  }
1500
1344
  function implicitReturn(body) {
1501
- const statements = children2(body).filter(
1345
+ const statements = children(body).filter(
1502
1346
  (child) => child.type !== "rescue" && child.type !== "ensure"
1503
1347
  );
1504
1348
  const last = statements[statements.length - 1];
@@ -1510,7 +1354,7 @@ function emitMethodFacts(emitter, method) {
1510
1354
  const params = field(method, "parameters");
1511
1355
  const declared = /* @__PURE__ */ new Set();
1512
1356
  let position = 0;
1513
- for (const param of params === null ? [] : children2(params)) {
1357
+ for (const param of params === null ? [] : children(params)) {
1514
1358
  const paramName = paramNameOf(param);
1515
1359
  if (paramName !== null) {
1516
1360
  declared.add(paramName.text);
@@ -1529,7 +1373,7 @@ function emitMethodFacts(emitter, method) {
1529
1373
  return funcKey;
1530
1374
  }
1531
1375
  const recordNested = (node) => {
1532
- for (const child of children2(node)) {
1376
+ for (const child of children(node)) {
1533
1377
  if (OWN_BODY_TYPES.has(child.type)) {
1534
1378
  add(emitter, "containsFn", funcKey, nodeId(emitter.filePath, child));
1535
1379
  continue;
@@ -1540,8 +1384,8 @@ function emitMethodFacts(emitter, method) {
1540
1384
  recordNested(body);
1541
1385
  walkExpressions(body, (child) => {
1542
1386
  if (child.type === "return") {
1543
- const first = children2(child)[0];
1544
- const returned = first?.type === "argument_list" ? children2(first)[0] : first;
1387
+ const first = children(child)[0];
1388
+ const returned = first?.type === "argument_list" ? children(first)[0] : first;
1545
1389
  if (returned !== void 0) {
1546
1390
  add(inside, "returnsValue", funcKey, valueKey(inside, returned));
1547
1391
  }
@@ -1585,13 +1429,13 @@ function emitClassFacts(emitter, cls) {
1585
1429
  const classKey = nodeId(emitter.filePath, cls);
1586
1430
  add(emitter, "objectValue", classKey);
1587
1431
  const superclass = field(cls, "superclass");
1588
- const base = superclass === null ? null : children2(superclass)[0] ?? null;
1432
+ const base = superclass === null ? null : children(superclass)[0] ?? null;
1589
1433
  if (base !== null) {
1590
1434
  add(emitter, "extends", classKey, valueKey(emitter, base));
1591
1435
  add(emitter, "extendsNamed", classKey, base.text);
1592
1436
  }
1593
1437
  const body = field(cls, "body");
1594
- for (const statement of body === null ? [] : children2(body)) {
1438
+ for (const statement of body === null ? [] : children(body)) {
1595
1439
  if (statement.type === "assignment") {
1596
1440
  const left = field(statement, "left");
1597
1441
  const right = field(statement, "right");
@@ -1633,7 +1477,7 @@ function emitValueFacts(db, filePath, root) {
1633
1477
  add(emitter, "exportsAs", filePath, name.text, key);
1634
1478
  };
1635
1479
  const walk = (node) => {
1636
- for (const child of children2(node)) {
1480
+ for (const child of children(node)) {
1637
1481
  if (NESTING_TYPES.has(child.type)) {
1638
1482
  declaresName(child, emitClassFacts(emitter, child));
1639
1483
  continue;
@@ -1664,7 +1508,7 @@ var OWN_ROOT_TYPES = /* @__PURE__ */ new Set([
1664
1508
  "module",
1665
1509
  "singleton_class"
1666
1510
  ]);
1667
- var BLOCK_TYPES2 = /* @__PURE__ */ new Set(["block", "do_block"]);
1511
+ var BLOCK_TYPES = /* @__PURE__ */ new Set(["block", "do_block"]);
1668
1512
  var METHOD_TYPES2 = /* @__PURE__ */ new Set(["method", "singleton_method"]);
1669
1513
  var NAME_TYPES = /* @__PURE__ */ new Set([
1670
1514
  "identifier",
@@ -2170,7 +2014,7 @@ function isTailExpression(node) {
2170
2014
  if (owner.type === "block" && owner.parent?.type === "lambda") {
2171
2015
  return true;
2172
2016
  }
2173
- if (METHOD_TYPES2.has(owner.type) || BLOCK_TYPES2.has(owner.type)) {
2017
+ if (METHOD_TYPES2.has(owner.type) || BLOCK_TYPES.has(owner.type)) {
2174
2018
  return true;
2175
2019
  }
2176
2020
  if (owner.type === "when") {
@@ -2189,7 +2033,7 @@ function isRoot(node) {
2189
2033
  if (OWN_ROOT_TYPES.has(node.type)) {
2190
2034
  return true;
2191
2035
  }
2192
- return BLOCK_TYPES2.has(node.type) && node.parent?.type === "call";
2036
+ return BLOCK_TYPES.has(node.type) && node.parent?.type === "call";
2193
2037
  }
2194
2038
  function isPathStatement(node, parent) {
2195
2039
  if (node.type === "elsif") {
@@ -2243,7 +2087,7 @@ function functionOf(node) {
2243
2087
  body: bodyOf(block)
2244
2088
  };
2245
2089
  }
2246
- if (BLOCK_TYPES2.has(node.type)) {
2090
+ if (BLOCK_TYPES.has(node.type)) {
2247
2091
  return {
2248
2092
  parameters: parametersOf(field(node, "parameters")),
2249
2093
  body: bodyOf(node)
@@ -2726,20 +2570,20 @@ function collectReturns(node, found) {
2726
2570
  function readingOfCall(call, declaration, names) {
2727
2571
  const argument = statusArgumentOf(call, declaration);
2728
2572
  if (argument === null) {
2729
- return declaration.defaultStatusCode === void 0 ? import_extractor4.absentReading : (0, import_extractor4.writtenReading)(declaration.defaultStatusCode, rangeOf(call));
2573
+ return declaration.defaultStatusCode === void 0 ? import_extractor3.absentReading : (0, import_extractor3.writtenReading)(declaration.defaultStatusCode, rangeOf(call));
2730
2574
  }
2731
2575
  const status = statusNumberOf(argument, names);
2732
2576
  if (status === null) {
2733
- return (0, import_extractor4.unreadableReading)(
2577
+ return (0, import_extractor3.unreadableReading)(
2734
2578
  "This response writes a status that does not settle on a number here, so this outcome claims none",
2735
2579
  rangeOf(argument)
2736
2580
  );
2737
2581
  }
2738
- return (0, import_extractor4.writtenReading)(status, rangeOf(argument));
2582
+ return (0, import_extractor3.writtenReading)(status, rangeOf(argument));
2739
2583
  }
2740
2584
  function effectsReaching(effects, conditions) {
2741
2585
  return effects.filter(
2742
- (effect) => (0, import_extractor4.guardsHoldOn)(
2586
+ (effect) => (0, import_extractor3.guardsHoldOn)(
2743
2587
  effect.type === "invocation" ? effect.preconditions : void 0,
2744
2588
  conditions
2745
2589
  )
@@ -2753,7 +2597,10 @@ function conditionOf(condition) {
2753
2597
  source: condition.source
2754
2598
  };
2755
2599
  }
2756
- function branchOf(outcome, pattern, effects, extraEffects) {
2600
+ function branchOf(outcome, pattern, effects, extraEffects, options) {
2601
+ if (outcome.fellThrough === true && options.fallthrough === "handOn") {
2602
+ return handsOnBranch(outcome, effects, extraEffects);
2603
+ }
2757
2604
  return {
2758
2605
  conditions: outcome.conditions,
2759
2606
  terminal: {
@@ -2778,7 +2625,28 @@ function branchOf(outcome, pattern, effects, extraEffects) {
2778
2625
  isDefault: outcome.conditions.length === 0
2779
2626
  };
2780
2627
  }
2781
- function responseBranches(method, pattern, effects, extraEffects) {
2628
+ function handsOnBranch(outcome, effects, extraEffects) {
2629
+ return {
2630
+ conditions: outcome.conditions,
2631
+ terminal: {
2632
+ kind: "delegate",
2633
+ statusCode: null,
2634
+ body: null,
2635
+ exceptionType: null,
2636
+ message: null,
2637
+ component: null,
2638
+ renderTree: null,
2639
+ delegateTarget: null,
2640
+ emitEvent: null,
2641
+ location: outcome.location
2642
+ },
2643
+ effects: effectsReaching(effects, outcome.conditions),
2644
+ ...extraEffects === void 0 ? {} : { extraEffects },
2645
+ location: outcome.location,
2646
+ isDefault: outcome.conditions.length === 0
2647
+ };
2648
+ }
2649
+ function responseBranches(method, pattern, effects, extraEffects, options = {}) {
2782
2650
  const declarations = pattern.responseStatusCalls ?? [];
2783
2651
  const body = field(method, "body");
2784
2652
  if (declarations.length === 0 || body === null) {
@@ -2793,7 +2661,7 @@ function responseBranches(method, pattern, effects, extraEffects) {
2793
2661
  (node) => lowered.terminalHome.has(node) && collectResponseCalls(node, byName, locals, []).length === 0
2794
2662
  );
2795
2663
  const terminals = [...responses, ...bareReturns];
2796
- const enumerated = (0, import_extractor4.enumerateOrDegrade)(
2664
+ const enumerated = (0, import_extractor3.enumerateOrDegrade)(
2797
2665
  {
2798
2666
  statements: lowered.statements,
2799
2667
  terminalsByStmt: lowered.terminalsByStmt
@@ -2804,7 +2672,7 @@ function responseBranches(method, pattern, effects, extraEffects) {
2804
2672
  const outcomes = [];
2805
2673
  for (const terminal of terminals) {
2806
2674
  const declaration = declarationOf(terminal, byName, locals);
2807
- const reading = declaration === void 0 ? import_extractor4.absentReading : readingOfCall(terminal, declaration, statusNames);
2675
+ const reading = declaration === void 0 ? import_extractor3.absentReading : readingOfCall(terminal, declaration, statusNames);
2808
2676
  for (const path7 of enumerated.byTerminal.get(terminal) ?? []) {
2809
2677
  outcomes.push({
2810
2678
  conditions: path7.map(conditionOf),
@@ -2816,18 +2684,370 @@ function responseBranches(method, pattern, effects, extraEffects) {
2816
2684
  for (const path7 of enumerated.fallthrough) {
2817
2685
  outcomes.push({
2818
2686
  conditions: path7.map(conditionOf),
2819
- reading: import_extractor4.absentReading,
2820
- location: rangeOf(method)
2687
+ reading: import_extractor3.absentReading,
2688
+ location: rangeOf(method),
2689
+ fellThrough: true
2821
2690
  });
2822
2691
  }
2823
2692
  if (outcomes.length === 0) {
2824
2693
  return null;
2825
2694
  }
2826
2695
  return outcomes.map(
2827
- (outcome) => branchOf(outcome, pattern, effects, extraEffects)
2696
+ (outcome) => branchOf(outcome, pattern, effects, extraEffects, options)
2828
2697
  );
2829
2698
  }
2830
2699
 
2700
+ // src/filters.ts
2701
+ function controllerFilters(pattern, ancestry) {
2702
+ const forms = pattern.filters ?? [];
2703
+ if (forms.length === 0) {
2704
+ return [];
2705
+ }
2706
+ const byName = new Map(forms.map((form) => [form.name, form]));
2707
+ const skipNames = /* @__PURE__ */ new Map();
2708
+ for (const form of forms) {
2709
+ if (form.skippedBy !== void 0) {
2710
+ skipNames.set(form.skippedBy, form.name);
2711
+ }
2712
+ }
2713
+ const declared = [];
2714
+ const skips = [];
2715
+ for (const { statement } of inheritedStatements(ancestry)) {
2716
+ const called = calledName(statement);
2717
+ if (called === null) {
2718
+ continue;
2719
+ }
2720
+ const form = byName.get(called);
2721
+ if (form !== void 0) {
2722
+ declared.push(...declarationsOf(statement, form));
2723
+ continue;
2724
+ }
2725
+ const skipped = skipNames.get(called);
2726
+ if (skipped !== void 0) {
2727
+ skips.push(...skipsOf(statement, skipped));
2728
+ }
2729
+ }
2730
+ const resolved = [];
2731
+ for (const declaration of applySkips(declared, skips)) {
2732
+ const found = methodInAncestry(ancestry, declaration.methodName);
2733
+ if (found.type !== "found") {
2734
+ continue;
2735
+ }
2736
+ resolved.push({
2737
+ filter: declaration.filter,
2738
+ methodName: declaration.methodName,
2739
+ method: found.method,
2740
+ file: found.block.file,
2741
+ enclosingQualifiedName: found.block.info.qualifiedName,
2742
+ only: declaration.only,
2743
+ except: declaration.except
2744
+ });
2745
+ }
2746
+ return [
2747
+ ...resolved.filter((one) => one.filter.onThrow !== true),
2748
+ ...resolved.filter((one) => one.filter.onThrow === true)
2749
+ ];
2750
+ }
2751
+ function filterCoversAction(filter, actionName) {
2752
+ if (filter.except.has(actionName)) {
2753
+ return false;
2754
+ }
2755
+ return filter.only === null || filter.only.has(actionName);
2756
+ }
2757
+ function filterReference(filter, displayPath) {
2758
+ return {
2759
+ file: displayPath,
2760
+ name: filter.methodName,
2761
+ ...filter.filter.onThrow === true ? { onThrow: true } : {}
2762
+ };
2763
+ }
2764
+ function filterUnit(filter, pattern, displayPath, body) {
2765
+ const range = rangeOf(filter.method);
2766
+ const branches = responseBranches(
2767
+ filter.method,
2768
+ pattern,
2769
+ body.effects ?? [],
2770
+ body.extraEffects,
2771
+ { fallthrough: "handOn" }
2772
+ );
2773
+ return {
2774
+ identity: {
2775
+ name: filter.methodName,
2776
+ nameKind: "binding",
2777
+ kind: "middleware",
2778
+ file: displayPath,
2779
+ range,
2780
+ span: spanOf(filter.method),
2781
+ exportName: filter.methodName,
2782
+ exportPath: [filter.enclosingQualifiedName, filter.methodName]
2783
+ },
2784
+ boundaryBinding: null,
2785
+ parameters: [],
2786
+ branches: branches ?? [handsOn(range, body)],
2787
+ bodyContent: body.bodyContent ?? "absent",
2788
+ dependencyCalls: [],
2789
+ declaredContract: null
2790
+ };
2791
+ }
2792
+ function handsOn(range, body) {
2793
+ return {
2794
+ conditions: [],
2795
+ terminal: {
2796
+ kind: "delegate",
2797
+ statusCode: null,
2798
+ body: null,
2799
+ exceptionType: null,
2800
+ message: null,
2801
+ component: null,
2802
+ renderTree: null,
2803
+ delegateTarget: null,
2804
+ emitEvent: null,
2805
+ location: range
2806
+ },
2807
+ effects: body.effects ?? [],
2808
+ ...body.extraEffects === void 0 ? {} : { extraEffects: body.extraEffects },
2809
+ location: range,
2810
+ isDefault: true
2811
+ };
2812
+ }
2813
+ function calledName(statement) {
2814
+ if (statement.type !== "call" || field(statement, "receiver") !== null) {
2815
+ return null;
2816
+ }
2817
+ return field(statement, "method")?.text ?? null;
2818
+ }
2819
+ function declarationsOf(statement, filter) {
2820
+ const args = readCallArgs(field(statement, "arguments"));
2821
+ const keywords = filter.actionKeywords;
2822
+ const only = keywords === void 0 ? null : namesUnder(args.keyword[keywords.include]);
2823
+ const except = keywords === void 0 ? null : namesUnder(args.keyword[keywords.exclude]);
2824
+ return methodNamesOf(args, filter).map((methodName) => ({
2825
+ filter,
2826
+ methodName,
2827
+ only,
2828
+ except: except ?? /* @__PURE__ */ new Set()
2829
+ }));
2830
+ }
2831
+ function methodNamesOf(args, filter) {
2832
+ if (filter.methodFrom === "withKeyword") {
2833
+ const named2 = args.keyword.with;
2834
+ const value = named2 === void 0 ? null : symbolValue(named2);
2835
+ return value === null ? [] : [value];
2836
+ }
2837
+ return args.positional.map((arg) => symbolValue(arg)).filter((name) => name !== null);
2838
+ }
2839
+ function skipsOf(statement, filterName) {
2840
+ const args = readCallArgs(field(statement, "arguments"));
2841
+ const actions = namesUnder(args.keyword.only);
2842
+ return args.positional.map((arg) => symbolValue(arg)).filter((name) => name !== null).map((methodName) => ({ filterName, methodName, actions }));
2843
+ }
2844
+ function applySkips(declared, skips) {
2845
+ const left = [];
2846
+ for (const declaration of declared) {
2847
+ const reaching = skips.filter(
2848
+ (skip) => skip.filterName === declaration.filter.name && skip.methodName === declaration.methodName
2849
+ );
2850
+ if (reaching.some((skip) => skip.actions === null)) {
2851
+ continue;
2852
+ }
2853
+ const except = new Set(declaration.except);
2854
+ for (const skip of reaching) {
2855
+ for (const action of skip.actions ?? []) {
2856
+ except.add(action);
2857
+ }
2858
+ }
2859
+ left.push({ ...declaration, except });
2860
+ }
2861
+ return left;
2862
+ }
2863
+ function namesUnder(node) {
2864
+ if (node === void 0) {
2865
+ return null;
2866
+ }
2867
+ const single = symbolValue(node);
2868
+ if (single !== null) {
2869
+ return /* @__PURE__ */ new Set([single]);
2870
+ }
2871
+ if (node.type !== "array" && node.type !== "symbol_array") {
2872
+ return null;
2873
+ }
2874
+ const names = /* @__PURE__ */ new Set();
2875
+ for (const child of bodyStatements(node)) {
2876
+ const bare = child.type === "bare_symbol" ? child.text : null;
2877
+ const name = symbolValue(child) ?? bare;
2878
+ if (name !== null) {
2879
+ names.add(name);
2880
+ }
2881
+ }
2882
+ return names;
2883
+ }
2884
+
2885
+ // src/paths/effects.ts
2886
+ var import_extractor4 = require("@suss/extractor");
2887
+ var RAISE_NAMES2 = /* @__PURE__ */ new Set(["raise", "fail"]);
2888
+ var BLOCK_TYPES2 = /* @__PURE__ */ new Set([
2889
+ "body_statement",
2890
+ "then",
2891
+ "else",
2892
+ "do_block",
2893
+ "block"
2894
+ ]);
2895
+ var LITERAL_ARGS = {
2896
+ string: (node) => ({
2897
+ kind: "string",
2898
+ value: node.text.replace(/^["']|["']$/g, "")
2899
+ }),
2900
+ integer: (node) => {
2901
+ const value = Number.parseInt(node.text, 10);
2902
+ return Number.isNaN(value) ? null : { kind: "number", value };
2903
+ },
2904
+ float: (node) => {
2905
+ const value = Number.parseFloat(node.text);
2906
+ return Number.isNaN(value) ? null : { kind: "number", value };
2907
+ },
2908
+ true: () => ({ kind: "boolean", value: true }),
2909
+ false: () => ({ kind: "boolean", value: false }),
2910
+ simple_symbol: (node) => ({ kind: "string", value: node.text.slice(1) })
2911
+ };
2912
+ function children2(node) {
2913
+ return node.namedChildren.filter((child) => child !== null);
2914
+ }
2915
+ function withoutChainLinks(calls) {
2916
+ const isLink = /* @__PURE__ */ new Set();
2917
+ for (const call of calls) {
2918
+ const receiver = field(call, "receiver");
2919
+ if (receiver !== null && receiver.type === "call") {
2920
+ isLink.add(receiver.id);
2921
+ }
2922
+ }
2923
+ return calls.filter((call) => !isLink.has(call.id));
2924
+ }
2925
+ function calleeMethodName(call) {
2926
+ if (call.type === "identifier") {
2927
+ return call.text;
2928
+ }
2929
+ const method = field(call, "method");
2930
+ if (method !== null) {
2931
+ return method.text;
2932
+ }
2933
+ return field(call, "receiver") !== null ? "call" : children2(call)[0]?.text;
2934
+ }
2935
+ function isPropertyRead2(node) {
2936
+ if (field(node, "receiver") === null || field(node, "arguments") !== null) {
2937
+ return false;
2938
+ }
2939
+ return calleeMethodName(node) !== "call";
2940
+ }
2941
+ function isRaise2(node) {
2942
+ const method = field(node, "method") ?? children2(node)[0] ?? null;
2943
+ return field(node, "receiver") === null && method !== null && RAISE_NAMES2.has(method.text);
2944
+ }
2945
+ var NO_INHERITED_METHODS = /* @__PURE__ */ new Set();
2946
+ function isInherited(node, inherited) {
2947
+ const method = field(node, "method");
2948
+ return field(node, "receiver") === null && method !== null && inherited.has(method.text);
2949
+ }
2950
+ function isCall(node, locals, inherited) {
2951
+ if (node.type === "call") {
2952
+ return !isPropertyRead2(node) && !isRaise2(node) && !isInherited(node, inherited);
2953
+ }
2954
+ return isBareMethodCall(node, locals) && !RAISE_NAMES2.has(node.text) && !inherited.has(node.text);
2955
+ }
2956
+ function collectCalls(node, locals, inherited, found) {
2957
+ for (const child of children2(node)) {
2958
+ if (OWN_BODY_TYPES.has(child.type)) {
2959
+ continue;
2960
+ }
2961
+ if (isCall(child, locals, inherited)) {
2962
+ found.push(child);
2963
+ }
2964
+ collectCalls(child, locals, inherited, found);
2965
+ }
2966
+ return found;
2967
+ }
2968
+ function bodyCalls(definitionNode, inherited = NO_INHERITED_METHODS) {
2969
+ const body = field(definitionNode, "body");
2970
+ if (body === null) {
2971
+ return [];
2972
+ }
2973
+ return collectCalls(body, localNamesIn(definitionNode), inherited, []);
2974
+ }
2975
+ function calleeText(call) {
2976
+ const receiver = field(call, "receiver");
2977
+ const method = calleeMethodName(call);
2978
+ if (method === void 0) {
2979
+ return call.text;
2980
+ }
2981
+ return receiver === null ? method : `${receiver.text}.${method}`;
2982
+ }
2983
+ function argOf(node) {
2984
+ const literal = LITERAL_ARGS[node.type]?.(node);
2985
+ if (literal !== null && literal !== void 0) {
2986
+ return literal;
2987
+ }
2988
+ if (node.type === "call" && !isPropertyRead2(node)) {
2989
+ return { kind: "call", callee: calleeText(node), args: argsOf(node) };
2990
+ }
2991
+ return { kind: "identifier", name: node.text };
2992
+ }
2993
+ function argsOf(call) {
2994
+ const args = field(call, "arguments");
2995
+ if (args === null) {
2996
+ return [];
2997
+ }
2998
+ return children2(args).map(
2999
+ (child) => child.type === "pair" ? argOf(field(child, "value") ?? child) : argOf(child)
3000
+ );
3001
+ }
3002
+ function enclosingStatement(call, body) {
3003
+ let current = call;
3004
+ while (current.parent !== null) {
3005
+ if (BLOCK_TYPES2.has(current.parent.type) || current.parent.id === body.id) {
3006
+ return current;
3007
+ }
3008
+ current = current.parent;
3009
+ }
3010
+ return current;
3011
+ }
3012
+ function invocationEffects(definitionNode, inherited = NO_INHERITED_METHODS) {
3013
+ const body = field(definitionNode, "body");
3014
+ if (body === null) {
3015
+ return [];
3016
+ }
3017
+ const calls = withoutChainLinks(bodyCalls(definitionNode, inherited));
3018
+ if (calls.length === 0) {
3019
+ return [];
3020
+ }
3021
+ const statementOf2 = /* @__PURE__ */ new Map();
3022
+ const statements = [];
3023
+ for (const call of calls) {
3024
+ const statement = enclosingStatement(call, body);
3025
+ statementOf2.set(call.id, statement);
3026
+ statements.push(statement);
3027
+ }
3028
+ const lowered = lowerRubyBody(body, statements);
3029
+ const enumerated = (0, import_extractor4.enumerateOrDegrade)(
3030
+ {
3031
+ statements: lowered.statements,
3032
+ terminalsByStmt: lowered.terminalsByStmt
3033
+ },
3034
+ statements
3035
+ );
3036
+ return calls.map((call) => {
3037
+ const statement = statementOf2.get(call.id);
3038
+ const conditions = (0, import_extractor4.sharedGatingConditions)(
3039
+ statement === void 0 ? void 0 : enumerated.byTerminal.get(statement)
3040
+ );
3041
+ return {
3042
+ type: "invocation",
3043
+ callee: calleeText(call),
3044
+ args: argsOf(call),
3045
+ async: false,
3046
+ ...conditions.length > 0 ? { preconditions: conditions } : {}
3047
+ };
3048
+ });
3049
+ }
3050
+
2831
3051
  // src/storage.ts
2832
3052
  var import_datalog2 = require("@suss/datalog");
2833
3053
  var import_ir_core = require("@suss/ir-core");
@@ -3127,7 +3347,23 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
3127
3347
  if (!reachesConfiguredBase(ancestry, info.qualifiedName, pattern.baseClassNames)) {
3128
3348
  return [];
3129
3349
  }
3350
+ const filters = controllerFilters(pattern, ancestry);
3130
3351
  const units = [];
3352
+ for (const filter of filters) {
3353
+ const displayPath = options.displayPathOf?.(filter.file) ?? filter.file;
3354
+ const raw = filterUnit(
3355
+ filter,
3356
+ pattern,
3357
+ displayPath,
3358
+ bodyOfMethod(filter.method, options)
3359
+ );
3360
+ units.push(raw);
3361
+ options.onReachSeed?.(raw, {
3362
+ file: filter.file,
3363
+ node: filter.method,
3364
+ enclosingQualifiedName: filter.enclosingQualifiedName
3365
+ });
3366
+ }
3131
3367
  for (const block of ownBlocks) {
3132
3368
  if (block.info.bodyNode === null) {
3133
3369
  continue;
@@ -3139,6 +3375,12 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
3139
3375
  if ((visibility.get(actionName) ?? "public") !== "public") {
3140
3376
  continue;
3141
3377
  }
3378
+ const around = filters.filter((filter) => filterCoversAction(filter, actionName)).map(
3379
+ (filter) => filterReference(
3380
+ filter,
3381
+ options.displayPathOf?.(filter.file) ?? filter.file
3382
+ )
3383
+ );
3142
3384
  const raw = buildControllerActionUnit(
3143
3385
  pack,
3144
3386
  pattern,
@@ -3146,7 +3388,8 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
3146
3388
  actionName,
3147
3389
  method,
3148
3390
  options.filePath,
3149
- options
3391
+ options,
3392
+ around
3150
3393
  );
3151
3394
  units.push(raw);
3152
3395
  options.onReachSeed?.(raw, {
@@ -3158,7 +3401,7 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
3158
3401
  }
3159
3402
  return units;
3160
3403
  }
3161
- function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actionName, method, filePath, bodyRead) {
3404
+ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actionName, method, filePath, bodyRead, wrappers = []) {
3162
3405
  const range = rangeOf(method);
3163
3406
  const route = pattern.routeFor(controllerQualifiedName, actionName);
3164
3407
  const body = bodyOfMethod(method, bodyRead);
@@ -3213,7 +3456,8 @@ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actio
3213
3456
  ],
3214
3457
  bodyContent: body.bodyContent ?? "absent",
3215
3458
  dependencyCalls: [],
3216
- declaredContract: null
3459
+ declaredContract: null,
3460
+ ...wrappers.length > 0 ? { wrappers: [...wrappers] } : {}
3217
3461
  };
3218
3462
  }
3219
3463
  function routingGapUnit(pattern, gaps) {
@@ -4379,6 +4623,14 @@ function inheritedMethodsIn(packs) {
4379
4623
  }
4380
4624
  return found;
4381
4625
  }
4626
+ function alreadyDiscovered(seen, raw) {
4627
+ const key = `${raw.identity.file}::${raw.identity.name}::${raw.identity.range.start}`;
4628
+ if (seen.has(key)) {
4629
+ return true;
4630
+ }
4631
+ seen.add(key);
4632
+ return false;
4633
+ }
4382
4634
  async function extractRubyProject(options) {
4383
4635
  const timer = options.onTiming !== void 0 ? (0, import_extractor10.createTimer)() : (0, import_extractor10.noopTimer)();
4384
4636
  const cacheDir = adapterStamp.declineWhenRunFromSource(
@@ -4440,6 +4692,7 @@ async function extractRubyProject(options) {
4440
4692
  const displayPathOf = (file) => options.workspaceRoot !== void 0 ? import_node_path4.default.relative(options.workspaceRoot, file) : file;
4441
4693
  const seeds = [];
4442
4694
  const summariesBySeed = /* @__PURE__ */ new Map();
4695
+ const discovered = /* @__PURE__ */ new Set();
4443
4696
  for (const { file, root } of parsed) {
4444
4697
  const displayPath = displayPathOf(file);
4445
4698
  const seedByRaw = /* @__PURE__ */ new Map();
@@ -4452,10 +4705,14 @@ async function extractRubyProject(options) {
4452
4705
  cache,
4453
4706
  ...storagePatterns.length > 0 ? { storage: { facts: db, patterns: storagePatterns } } : {},
4454
4707
  inheritedMethods,
4708
+ displayPathOf,
4455
4709
  onReachSeed: (raw, seed) => seedByRaw.set(raw, seed)
4456
4710
  })
4457
4711
  );
4458
4712
  for (const raw of rawUnits) {
4713
+ if (alreadyDiscovered(discovered, raw)) {
4714
+ continue;
4715
+ }
4459
4716
  const summary = timer.time(
4460
4717
  "summarize",
4461
4718
  () => (0, import_extractor10.assembleSummary)(raw, { gapHandling: "permissive" })
@@ -4558,12 +4815,18 @@ async function extractRubyProject(options) {
4558
4815
  }
4559
4816
  (0, import_behavioral_ir4.disambiguateSummaryIds)(summaries);
4560
4817
  (0, import_behavioral_ir4.linkCallsToSummaries)(summaries);
4818
+ const composed = timer.time(
4819
+ "summarize",
4820
+ () => (0, import_extractor10.composeWrappers)(summaries, {
4821
+ gapHandling: options.gapHandling ?? "permissive"
4822
+ })
4823
+ );
4561
4824
  await timer.timeAsync("cache.write", async () => {
4562
- if (cacheDir === null || summaries.length === 0) {
4825
+ if (cacheDir === null || composed.length === 0) {
4563
4826
  return;
4564
4827
  }
4565
4828
  try {
4566
- await extractionCache.write(cacheInput, summaries);
4829
+ await extractionCache.write(cacheInput, composed);
4567
4830
  } catch {
4568
4831
  }
4569
4832
  });
@@ -4572,11 +4835,11 @@ async function extractRubyProject(options) {
4572
4835
  packs: options.packs,
4573
4836
  tallies,
4574
4837
  filesWalked: options.files.length,
4575
- summaries
4838
+ summaries: composed
4576
4839
  })
4577
4840
  );
4578
4841
  options.onTiming?.(timer.report());
4579
- return { summaries, facts: db };
4842
+ return { summaries: composed, facts: db };
4580
4843
  }
4581
4844
  function fileDependenciesOf(db, displayPathOf) {
4582
4845
  const byFile = /* @__PURE__ */ new Map();