@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.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"]);
|