@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/README.md +66 -0
- package/dist/index.cjs +819 -6
- package/dist/index.js +815 -2
- package/package.json +5 -4
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
|
|
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,
|
|
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
|
|
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,
|
|
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"]);
|