@suss/adapter-ruby 0.3.2-0 → 0.4.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
@@ -56,6 +56,7 @@ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${_
56
56
  var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
57
57
 
58
58
  // src/ast.ts
59
+ var import_extractor = require("@suss/extractor");
59
60
  function rangeOf(node) {
60
61
  return {
61
62
  start: node.startPosition.row + 1,
@@ -140,6 +141,8 @@ function readCallArgs(argumentList) {
140
141
  }
141
142
  return { positional, keyword };
142
143
  }
144
+ var NodeMap = class extends import_extractor.IdMap {
145
+ };
143
146
 
144
147
  // src/constantPath.ts
145
148
  var import_node_fs = __toESM(require("fs"), 1);
@@ -422,7 +425,352 @@ function inheritedStatements(ancestry) {
422
425
 
423
426
  // src/discovery.ts
424
427
  var import_behavioral_ir = require("@suss/behavioral-ir");
425
- var import_extractor = require("@suss/extractor");
428
+ var import_extractor3 = require("@suss/extractor");
429
+
430
+ // src/paths/effects.ts
431
+ var import_extractor2 = require("@suss/extractor");
432
+
433
+ // src/paths/lowering.ts
434
+ var RAISE_NAMES = /* @__PURE__ */ new Set(["raise", "fail"]);
435
+ var CAPTURES_RETURN = /* @__PURE__ */ new Set([
436
+ "method",
437
+ "singleton_method",
438
+ "lambda",
439
+ "class",
440
+ "module",
441
+ "singleton_class"
442
+ ]);
443
+ var KEYWORD_EXITS = {
444
+ return: "return",
445
+ break: "break",
446
+ next: "continue",
447
+ redo: "continue"
448
+ };
449
+ function isRaise(node) {
450
+ if (node.type !== "call") {
451
+ return false;
452
+ }
453
+ const receiver = field(node, "receiver");
454
+ const method = field(node, "method") ?? node.namedChildren[0] ?? null;
455
+ return receiver === null && method !== null && RAISE_NAMES.has(method.text);
456
+ }
457
+ function exitOf(node) {
458
+ if (isRaise(node)) {
459
+ return "throw";
460
+ }
461
+ return KEYWORD_EXITS[node.type] ?? null;
462
+ }
463
+ function exitKindOf(node) {
464
+ let sawReturn = false;
465
+ const raises = (current) => {
466
+ for (const child of current.namedChildren) {
467
+ if (child === null || CAPTURES_RETURN.has(child.type)) {
468
+ continue;
469
+ }
470
+ if (isRaise(child)) {
471
+ return true;
472
+ }
473
+ if (child.type === "return") {
474
+ sawReturn = true;
475
+ }
476
+ if (raises(child)) {
477
+ return true;
478
+ }
479
+ }
480
+ return false;
481
+ };
482
+ if (isRaise(node) || raises(node)) {
483
+ return "throw";
484
+ }
485
+ return node.type === "return" || sawReturn ? "return" : null;
486
+ }
487
+ function handleOf(node) {
488
+ return node === null ? { sourceText: "", expression: null } : { sourceText: node.text, expression: node };
489
+ }
490
+ function blockStatements(block) {
491
+ if (block === null) {
492
+ return [];
493
+ }
494
+ return block.namedChildren.filter((child) => child !== null);
495
+ }
496
+ var Lowerer = class {
497
+ constructor(terminals) {
498
+ this.terminals = terminals;
499
+ }
500
+ terminals;
501
+ terminalsByStmt = /* @__PURE__ */ new Map();
502
+ terminalHome = new NodeMap();
503
+ attach(statement, candidates) {
504
+ const found = candidates.flatMap((node) => {
505
+ const match = node === null ? void 0 : this.terminals.get(node.id);
506
+ return match === void 0 ? [] : [match];
507
+ });
508
+ if (found.length > 0) {
509
+ this.terminalsByStmt.set(statement, found);
510
+ for (const node of found) {
511
+ this.terminalHome.set(node, statement);
512
+ }
513
+ }
514
+ return statement;
515
+ }
516
+ lowerBlock(block) {
517
+ return blockStatements(block).map((statement) => this.lower(statement));
518
+ }
519
+ /** An elsif chain reads as an if nested in the previous else arm. */
520
+ lowerAlternative(node) {
521
+ if (node === null) {
522
+ return null;
523
+ }
524
+ if (node.type !== "elsif") {
525
+ return this.lowerBlock(node);
526
+ }
527
+ const nested = {
528
+ kind: "if",
529
+ condition: handleOf(field(node, "condition")),
530
+ thenBody: this.lowerBlock(field(node, "consequence")),
531
+ elseBody: this.lowerAlternative(field(node, "alternative")),
532
+ exitKind: exitKindOf(node)
533
+ };
534
+ this.attach(nested, [field(node, "condition")]);
535
+ return [nested];
536
+ }
537
+ lowerIf(node) {
538
+ return {
539
+ kind: "if",
540
+ condition: handleOf(field(node, "condition")),
541
+ thenBody: this.lowerBlock(field(node, "consequence")),
542
+ elseBody: this.lowerAlternative(field(node, "alternative")),
543
+ exitKind: exitKindOf(node)
544
+ };
545
+ }
546
+ lowerBegin(node) {
547
+ const rescues = node.namedChildren.filter(
548
+ (child) => child !== null && child.type === "rescue"
549
+ );
550
+ const ensureClause = node.namedChildren.find(
551
+ (child) => child !== null && child.type === "ensure"
552
+ ) ?? null;
553
+ const tryBody = node.namedChildren.filter(
554
+ (child) => child !== null && child.type !== "rescue" && child.type !== "ensure" && child.type !== "else"
555
+ );
556
+ return {
557
+ kind: "try",
558
+ tryBody: tryBody.map((statement) => this.lower(statement)),
559
+ catchBody: rescues.length > 0 ? rescues.flatMap((clause) => this.lowerBlock(field(clause, "body"))) : null,
560
+ finallyBody: ensureClause === null ? null : this.lowerBlock(ensureClause),
561
+ exitKind: exitKindOf(node)
562
+ };
563
+ }
564
+ lowerCase(node) {
565
+ const groups = [];
566
+ for (const child of node.namedChildren) {
567
+ if (child === null) {
568
+ continue;
569
+ }
570
+ if (child.type === "when") {
571
+ groups.push({
572
+ condition: handleOf(field(child, "pattern")),
573
+ hasTrailingBreak: false,
574
+ body: this.lowerBlock(field(child, "body"))
575
+ });
576
+ }
577
+ if (child.type === "else") {
578
+ groups.push({
579
+ condition: null,
580
+ hasTrailingBreak: false,
581
+ body: this.lowerBlock(child)
582
+ });
583
+ }
584
+ }
585
+ return { kind: "switch", groups, exitKind: exitKindOf(node) };
586
+ }
587
+ lower(node) {
588
+ const exit = exitOf(node);
589
+ if (exit !== null) {
590
+ return this.attach({ kind: "exit", exit, exitKind: exitKindOf(node) }, [
591
+ node
592
+ ]);
593
+ }
594
+ if (node.type === "if" || node.type === "unless") {
595
+ return this.attach(this.lowerIf(node), [field(node, "condition")]);
596
+ }
597
+ if (node.type === "while" || node.type === "until" || node.type === "for") {
598
+ const header = field(node, "condition") ?? field(node, "value");
599
+ return this.attach(
600
+ {
601
+ kind: "loop",
602
+ condition: handleOf(header),
603
+ body: this.lowerBlock(field(node, "body")),
604
+ exitKind: exitKindOf(node)
605
+ },
606
+ [header]
607
+ );
608
+ }
609
+ const attachedBlock = node.namedChildren.find(
610
+ (child) => child !== null && (child.type === "do_block" || child.type === "block")
611
+ );
612
+ if (node.type === "call" && attachedBlock !== void 0) {
613
+ return this.attach(
614
+ {
615
+ kind: "loop",
616
+ condition: handleOf(node),
617
+ body: this.lowerBlock(field(attachedBlock, "body") ?? attachedBlock),
618
+ exitKind: exitKindOf(node)
619
+ },
620
+ [node]
621
+ );
622
+ }
623
+ if (node.type === "begin" || node.type === "rescue_modifier") {
624
+ return this.attach(this.lowerBegin(node), []);
625
+ }
626
+ if (node.type === "case" || node.type === "case_match") {
627
+ return this.attach(this.lowerCase(node), [field(node, "value")]);
628
+ }
629
+ return this.attach({ kind: "opaque", exitKind: exitKindOf(node) }, [node]);
630
+ }
631
+ };
632
+ function lowerRubyBody(body, terminals) {
633
+ const lowerer = new Lowerer(
634
+ new Map(terminals.map((terminal) => [terminal.id, terminal]))
635
+ );
636
+ return {
637
+ statements: lowerer.lowerBlock(body),
638
+ terminalsByStmt: lowerer.terminalsByStmt,
639
+ terminalHome: lowerer.terminalHome
640
+ };
641
+ }
642
+
643
+ // src/paths/effects.ts
644
+ var DECLARATION_TYPES = /* @__PURE__ */ new Set([
645
+ "method",
646
+ "singleton_method",
647
+ "lambda",
648
+ "class",
649
+ "module",
650
+ "singleton_class"
651
+ ]);
652
+ var RAISE_NAMES2 = /* @__PURE__ */ new Set(["raise", "fail"]);
653
+ var BLOCK_TYPES = /* @__PURE__ */ new Set([
654
+ "body_statement",
655
+ "then",
656
+ "else",
657
+ "do_block",
658
+ "block"
659
+ ]);
660
+ var LITERAL_ARGS = {
661
+ string: (node) => ({
662
+ kind: "string",
663
+ value: node.text.replace(/^["']|["']$/g, "")
664
+ }),
665
+ integer: (node) => {
666
+ const value = Number.parseInt(node.text, 10);
667
+ return Number.isNaN(value) ? null : { kind: "number", value };
668
+ },
669
+ float: (node) => {
670
+ const value = Number.parseFloat(node.text);
671
+ return Number.isNaN(value) ? null : { kind: "number", value };
672
+ },
673
+ true: () => ({ kind: "boolean", value: true }),
674
+ false: () => ({ kind: "boolean", value: false }),
675
+ simple_symbol: (node) => ({ kind: "string", value: node.text.slice(1) })
676
+ };
677
+ function children(node) {
678
+ return node.namedChildren.filter((child) => child !== null);
679
+ }
680
+ function isPropertyRead(node) {
681
+ return field(node, "receiver") !== null && field(node, "arguments") === null;
682
+ }
683
+ function isRaise2(node) {
684
+ const method = field(node, "method") ?? children(node)[0] ?? null;
685
+ return field(node, "receiver") === null && method !== null && RAISE_NAMES2.has(method.text);
686
+ }
687
+ function bodyCalls(node, found = []) {
688
+ for (const child of children(node)) {
689
+ if (DECLARATION_TYPES.has(child.type)) {
690
+ continue;
691
+ }
692
+ if (child.type === "call" && !isPropertyRead(child) && !isRaise2(child)) {
693
+ found.push(child);
694
+ }
695
+ bodyCalls(child, found);
696
+ }
697
+ return found;
698
+ }
699
+ function calleeText(call) {
700
+ const receiver = field(call, "receiver");
701
+ const method = field(call, "method") ?? children(call)[0];
702
+ if (method === void 0) {
703
+ return call.text;
704
+ }
705
+ return receiver === null ? method.text : `${receiver.text}.${method.text}`;
706
+ }
707
+ function argOf(node) {
708
+ const literal = LITERAL_ARGS[node.type]?.(node);
709
+ if (literal !== null && literal !== void 0) {
710
+ return literal;
711
+ }
712
+ if (node.type === "call" && !isPropertyRead(node)) {
713
+ return { kind: "call", callee: calleeText(node), args: argsOf(node) };
714
+ }
715
+ return { kind: "identifier", name: node.text };
716
+ }
717
+ function argsOf(call) {
718
+ const args = field(call, "arguments");
719
+ if (args === null) {
720
+ return [];
721
+ }
722
+ return children(args).map(
723
+ (child) => child.type === "pair" ? argOf(field(child, "value") ?? child) : argOf(child)
724
+ );
725
+ }
726
+ function enclosingStatement(call, body) {
727
+ let current = call;
728
+ while (current.parent !== null) {
729
+ if (BLOCK_TYPES.has(current.parent.type) || current.parent.id === body.id) {
730
+ return current;
731
+ }
732
+ current = current.parent;
733
+ }
734
+ return current;
735
+ }
736
+ function invocationEffects(definitionNode) {
737
+ const body = field(definitionNode, "body");
738
+ if (body === null) {
739
+ return [];
740
+ }
741
+ const calls = bodyCalls(body);
742
+ if (calls.length === 0) {
743
+ return [];
744
+ }
745
+ const statementOf = /* @__PURE__ */ new Map();
746
+ const statements = [];
747
+ for (const call of calls) {
748
+ const statement = enclosingStatement(call, body);
749
+ statementOf.set(call.id, statement);
750
+ statements.push(statement);
751
+ }
752
+ const lowered = lowerRubyBody(body, statements);
753
+ const enumerated = (0, import_extractor2.enumerateOrDegrade)(
754
+ {
755
+ statements: lowered.statements,
756
+ terminalsByStmt: lowered.terminalsByStmt
757
+ },
758
+ statements
759
+ );
760
+ return calls.map((call) => {
761
+ const statement = statementOf.get(call.id);
762
+ const conditions = (0, import_extractor2.sharedGatingConditions)(
763
+ statement === void 0 ? void 0 : enumerated.byTerminal.get(statement)
764
+ );
765
+ return {
766
+ type: "invocation",
767
+ callee: calleeText(call),
768
+ args: argsOf(call),
769
+ async: false,
770
+ ...conditions.length > 0 ? { preconditions: conditions } : {}
771
+ };
772
+ });
773
+ }
426
774
 
427
775
  // src/typeShape.ts
428
776
  function scalarLookupName(qualifiedName, prefixes) {
@@ -557,10 +905,37 @@ async function graphqlObjectFieldUnits(pattern, pack, info, ownBlocks, options)
557
905
  (decl) => buildFieldUnit(pack, typeName, decl, options.filePath)
558
906
  );
559
907
  }
908
+ function branchesFor(body, range) {
909
+ if (body.effects === void 0) {
910
+ return [];
911
+ }
912
+ return [
913
+ {
914
+ conditions: [],
915
+ terminal: {
916
+ kind: "void",
917
+ statusCode: null,
918
+ body: null,
919
+ exceptionType: null,
920
+ message: null,
921
+ component: null,
922
+ renderTree: null,
923
+ delegateTarget: null,
924
+ emitEvent: null,
925
+ location: range
926
+ },
927
+ effects: body.effects,
928
+ location: range,
929
+ isDefault: true
930
+ }
931
+ ];
932
+ }
560
933
  function bodyOfMethod(method) {
934
+ const effects = invocationEffects(method);
561
935
  return {
562
936
  bodyContent: methodHasStatements(method) ? "statements" : "empty",
563
- readings: []
937
+ readings: [],
938
+ ...effects.length > 0 ? { effects } : {}
564
939
  };
565
940
  }
566
941
  var NO_METHOD_BEHIND_IT = {
@@ -568,7 +943,7 @@ var NO_METHOD_BEHIND_IT = {
568
943
  readings: []
569
944
  };
570
945
  function methodNotSettled(reason, range) {
571
- return { readings: [(0, import_extractor.unreadableReading)(reason, range)] };
946
+ return { readings: [(0, import_extractor3.unreadableReading)(reason, range)] };
572
947
  }
573
948
  function bodyFromLookup(found, range, subject, nothingThere) {
574
949
  const table = {
@@ -797,7 +1172,7 @@ function buildFieldUnit(pack, typeName, decl, filePath) {
797
1172
  fieldName: decl.fieldName
798
1173
  }),
799
1174
  parameters,
800
- branches: [],
1175
+ branches: branchesFor(decl.body, rangeOf(decl.node)),
801
1176
  ...decl.body.bodyContent !== void 0 ? { bodyContent: decl.body.bodyContent } : {},
802
1177
  dependencyCalls: [],
803
1178
  declaredContract: null,
@@ -857,10 +1232,445 @@ async function parseRuby(source) {
857
1232
  var import_node_fs3 = __toESM(require("fs"), 1);
858
1233
  var import_node_path3 = __toESM(require("path"), 1);
859
1234
  var import_datalog = require("@suss/datalog");
860
- var import_extractor2 = require("@suss/extractor");
1235
+ var import_extractor4 = require("@suss/extractor");
1236
+
1237
+ // src/facts/values.ts
1238
+ function nodeId(filePath, node) {
1239
+ return `${filePath}:${node.startIndex}-${node.endIndex}`;
1240
+ }
1241
+ function nameId(filePath, name) {
1242
+ return `${filePath}#${name}`;
1243
+ }
1244
+ var DECLARATION_TYPES2 = /* @__PURE__ */ new Set([
1245
+ "method",
1246
+ "singleton_method",
1247
+ "lambda",
1248
+ "class",
1249
+ "module",
1250
+ "singleton_class"
1251
+ ]);
1252
+ var WRITTEN_VALUE_TYPES = /* @__PURE__ */ new Set([
1253
+ "string",
1254
+ "integer",
1255
+ "float",
1256
+ "true",
1257
+ "false",
1258
+ "nil",
1259
+ "simple_symbol",
1260
+ "hash_key_symbol"
1261
+ ]);
1262
+ function children2(node) {
1263
+ return node.namedChildren.filter((child) => child !== null);
1264
+ }
1265
+ function add(emitter, relation, ...tuple) {
1266
+ emitter.db.add(relation, tuple);
1267
+ }
1268
+ function valueKey(emitter, value) {
1269
+ if (value.type !== "identifier" && value.type !== "constant") {
1270
+ return nodeId(emitter.filePath, value);
1271
+ }
1272
+ const enclosing = emitter.enclosing;
1273
+ if (enclosing !== null && enclosing.params.has(value.text)) {
1274
+ return `${enclosing.funcKey}#${value.text}`;
1275
+ }
1276
+ return nameId(emitter.filePath, value.text);
1277
+ }
1278
+ function pairKeyText(key) {
1279
+ if (key.type === "hash_key_symbol") {
1280
+ return key.text;
1281
+ }
1282
+ if (key.type === "simple_symbol") {
1283
+ return key.text.slice(1);
1284
+ }
1285
+ if (key.type === "string") {
1286
+ return key.text.slice(1, -1);
1287
+ }
1288
+ return null;
1289
+ }
1290
+ function isPropertyRead2(node) {
1291
+ return node.type === "call" && field(node, "receiver") !== null && field(node, "arguments") === null && children2(node).every(
1292
+ (child) => child.type !== "do_block" && child.type !== "block"
1293
+ );
1294
+ }
1295
+ function emitPropertyRead(emitter, node) {
1296
+ const receiver = field(node, "receiver");
1297
+ const method = field(node, "method");
1298
+ if (receiver === null || method === null) {
1299
+ return;
1300
+ }
1301
+ add(
1302
+ emitter,
1303
+ "readsProperty",
1304
+ nodeId(emitter.filePath, node),
1305
+ valueKey(emitter, receiver),
1306
+ method.text
1307
+ );
1308
+ }
1309
+ function emitCall(emitter, call) {
1310
+ add(emitter, "writtenValue", nodeId(emitter.filePath, call));
1311
+ const method = field(call, "method");
1312
+ if (method === null) {
1313
+ return;
1314
+ }
1315
+ const receiver = field(call, "receiver");
1316
+ const callKey = nodeId(emitter.filePath, call);
1317
+ const calleeKey = receiver === null ? valueKey(emitter, method) : nodeId(emitter.filePath, method);
1318
+ add(emitter, "call", callKey, calleeKey);
1319
+ if (receiver !== null) {
1320
+ add(
1321
+ emitter,
1322
+ "readsProperty",
1323
+ calleeKey,
1324
+ valueKey(emitter, receiver),
1325
+ method.text
1326
+ );
1327
+ }
1328
+ const args = field(call, "arguments");
1329
+ let position = 0;
1330
+ for (const argument of args === null ? [] : children2(args)) {
1331
+ if (argument.type === "pair") {
1332
+ const key = field(argument, "key");
1333
+ const value = field(argument, "value");
1334
+ const keyText = key === null ? null : pairKeyText(key);
1335
+ if (keyText !== null && value !== null) {
1336
+ add(
1337
+ emitter,
1338
+ "callKeywordArg",
1339
+ callKey,
1340
+ keyText,
1341
+ valueKey(emitter, value)
1342
+ );
1343
+ }
1344
+ continue;
1345
+ }
1346
+ add(
1347
+ emitter,
1348
+ "callArg",
1349
+ callKey,
1350
+ String(position),
1351
+ valueKey(emitter, argument)
1352
+ );
1353
+ position += 1;
1354
+ }
1355
+ }
1356
+ function emitArray(emitter, array) {
1357
+ const objectKey = nodeId(emitter.filePath, array);
1358
+ add(emitter, "objectValue", objectKey);
1359
+ let position = 0;
1360
+ for (const element of children2(array)) {
1361
+ add(
1362
+ emitter,
1363
+ "holdsProperty",
1364
+ objectKey,
1365
+ String(position),
1366
+ valueKey(emitter, element)
1367
+ );
1368
+ position += 1;
1369
+ }
1370
+ }
1371
+ function emitHash(emitter, hash) {
1372
+ const objectKey = nodeId(emitter.filePath, hash);
1373
+ add(emitter, "objectValue", objectKey);
1374
+ for (const pair of children2(hash)) {
1375
+ if (pair.type !== "pair") {
1376
+ continue;
1377
+ }
1378
+ const key = field(pair, "key");
1379
+ const value = field(pair, "value");
1380
+ const keyText = key === null ? null : pairKeyText(key);
1381
+ if (keyText === null || value === null) {
1382
+ continue;
1383
+ }
1384
+ add(emitter, "holdsProperty", objectKey, keyText, valueKey(emitter, value));
1385
+ }
1386
+ }
1387
+ function walkExpressions(node, visit) {
1388
+ for (const child of children2(node)) {
1389
+ if (DECLARATION_TYPES2.has(child.type)) {
1390
+ continue;
1391
+ }
1392
+ visit(child);
1393
+ walkExpressions(child, visit);
1394
+ }
1395
+ }
1396
+ function emitExpressionFacts(emitter, node) {
1397
+ walkExpressions(node, (child) => {
1398
+ if (isPropertyRead2(child)) {
1399
+ emitPropertyRead(emitter, child);
1400
+ } else if (child.type === "call") {
1401
+ emitCall(emitter, child);
1402
+ }
1403
+ if (child.type === "array") {
1404
+ emitArray(emitter, child);
1405
+ }
1406
+ if (child.type === "hash") {
1407
+ emitHash(emitter, child);
1408
+ }
1409
+ if (WRITTEN_VALUE_TYPES.has(child.type)) {
1410
+ add(emitter, "writtenValue", nodeId(emitter.filePath, child));
1411
+ }
1412
+ });
1413
+ }
1414
+ function implicitReturn(body) {
1415
+ const statements = children2(body).filter(
1416
+ (child) => child.type !== "rescue" && child.type !== "ensure"
1417
+ );
1418
+ const last = statements[statements.length - 1];
1419
+ return last === void 0 || last.type === "return" ? null : last;
1420
+ }
1421
+ function emitMethodFacts(emitter, method) {
1422
+ const funcKey = nodeId(emitter.filePath, method);
1423
+ add(emitter, "func", funcKey);
1424
+ const params = field(method, "parameters");
1425
+ const declared = /* @__PURE__ */ new Set();
1426
+ let position = 0;
1427
+ for (const param of params === null ? [] : children2(params)) {
1428
+ const paramName = param.type === "identifier" ? param : field(param, "name") ?? null;
1429
+ if (paramName !== null) {
1430
+ declared.add(paramName.text);
1431
+ const paramKey = `${funcKey}#${paramName.text}`;
1432
+ add(emitter, "paramOf", funcKey, String(position), paramKey);
1433
+ add(emitter, "paramNamed", funcKey, paramName.text, paramKey);
1434
+ }
1435
+ position += 1;
1436
+ }
1437
+ const inside = {
1438
+ ...emitter,
1439
+ enclosing: { funcKey, params: declared }
1440
+ };
1441
+ const body = field(method, "body");
1442
+ if (body === null) {
1443
+ return funcKey;
1444
+ }
1445
+ const recordNested = (node) => {
1446
+ for (const child of children2(node)) {
1447
+ if (DECLARATION_TYPES2.has(child.type)) {
1448
+ add(emitter, "containsFn", funcKey, nodeId(emitter.filePath, child));
1449
+ continue;
1450
+ }
1451
+ recordNested(child);
1452
+ }
1453
+ };
1454
+ recordNested(body);
1455
+ walkExpressions(body, (child) => {
1456
+ if (child.type === "return") {
1457
+ const first = children2(child)[0];
1458
+ const returned = first?.type === "argument_list" ? children2(first)[0] : first;
1459
+ if (returned !== void 0) {
1460
+ add(inside, "returnsValue", funcKey, valueKey(inside, returned));
1461
+ }
1462
+ }
1463
+ if (child.type === "call") {
1464
+ add(inside, "bodyCalls", funcKey, nodeId(inside.filePath, child));
1465
+ }
1466
+ });
1467
+ const implicit = implicitReturn(body);
1468
+ if (implicit !== null) {
1469
+ add(inside, "returnsValue", funcKey, valueKey(inside, implicit));
1470
+ }
1471
+ emitExpressionFacts(inside, body);
1472
+ return funcKey;
1473
+ }
1474
+ function emitAssignment(emitter, assignment) {
1475
+ const left = field(assignment, "left");
1476
+ const right = field(assignment, "right");
1477
+ if (right === null) {
1478
+ return;
1479
+ }
1480
+ if (left?.type !== "identifier" && left?.type !== "constant") {
1481
+ return;
1482
+ }
1483
+ add(
1484
+ emitter,
1485
+ "binds",
1486
+ nameId(emitter.filePath, left.text),
1487
+ valueKey(emitter, right)
1488
+ );
1489
+ add(
1490
+ emitter,
1491
+ "exportsAs",
1492
+ emitter.filePath,
1493
+ left.text,
1494
+ valueKey(emitter, right)
1495
+ );
1496
+ }
1497
+ var METHOD_TYPES = /* @__PURE__ */ new Set(["method", "singleton_method"]);
1498
+ function emitClassFacts(emitter, cls) {
1499
+ const classKey = nodeId(emitter.filePath, cls);
1500
+ add(emitter, "objectValue", classKey);
1501
+ const body = field(cls, "body");
1502
+ for (const statement of body === null ? [] : children2(body)) {
1503
+ if (statement.type === "assignment") {
1504
+ const left = field(statement, "left");
1505
+ const right = field(statement, "right");
1506
+ if (left !== null && right !== null && left.type === "constant") {
1507
+ add(
1508
+ emitter,
1509
+ "holdsProperty",
1510
+ classKey,
1511
+ left.text,
1512
+ valueKey(emitter, right)
1513
+ );
1514
+ }
1515
+ continue;
1516
+ }
1517
+ if (!METHOD_TYPES.has(statement.type)) {
1518
+ continue;
1519
+ }
1520
+ const funcKey = emitMethodFacts(emitter, statement);
1521
+ const name = field(statement, "name");
1522
+ if (name !== null) {
1523
+ add(emitter, "holdsProperty", classKey, name.text, funcKey);
1524
+ }
1525
+ }
1526
+ return classKey;
1527
+ }
1528
+ function emitValueFacts(db, filePath, root) {
1529
+ const emitter = { db, filePath, enclosing: null };
1530
+ const declaresName = (child, key) => {
1531
+ const name = field(child, "name");
1532
+ if (name === null) {
1533
+ return;
1534
+ }
1535
+ add(emitter, "binds", nameId(filePath, name.text), key);
1536
+ add(emitter, "exportsAs", filePath, name.text, key);
1537
+ };
1538
+ const walk = (node) => {
1539
+ for (const child of children2(node)) {
1540
+ if (child.type === "class") {
1541
+ declaresName(child, emitClassFacts(emitter, child));
1542
+ continue;
1543
+ }
1544
+ if (METHOD_TYPES.has(child.type)) {
1545
+ declaresName(child, emitMethodFacts(emitter, child));
1546
+ }
1547
+ if (child.type === "assignment") {
1548
+ emitAssignment(emitter, child);
1549
+ }
1550
+ walk(child);
1551
+ }
1552
+ };
1553
+ walk(root);
1554
+ emitExpressionFacts(emitter, root);
1555
+ }
1556
+
1557
+ // src/facts/constants.ts
1558
+ var NESTING_TYPES = /* @__PURE__ */ new Set(["class", "module"]);
1559
+ function children3(node) {
1560
+ return node.namedChildren.filter((child) => child !== null);
1561
+ }
1562
+ function writtenName(node) {
1563
+ if (node.type === "constant") {
1564
+ return node.text;
1565
+ }
1566
+ if (node.type !== "scope_resolution") {
1567
+ return null;
1568
+ }
1569
+ const scope = field(node, "scope");
1570
+ const name = field(node, "name");
1571
+ if (name === null) {
1572
+ return null;
1573
+ }
1574
+ const prefix = scope === null ? "" : writtenName(scope);
1575
+ return prefix === null ? null : `${prefix}::${name.text}`;
1576
+ }
1577
+ function qualify(nesting, name) {
1578
+ return [...nesting, name].join("::");
1579
+ }
1580
+ function isDeclaration(node, parent) {
1581
+ if (parent === null) {
1582
+ return false;
1583
+ }
1584
+ if (NESTING_TYPES.has(parent.type)) {
1585
+ return field(parent, "name") === node;
1586
+ }
1587
+ if (parent.type === "assignment") {
1588
+ return field(parent, "left") === node;
1589
+ }
1590
+ return parent.type === "scope_resolution";
1591
+ }
1592
+ function collectFileConstants(filePath, root) {
1593
+ const definitions = [];
1594
+ const references = [];
1595
+ const walk = (node, parent, nesting) => {
1596
+ if (NESTING_TYPES.has(node.type)) {
1597
+ const name = field(node, "name");
1598
+ const written = name === null ? null : writtenName(name);
1599
+ if (written !== null) {
1600
+ definitions.push({
1601
+ qualifiedName: qualify(nesting, written),
1602
+ key: nodeId(filePath, node)
1603
+ });
1604
+ const inside = [...nesting, ...written.split("::")];
1605
+ for (const child of children3(node)) {
1606
+ walk(child, node, inside);
1607
+ }
1608
+ return;
1609
+ }
1610
+ }
1611
+ if (node.type === "assignment") {
1612
+ const left = field(node, "left");
1613
+ const right = field(node, "right");
1614
+ if (left !== null && left.type === "constant" && right !== null) {
1615
+ definitions.push({
1616
+ qualifiedName: qualify(nesting, left.text),
1617
+ key: nodeId(filePath, right)
1618
+ });
1619
+ }
1620
+ }
1621
+ if ((node.type === "constant" || node.type === "scope_resolution") && !isDeclaration(node, parent)) {
1622
+ const written = writtenName(node);
1623
+ if (written !== null) {
1624
+ references.push({
1625
+ key: node.type === "constant" ? `${filePath}#${node.text}` : nodeId(filePath, node),
1626
+ written,
1627
+ nesting
1628
+ });
1629
+ }
1630
+ }
1631
+ for (const child of children3(node)) {
1632
+ walk(child, node, nesting);
1633
+ }
1634
+ };
1635
+ walk(root, null, []);
1636
+ return { definitions, references };
1637
+ }
1638
+ function emitConstantBindings(db, perFile) {
1639
+ const byName = /* @__PURE__ */ new Map();
1640
+ const files = [...perFile];
1641
+ for (const file of files) {
1642
+ for (const definition of file.definitions) {
1643
+ const found = byName.get(definition.qualifiedName) ?? [];
1644
+ found.push(definition.key);
1645
+ byName.set(definition.qualifiedName, found);
1646
+ }
1647
+ }
1648
+ const settles = (candidate) => {
1649
+ const found = byName.get(candidate);
1650
+ return found !== void 0 && found.length === 1 ? found[0] ?? null : null;
1651
+ };
1652
+ for (const file of files) {
1653
+ for (const reference of file.references) {
1654
+ for (let depth = reference.nesting.length; depth >= 0; depth--) {
1655
+ const candidate = qualify(
1656
+ reference.nesting.slice(0, depth),
1657
+ reference.written
1658
+ );
1659
+ const key = settles(candidate);
1660
+ if (key !== null) {
1661
+ db.add("binds", [reference.key, key]);
1662
+ break;
1663
+ }
1664
+ }
1665
+ }
1666
+ }
1667
+ }
1668
+
1669
+ // src/project.ts
861
1670
  async function extractRubyProject(options) {
862
1671
  const db = new import_datalog.Database();
863
1672
  const summaries = [];
1673
+ const constants = [];
864
1674
  const cache = createFileCache(
865
1675
  (source) => parseRuby(source).then((tree) => tree.rootNode),
866
1676
  (absPath) => import_node_fs3.default.existsSync(absPath) ? import_node_fs3.default.readFileSync(absPath, "utf8") : null
@@ -877,12 +1687,15 @@ async function extractRubyProject(options) {
877
1687
  cache
878
1688
  });
879
1689
  for (const raw of rawUnits) {
880
- const summary = (0, import_extractor2.assembleSummary)(raw, { gapHandling: "permissive" });
1690
+ const summary = (0, import_extractor4.assembleSummary)(raw, { gapHandling: "permissive" });
881
1691
  summary.confidence = { source: "inferred_static", level: "low" };
882
1692
  summaries.push(summary);
883
1693
  emitEntryFact(db, file, raw.identity.range, raw.identity.name);
884
1694
  }
1695
+ emitValueFacts(db, file, root);
1696
+ constants.push(collectFileConstants(file, root));
885
1697
  }
1698
+ emitConstantBindings(db, constants);
886
1699
  return { summaries, facts: db };
887
1700
  }
888
1701
  var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set(["vendor", "node_modules", "tmp", ".git"]);