@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.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // src/ast.ts
2
+ import { IdMap, IdSet } from "@suss/extractor";
2
3
  function rangeOf(node) {
3
4
  return {
4
5
  start: node.startPosition.row + 1,
@@ -83,6 +84,8 @@ function readCallArgs(argumentList) {
83
84
  }
84
85
  return { positional, keyword };
85
86
  }
87
+ var NodeMap = class extends IdMap {
88
+ };
86
89
 
87
90
  // src/constantPath.ts
88
91
  import fs from "fs";
@@ -367,6 +370,351 @@ function inheritedStatements(ancestry) {
367
370
  import { dispatchByType, graphqlResolverBinding } from "@suss/behavioral-ir";
368
371
  import { unreadableReading } from "@suss/extractor";
369
372
 
373
+ // src/paths/effects.ts
374
+ import { enumerateOrDegrade, sharedGatingConditions } from "@suss/extractor";
375
+
376
+ // src/paths/lowering.ts
377
+ var RAISE_NAMES = /* @__PURE__ */ new Set(["raise", "fail"]);
378
+ var CAPTURES_RETURN = /* @__PURE__ */ new Set([
379
+ "method",
380
+ "singleton_method",
381
+ "lambda",
382
+ "class",
383
+ "module",
384
+ "singleton_class"
385
+ ]);
386
+ var KEYWORD_EXITS = {
387
+ return: "return",
388
+ break: "break",
389
+ next: "continue",
390
+ redo: "continue"
391
+ };
392
+ function isRaise(node) {
393
+ if (node.type !== "call") {
394
+ return false;
395
+ }
396
+ const receiver = field(node, "receiver");
397
+ const method = field(node, "method") ?? node.namedChildren[0] ?? null;
398
+ return receiver === null && method !== null && RAISE_NAMES.has(method.text);
399
+ }
400
+ function exitOf(node) {
401
+ if (isRaise(node)) {
402
+ return "throw";
403
+ }
404
+ return KEYWORD_EXITS[node.type] ?? null;
405
+ }
406
+ function exitKindOf(node) {
407
+ let sawReturn = false;
408
+ const raises = (current) => {
409
+ for (const child of current.namedChildren) {
410
+ if (child === null || CAPTURES_RETURN.has(child.type)) {
411
+ continue;
412
+ }
413
+ if (isRaise(child)) {
414
+ return true;
415
+ }
416
+ if (child.type === "return") {
417
+ sawReturn = true;
418
+ }
419
+ if (raises(child)) {
420
+ return true;
421
+ }
422
+ }
423
+ return false;
424
+ };
425
+ if (isRaise(node) || raises(node)) {
426
+ return "throw";
427
+ }
428
+ return node.type === "return" || sawReturn ? "return" : null;
429
+ }
430
+ function handleOf(node) {
431
+ return node === null ? { sourceText: "", expression: null } : { sourceText: node.text, expression: node };
432
+ }
433
+ function blockStatements(block) {
434
+ if (block === null) {
435
+ return [];
436
+ }
437
+ return block.namedChildren.filter((child) => child !== null);
438
+ }
439
+ var Lowerer = class {
440
+ constructor(terminals) {
441
+ this.terminals = terminals;
442
+ }
443
+ terminals;
444
+ terminalsByStmt = /* @__PURE__ */ new Map();
445
+ terminalHome = new NodeMap();
446
+ attach(statement, candidates) {
447
+ const found = candidates.flatMap((node) => {
448
+ const match = node === null ? void 0 : this.terminals.get(node.id);
449
+ return match === void 0 ? [] : [match];
450
+ });
451
+ if (found.length > 0) {
452
+ this.terminalsByStmt.set(statement, found);
453
+ for (const node of found) {
454
+ this.terminalHome.set(node, statement);
455
+ }
456
+ }
457
+ return statement;
458
+ }
459
+ lowerBlock(block) {
460
+ return blockStatements(block).map((statement) => this.lower(statement));
461
+ }
462
+ /** An elsif chain reads as an if nested in the previous else arm. */
463
+ lowerAlternative(node) {
464
+ if (node === null) {
465
+ return null;
466
+ }
467
+ if (node.type !== "elsif") {
468
+ return this.lowerBlock(node);
469
+ }
470
+ const nested = {
471
+ kind: "if",
472
+ condition: handleOf(field(node, "condition")),
473
+ thenBody: this.lowerBlock(field(node, "consequence")),
474
+ elseBody: this.lowerAlternative(field(node, "alternative")),
475
+ exitKind: exitKindOf(node)
476
+ };
477
+ this.attach(nested, [field(node, "condition")]);
478
+ return [nested];
479
+ }
480
+ lowerIf(node) {
481
+ return {
482
+ kind: "if",
483
+ condition: handleOf(field(node, "condition")),
484
+ thenBody: this.lowerBlock(field(node, "consequence")),
485
+ elseBody: this.lowerAlternative(field(node, "alternative")),
486
+ exitKind: exitKindOf(node)
487
+ };
488
+ }
489
+ lowerBegin(node) {
490
+ const rescues = node.namedChildren.filter(
491
+ (child) => child !== null && child.type === "rescue"
492
+ );
493
+ const ensureClause = node.namedChildren.find(
494
+ (child) => child !== null && child.type === "ensure"
495
+ ) ?? null;
496
+ const tryBody = node.namedChildren.filter(
497
+ (child) => child !== null && child.type !== "rescue" && child.type !== "ensure" && child.type !== "else"
498
+ );
499
+ return {
500
+ kind: "try",
501
+ tryBody: tryBody.map((statement) => this.lower(statement)),
502
+ catchBody: rescues.length > 0 ? rescues.flatMap((clause) => this.lowerBlock(field(clause, "body"))) : null,
503
+ finallyBody: ensureClause === null ? null : this.lowerBlock(ensureClause),
504
+ exitKind: exitKindOf(node)
505
+ };
506
+ }
507
+ lowerCase(node) {
508
+ const groups = [];
509
+ for (const child of node.namedChildren) {
510
+ if (child === null) {
511
+ continue;
512
+ }
513
+ if (child.type === "when") {
514
+ groups.push({
515
+ condition: handleOf(field(child, "pattern")),
516
+ hasTrailingBreak: false,
517
+ body: this.lowerBlock(field(child, "body"))
518
+ });
519
+ }
520
+ if (child.type === "else") {
521
+ groups.push({
522
+ condition: null,
523
+ hasTrailingBreak: false,
524
+ body: this.lowerBlock(child)
525
+ });
526
+ }
527
+ }
528
+ return { kind: "switch", groups, exitKind: exitKindOf(node) };
529
+ }
530
+ lower(node) {
531
+ const exit = exitOf(node);
532
+ if (exit !== null) {
533
+ return this.attach({ kind: "exit", exit, exitKind: exitKindOf(node) }, [
534
+ node
535
+ ]);
536
+ }
537
+ if (node.type === "if" || node.type === "unless") {
538
+ return this.attach(this.lowerIf(node), [field(node, "condition")]);
539
+ }
540
+ if (node.type === "while" || node.type === "until" || node.type === "for") {
541
+ const header = field(node, "condition") ?? field(node, "value");
542
+ return this.attach(
543
+ {
544
+ kind: "loop",
545
+ condition: handleOf(header),
546
+ body: this.lowerBlock(field(node, "body")),
547
+ exitKind: exitKindOf(node)
548
+ },
549
+ [header]
550
+ );
551
+ }
552
+ const attachedBlock = node.namedChildren.find(
553
+ (child) => child !== null && (child.type === "do_block" || child.type === "block")
554
+ );
555
+ if (node.type === "call" && attachedBlock !== void 0) {
556
+ return this.attach(
557
+ {
558
+ kind: "loop",
559
+ condition: handleOf(node),
560
+ body: this.lowerBlock(field(attachedBlock, "body") ?? attachedBlock),
561
+ exitKind: exitKindOf(node)
562
+ },
563
+ [node]
564
+ );
565
+ }
566
+ if (node.type === "begin" || node.type === "rescue_modifier") {
567
+ return this.attach(this.lowerBegin(node), []);
568
+ }
569
+ if (node.type === "case" || node.type === "case_match") {
570
+ return this.attach(this.lowerCase(node), [field(node, "value")]);
571
+ }
572
+ return this.attach({ kind: "opaque", exitKind: exitKindOf(node) }, [node]);
573
+ }
574
+ };
575
+ function lowerRubyBody(body, terminals) {
576
+ const lowerer = new Lowerer(
577
+ new Map(terminals.map((terminal) => [terminal.id, terminal]))
578
+ );
579
+ return {
580
+ statements: lowerer.lowerBlock(body),
581
+ terminalsByStmt: lowerer.terminalsByStmt,
582
+ terminalHome: lowerer.terminalHome
583
+ };
584
+ }
585
+
586
+ // src/paths/effects.ts
587
+ var DECLARATION_TYPES = /* @__PURE__ */ new Set([
588
+ "method",
589
+ "singleton_method",
590
+ "lambda",
591
+ "class",
592
+ "module",
593
+ "singleton_class"
594
+ ]);
595
+ var RAISE_NAMES2 = /* @__PURE__ */ new Set(["raise", "fail"]);
596
+ var BLOCK_TYPES = /* @__PURE__ */ new Set([
597
+ "body_statement",
598
+ "then",
599
+ "else",
600
+ "do_block",
601
+ "block"
602
+ ]);
603
+ var LITERAL_ARGS = {
604
+ string: (node) => ({
605
+ kind: "string",
606
+ value: node.text.replace(/^["']|["']$/g, "")
607
+ }),
608
+ integer: (node) => {
609
+ const value = Number.parseInt(node.text, 10);
610
+ return Number.isNaN(value) ? null : { kind: "number", value };
611
+ },
612
+ float: (node) => {
613
+ const value = Number.parseFloat(node.text);
614
+ return Number.isNaN(value) ? null : { kind: "number", value };
615
+ },
616
+ true: () => ({ kind: "boolean", value: true }),
617
+ false: () => ({ kind: "boolean", value: false }),
618
+ simple_symbol: (node) => ({ kind: "string", value: node.text.slice(1) })
619
+ };
620
+ function children(node) {
621
+ return node.namedChildren.filter((child) => child !== null);
622
+ }
623
+ function isPropertyRead(node) {
624
+ return field(node, "receiver") !== null && field(node, "arguments") === null;
625
+ }
626
+ function isRaise2(node) {
627
+ const method = field(node, "method") ?? children(node)[0] ?? null;
628
+ return field(node, "receiver") === null && method !== null && RAISE_NAMES2.has(method.text);
629
+ }
630
+ function bodyCalls(node, found = []) {
631
+ for (const child of children(node)) {
632
+ if (DECLARATION_TYPES.has(child.type)) {
633
+ continue;
634
+ }
635
+ if (child.type === "call" && !isPropertyRead(child) && !isRaise2(child)) {
636
+ found.push(child);
637
+ }
638
+ bodyCalls(child, found);
639
+ }
640
+ return found;
641
+ }
642
+ function calleeText(call) {
643
+ const receiver = field(call, "receiver");
644
+ const method = field(call, "method") ?? children(call)[0];
645
+ if (method === void 0) {
646
+ return call.text;
647
+ }
648
+ return receiver === null ? method.text : `${receiver.text}.${method.text}`;
649
+ }
650
+ function argOf(node) {
651
+ const literal = LITERAL_ARGS[node.type]?.(node);
652
+ if (literal !== null && literal !== void 0) {
653
+ return literal;
654
+ }
655
+ if (node.type === "call" && !isPropertyRead(node)) {
656
+ return { kind: "call", callee: calleeText(node), args: argsOf(node) };
657
+ }
658
+ return { kind: "identifier", name: node.text };
659
+ }
660
+ function argsOf(call) {
661
+ const args = field(call, "arguments");
662
+ if (args === null) {
663
+ return [];
664
+ }
665
+ return children(args).map(
666
+ (child) => child.type === "pair" ? argOf(field(child, "value") ?? child) : argOf(child)
667
+ );
668
+ }
669
+ function enclosingStatement(call, body) {
670
+ let current = call;
671
+ while (current.parent !== null) {
672
+ if (BLOCK_TYPES.has(current.parent.type) || current.parent.id === body.id) {
673
+ return current;
674
+ }
675
+ current = current.parent;
676
+ }
677
+ return current;
678
+ }
679
+ function invocationEffects(definitionNode) {
680
+ const body = field(definitionNode, "body");
681
+ if (body === null) {
682
+ return [];
683
+ }
684
+ const calls = bodyCalls(body);
685
+ if (calls.length === 0) {
686
+ return [];
687
+ }
688
+ const statementOf = /* @__PURE__ */ new Map();
689
+ const statements = [];
690
+ for (const call of calls) {
691
+ const statement = enclosingStatement(call, body);
692
+ statementOf.set(call.id, statement);
693
+ statements.push(statement);
694
+ }
695
+ const lowered = lowerRubyBody(body, statements);
696
+ const enumerated = enumerateOrDegrade(
697
+ {
698
+ statements: lowered.statements,
699
+ terminalsByStmt: lowered.terminalsByStmt
700
+ },
701
+ statements
702
+ );
703
+ return calls.map((call) => {
704
+ const statement = statementOf.get(call.id);
705
+ const conditions = sharedGatingConditions(
706
+ statement === void 0 ? void 0 : enumerated.byTerminal.get(statement)
707
+ );
708
+ return {
709
+ type: "invocation",
710
+ callee: calleeText(call),
711
+ args: argsOf(call),
712
+ async: false,
713
+ ...conditions.length > 0 ? { preconditions: conditions } : {}
714
+ };
715
+ });
716
+ }
717
+
370
718
  // src/typeShape.ts
371
719
  function scalarLookupName(qualifiedName, prefixes) {
372
720
  for (const prefix of prefixes) {
@@ -500,10 +848,37 @@ async function graphqlObjectFieldUnits(pattern, pack, info, ownBlocks, options)
500
848
  (decl) => buildFieldUnit(pack, typeName, decl, options.filePath)
501
849
  );
502
850
  }
851
+ function branchesFor(body, range) {
852
+ if (body.effects === void 0) {
853
+ return [];
854
+ }
855
+ return [
856
+ {
857
+ conditions: [],
858
+ terminal: {
859
+ kind: "void",
860
+ statusCode: null,
861
+ body: null,
862
+ exceptionType: null,
863
+ message: null,
864
+ component: null,
865
+ renderTree: null,
866
+ delegateTarget: null,
867
+ emitEvent: null,
868
+ location: range
869
+ },
870
+ effects: body.effects,
871
+ location: range,
872
+ isDefault: true
873
+ }
874
+ ];
875
+ }
503
876
  function bodyOfMethod(method) {
877
+ const effects = invocationEffects(method);
504
878
  return {
505
879
  bodyContent: methodHasStatements(method) ? "statements" : "empty",
506
- readings: []
880
+ readings: [],
881
+ ...effects.length > 0 ? { effects } : {}
507
882
  };
508
883
  }
509
884
  var NO_METHOD_BEHIND_IT = {
@@ -740,7 +1115,7 @@ function buildFieldUnit(pack, typeName, decl, filePath) {
740
1115
  fieldName: decl.fieldName
741
1116
  }),
742
1117
  parameters,
743
- branches: [],
1118
+ branches: branchesFor(decl.body, rangeOf(decl.node)),
744
1119
  ...decl.body.bodyContent !== void 0 ? { bodyContent: decl.body.bodyContent } : {},
745
1120
  dependencyCalls: [],
746
1121
  declaredContract: null,
@@ -801,9 +1176,444 @@ import fs3 from "fs";
801
1176
  import path3 from "path";
802
1177
  import { Database } from "@suss/datalog";
803
1178
  import { assembleSummary } from "@suss/extractor";
1179
+
1180
+ // src/facts/values.ts
1181
+ function nodeId(filePath, node) {
1182
+ return `${filePath}:${node.startIndex}-${node.endIndex}`;
1183
+ }
1184
+ function nameId(filePath, name) {
1185
+ return `${filePath}#${name}`;
1186
+ }
1187
+ var DECLARATION_TYPES2 = /* @__PURE__ */ new Set([
1188
+ "method",
1189
+ "singleton_method",
1190
+ "lambda",
1191
+ "class",
1192
+ "module",
1193
+ "singleton_class"
1194
+ ]);
1195
+ var WRITTEN_VALUE_TYPES = /* @__PURE__ */ new Set([
1196
+ "string",
1197
+ "integer",
1198
+ "float",
1199
+ "true",
1200
+ "false",
1201
+ "nil",
1202
+ "simple_symbol",
1203
+ "hash_key_symbol"
1204
+ ]);
1205
+ function children2(node) {
1206
+ return node.namedChildren.filter((child) => child !== null);
1207
+ }
1208
+ function add(emitter, relation, ...tuple) {
1209
+ emitter.db.add(relation, tuple);
1210
+ }
1211
+ function valueKey(emitter, value) {
1212
+ if (value.type !== "identifier" && value.type !== "constant") {
1213
+ return nodeId(emitter.filePath, value);
1214
+ }
1215
+ const enclosing = emitter.enclosing;
1216
+ if (enclosing !== null && enclosing.params.has(value.text)) {
1217
+ return `${enclosing.funcKey}#${value.text}`;
1218
+ }
1219
+ return nameId(emitter.filePath, value.text);
1220
+ }
1221
+ function pairKeyText(key) {
1222
+ if (key.type === "hash_key_symbol") {
1223
+ return key.text;
1224
+ }
1225
+ if (key.type === "simple_symbol") {
1226
+ return key.text.slice(1);
1227
+ }
1228
+ if (key.type === "string") {
1229
+ return key.text.slice(1, -1);
1230
+ }
1231
+ return null;
1232
+ }
1233
+ function isPropertyRead2(node) {
1234
+ return node.type === "call" && field(node, "receiver") !== null && field(node, "arguments") === null && children2(node).every(
1235
+ (child) => child.type !== "do_block" && child.type !== "block"
1236
+ );
1237
+ }
1238
+ function emitPropertyRead(emitter, node) {
1239
+ const receiver = field(node, "receiver");
1240
+ const method = field(node, "method");
1241
+ if (receiver === null || method === null) {
1242
+ return;
1243
+ }
1244
+ add(
1245
+ emitter,
1246
+ "readsProperty",
1247
+ nodeId(emitter.filePath, node),
1248
+ valueKey(emitter, receiver),
1249
+ method.text
1250
+ );
1251
+ }
1252
+ function emitCall(emitter, call) {
1253
+ add(emitter, "writtenValue", nodeId(emitter.filePath, call));
1254
+ const method = field(call, "method");
1255
+ if (method === null) {
1256
+ return;
1257
+ }
1258
+ const receiver = field(call, "receiver");
1259
+ const callKey = nodeId(emitter.filePath, call);
1260
+ const calleeKey = receiver === null ? valueKey(emitter, method) : nodeId(emitter.filePath, method);
1261
+ add(emitter, "call", callKey, calleeKey);
1262
+ if (receiver !== null) {
1263
+ add(
1264
+ emitter,
1265
+ "readsProperty",
1266
+ calleeKey,
1267
+ valueKey(emitter, receiver),
1268
+ method.text
1269
+ );
1270
+ }
1271
+ const args = field(call, "arguments");
1272
+ let position = 0;
1273
+ for (const argument of args === null ? [] : children2(args)) {
1274
+ if (argument.type === "pair") {
1275
+ const key = field(argument, "key");
1276
+ const value = field(argument, "value");
1277
+ const keyText = key === null ? null : pairKeyText(key);
1278
+ if (keyText !== null && value !== null) {
1279
+ add(
1280
+ emitter,
1281
+ "callKeywordArg",
1282
+ callKey,
1283
+ keyText,
1284
+ valueKey(emitter, value)
1285
+ );
1286
+ }
1287
+ continue;
1288
+ }
1289
+ add(
1290
+ emitter,
1291
+ "callArg",
1292
+ callKey,
1293
+ String(position),
1294
+ valueKey(emitter, argument)
1295
+ );
1296
+ position += 1;
1297
+ }
1298
+ }
1299
+ function emitArray(emitter, array) {
1300
+ const objectKey = nodeId(emitter.filePath, array);
1301
+ add(emitter, "objectValue", objectKey);
1302
+ let position = 0;
1303
+ for (const element of children2(array)) {
1304
+ add(
1305
+ emitter,
1306
+ "holdsProperty",
1307
+ objectKey,
1308
+ String(position),
1309
+ valueKey(emitter, element)
1310
+ );
1311
+ position += 1;
1312
+ }
1313
+ }
1314
+ function emitHash(emitter, hash) {
1315
+ const objectKey = nodeId(emitter.filePath, hash);
1316
+ add(emitter, "objectValue", objectKey);
1317
+ for (const pair of children2(hash)) {
1318
+ if (pair.type !== "pair") {
1319
+ continue;
1320
+ }
1321
+ const key = field(pair, "key");
1322
+ const value = field(pair, "value");
1323
+ const keyText = key === null ? null : pairKeyText(key);
1324
+ if (keyText === null || value === null) {
1325
+ continue;
1326
+ }
1327
+ add(emitter, "holdsProperty", objectKey, keyText, valueKey(emitter, value));
1328
+ }
1329
+ }
1330
+ function walkExpressions(node, visit) {
1331
+ for (const child of children2(node)) {
1332
+ if (DECLARATION_TYPES2.has(child.type)) {
1333
+ continue;
1334
+ }
1335
+ visit(child);
1336
+ walkExpressions(child, visit);
1337
+ }
1338
+ }
1339
+ function emitExpressionFacts(emitter, node) {
1340
+ walkExpressions(node, (child) => {
1341
+ if (isPropertyRead2(child)) {
1342
+ emitPropertyRead(emitter, child);
1343
+ } else if (child.type === "call") {
1344
+ emitCall(emitter, child);
1345
+ }
1346
+ if (child.type === "array") {
1347
+ emitArray(emitter, child);
1348
+ }
1349
+ if (child.type === "hash") {
1350
+ emitHash(emitter, child);
1351
+ }
1352
+ if (WRITTEN_VALUE_TYPES.has(child.type)) {
1353
+ add(emitter, "writtenValue", nodeId(emitter.filePath, child));
1354
+ }
1355
+ });
1356
+ }
1357
+ function implicitReturn(body) {
1358
+ const statements = children2(body).filter(
1359
+ (child) => child.type !== "rescue" && child.type !== "ensure"
1360
+ );
1361
+ const last = statements[statements.length - 1];
1362
+ return last === void 0 || last.type === "return" ? null : last;
1363
+ }
1364
+ function emitMethodFacts(emitter, method) {
1365
+ const funcKey = nodeId(emitter.filePath, method);
1366
+ add(emitter, "func", funcKey);
1367
+ const params = field(method, "parameters");
1368
+ const declared = /* @__PURE__ */ new Set();
1369
+ let position = 0;
1370
+ for (const param of params === null ? [] : children2(params)) {
1371
+ const paramName = param.type === "identifier" ? param : field(param, "name") ?? null;
1372
+ if (paramName !== null) {
1373
+ declared.add(paramName.text);
1374
+ const paramKey = `${funcKey}#${paramName.text}`;
1375
+ add(emitter, "paramOf", funcKey, String(position), paramKey);
1376
+ add(emitter, "paramNamed", funcKey, paramName.text, paramKey);
1377
+ }
1378
+ position += 1;
1379
+ }
1380
+ const inside = {
1381
+ ...emitter,
1382
+ enclosing: { funcKey, params: declared }
1383
+ };
1384
+ const body = field(method, "body");
1385
+ if (body === null) {
1386
+ return funcKey;
1387
+ }
1388
+ const recordNested = (node) => {
1389
+ for (const child of children2(node)) {
1390
+ if (DECLARATION_TYPES2.has(child.type)) {
1391
+ add(emitter, "containsFn", funcKey, nodeId(emitter.filePath, child));
1392
+ continue;
1393
+ }
1394
+ recordNested(child);
1395
+ }
1396
+ };
1397
+ recordNested(body);
1398
+ walkExpressions(body, (child) => {
1399
+ if (child.type === "return") {
1400
+ const first = children2(child)[0];
1401
+ const returned = first?.type === "argument_list" ? children2(first)[0] : first;
1402
+ if (returned !== void 0) {
1403
+ add(inside, "returnsValue", funcKey, valueKey(inside, returned));
1404
+ }
1405
+ }
1406
+ if (child.type === "call") {
1407
+ add(inside, "bodyCalls", funcKey, nodeId(inside.filePath, child));
1408
+ }
1409
+ });
1410
+ const implicit = implicitReturn(body);
1411
+ if (implicit !== null) {
1412
+ add(inside, "returnsValue", funcKey, valueKey(inside, implicit));
1413
+ }
1414
+ emitExpressionFacts(inside, body);
1415
+ return funcKey;
1416
+ }
1417
+ function emitAssignment(emitter, assignment) {
1418
+ const left = field(assignment, "left");
1419
+ const right = field(assignment, "right");
1420
+ if (right === null) {
1421
+ return;
1422
+ }
1423
+ if (left?.type !== "identifier" && left?.type !== "constant") {
1424
+ return;
1425
+ }
1426
+ add(
1427
+ emitter,
1428
+ "binds",
1429
+ nameId(emitter.filePath, left.text),
1430
+ valueKey(emitter, right)
1431
+ );
1432
+ add(
1433
+ emitter,
1434
+ "exportsAs",
1435
+ emitter.filePath,
1436
+ left.text,
1437
+ valueKey(emitter, right)
1438
+ );
1439
+ }
1440
+ var METHOD_TYPES = /* @__PURE__ */ new Set(["method", "singleton_method"]);
1441
+ function emitClassFacts(emitter, cls) {
1442
+ const classKey = nodeId(emitter.filePath, cls);
1443
+ add(emitter, "objectValue", classKey);
1444
+ const body = field(cls, "body");
1445
+ for (const statement of body === null ? [] : children2(body)) {
1446
+ if (statement.type === "assignment") {
1447
+ const left = field(statement, "left");
1448
+ const right = field(statement, "right");
1449
+ if (left !== null && right !== null && left.type === "constant") {
1450
+ add(
1451
+ emitter,
1452
+ "holdsProperty",
1453
+ classKey,
1454
+ left.text,
1455
+ valueKey(emitter, right)
1456
+ );
1457
+ }
1458
+ continue;
1459
+ }
1460
+ if (!METHOD_TYPES.has(statement.type)) {
1461
+ continue;
1462
+ }
1463
+ const funcKey = emitMethodFacts(emitter, statement);
1464
+ const name = field(statement, "name");
1465
+ if (name !== null) {
1466
+ add(emitter, "holdsProperty", classKey, name.text, funcKey);
1467
+ }
1468
+ }
1469
+ return classKey;
1470
+ }
1471
+ function emitValueFacts(db, filePath, root) {
1472
+ const emitter = { db, filePath, enclosing: null };
1473
+ const declaresName = (child, key) => {
1474
+ const name = field(child, "name");
1475
+ if (name === null) {
1476
+ return;
1477
+ }
1478
+ add(emitter, "binds", nameId(filePath, name.text), key);
1479
+ add(emitter, "exportsAs", filePath, name.text, key);
1480
+ };
1481
+ const walk = (node) => {
1482
+ for (const child of children2(node)) {
1483
+ if (child.type === "class") {
1484
+ declaresName(child, emitClassFacts(emitter, child));
1485
+ continue;
1486
+ }
1487
+ if (METHOD_TYPES.has(child.type)) {
1488
+ declaresName(child, emitMethodFacts(emitter, child));
1489
+ }
1490
+ if (child.type === "assignment") {
1491
+ emitAssignment(emitter, child);
1492
+ }
1493
+ walk(child);
1494
+ }
1495
+ };
1496
+ walk(root);
1497
+ emitExpressionFacts(emitter, root);
1498
+ }
1499
+
1500
+ // src/facts/constants.ts
1501
+ var NESTING_TYPES = /* @__PURE__ */ new Set(["class", "module"]);
1502
+ function children3(node) {
1503
+ return node.namedChildren.filter((child) => child !== null);
1504
+ }
1505
+ function writtenName(node) {
1506
+ if (node.type === "constant") {
1507
+ return node.text;
1508
+ }
1509
+ if (node.type !== "scope_resolution") {
1510
+ return null;
1511
+ }
1512
+ const scope = field(node, "scope");
1513
+ const name = field(node, "name");
1514
+ if (name === null) {
1515
+ return null;
1516
+ }
1517
+ const prefix = scope === null ? "" : writtenName(scope);
1518
+ return prefix === null ? null : `${prefix}::${name.text}`;
1519
+ }
1520
+ function qualify(nesting, name) {
1521
+ return [...nesting, name].join("::");
1522
+ }
1523
+ function isDeclaration(node, parent) {
1524
+ if (parent === null) {
1525
+ return false;
1526
+ }
1527
+ if (NESTING_TYPES.has(parent.type)) {
1528
+ return field(parent, "name") === node;
1529
+ }
1530
+ if (parent.type === "assignment") {
1531
+ return field(parent, "left") === node;
1532
+ }
1533
+ return parent.type === "scope_resolution";
1534
+ }
1535
+ function collectFileConstants(filePath, root) {
1536
+ const definitions = [];
1537
+ const references = [];
1538
+ const walk = (node, parent, nesting) => {
1539
+ if (NESTING_TYPES.has(node.type)) {
1540
+ const name = field(node, "name");
1541
+ const written = name === null ? null : writtenName(name);
1542
+ if (written !== null) {
1543
+ definitions.push({
1544
+ qualifiedName: qualify(nesting, written),
1545
+ key: nodeId(filePath, node)
1546
+ });
1547
+ const inside = [...nesting, ...written.split("::")];
1548
+ for (const child of children3(node)) {
1549
+ walk(child, node, inside);
1550
+ }
1551
+ return;
1552
+ }
1553
+ }
1554
+ if (node.type === "assignment") {
1555
+ const left = field(node, "left");
1556
+ const right = field(node, "right");
1557
+ if (left !== null && left.type === "constant" && right !== null) {
1558
+ definitions.push({
1559
+ qualifiedName: qualify(nesting, left.text),
1560
+ key: nodeId(filePath, right)
1561
+ });
1562
+ }
1563
+ }
1564
+ if ((node.type === "constant" || node.type === "scope_resolution") && !isDeclaration(node, parent)) {
1565
+ const written = writtenName(node);
1566
+ if (written !== null) {
1567
+ references.push({
1568
+ key: node.type === "constant" ? `${filePath}#${node.text}` : nodeId(filePath, node),
1569
+ written,
1570
+ nesting
1571
+ });
1572
+ }
1573
+ }
1574
+ for (const child of children3(node)) {
1575
+ walk(child, node, nesting);
1576
+ }
1577
+ };
1578
+ walk(root, null, []);
1579
+ return { definitions, references };
1580
+ }
1581
+ function emitConstantBindings(db, perFile) {
1582
+ const byName = /* @__PURE__ */ new Map();
1583
+ const files = [...perFile];
1584
+ for (const file of files) {
1585
+ for (const definition of file.definitions) {
1586
+ const found = byName.get(definition.qualifiedName) ?? [];
1587
+ found.push(definition.key);
1588
+ byName.set(definition.qualifiedName, found);
1589
+ }
1590
+ }
1591
+ const settles = (candidate) => {
1592
+ const found = byName.get(candidate);
1593
+ return found !== void 0 && found.length === 1 ? found[0] ?? null : null;
1594
+ };
1595
+ for (const file of files) {
1596
+ for (const reference of file.references) {
1597
+ for (let depth = reference.nesting.length; depth >= 0; depth--) {
1598
+ const candidate = qualify(
1599
+ reference.nesting.slice(0, depth),
1600
+ reference.written
1601
+ );
1602
+ const key = settles(candidate);
1603
+ if (key !== null) {
1604
+ db.add("binds", [reference.key, key]);
1605
+ break;
1606
+ }
1607
+ }
1608
+ }
1609
+ }
1610
+ }
1611
+
1612
+ // src/project.ts
804
1613
  async function extractRubyProject(options) {
805
1614
  const db = new Database();
806
1615
  const summaries = [];
1616
+ const constants = [];
807
1617
  const cache = createFileCache(
808
1618
  (source) => parseRuby(source).then((tree) => tree.rootNode),
809
1619
  (absPath) => fs3.existsSync(absPath) ? fs3.readFileSync(absPath, "utf8") : null
@@ -825,7 +1635,10 @@ async function extractRubyProject(options) {
825
1635
  summaries.push(summary);
826
1636
  emitEntryFact(db, file, raw.identity.range, raw.identity.name);
827
1637
  }
1638
+ emitValueFacts(db, file, root);
1639
+ constants.push(collectFileConstants(file, root));
828
1640
  }
1641
+ emitConstantBindings(db, constants);
829
1642
  return { summaries, facts: db };
830
1643
  }
831
1644
  var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set(["vendor", "node_modules", "tmp", ".git"]);