@suss/adapter-ruby 0.24.4 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +132 -61
- package/dist/index.d.cts +32 -3
- package/dist/index.d.ts +32 -3
- package/dist/index.js +132 -61
- package/package.json +7 -7
package/dist/index.cjs
CHANGED
|
@@ -308,7 +308,7 @@ function autoloadDirectories(root) {
|
|
|
308
308
|
// src/scope.ts
|
|
309
309
|
function qualifyConstantRef(node, nesting) {
|
|
310
310
|
if (node.type === "scope_resolution") {
|
|
311
|
-
return node
|
|
311
|
+
return compoundName(node);
|
|
312
312
|
}
|
|
313
313
|
if (node.type === "constant") {
|
|
314
314
|
const innermost = nesting[0] ?? null;
|
|
@@ -318,13 +318,16 @@ function qualifyConstantRef(node, nesting) {
|
|
|
318
318
|
}
|
|
319
319
|
function constantRefCandidates(node, nesting) {
|
|
320
320
|
if (node.type === "scope_resolution") {
|
|
321
|
-
return [node
|
|
321
|
+
return [compoundName(node)];
|
|
322
322
|
}
|
|
323
323
|
if (node.type === "constant") {
|
|
324
324
|
return [...nesting.map((level) => `${level}::${node.text}`), node.text];
|
|
325
325
|
}
|
|
326
326
|
return [];
|
|
327
327
|
}
|
|
328
|
+
function compoundName(node) {
|
|
329
|
+
return node.text.replace(/^::/, "");
|
|
330
|
+
}
|
|
328
331
|
function shadowingClassFor(node, nesting, knownClasses) {
|
|
329
332
|
if (node.type !== "constant") {
|
|
330
333
|
return null;
|
|
@@ -850,7 +853,7 @@ var Lowerer = class {
|
|
|
850
853
|
return [nested];
|
|
851
854
|
}
|
|
852
855
|
/**
|
|
853
|
-
* `unless ok` runs its body when `ok` is false, so it
|
|
856
|
+
* `unless ok` runs its body when `ok` is false, so it lowers to an `if` on
|
|
854
857
|
* the same test with the two arms the other way around. Without the swap
|
|
855
858
|
* a summary would say a branch runs on the test that skips it.
|
|
856
859
|
*/
|
|
@@ -866,7 +869,7 @@ var Lowerer = class {
|
|
|
866
869
|
exitKind: exitKindOf(node)
|
|
867
870
|
};
|
|
868
871
|
}
|
|
869
|
-
/** `render :gone if expired?` gates one statement on one test, so it
|
|
872
|
+
/** `render :gone if expired?` gates one statement on one test, so it lowers to an if with one arm, on whichever side the test puts it. */
|
|
870
873
|
lowerIfModifier(node) {
|
|
871
874
|
const body = field(node, "body");
|
|
872
875
|
const gated = body === null ? [] : [this.lower(body)];
|
|
@@ -3044,8 +3047,11 @@ function receiverPrefix(receiver, method, root, pattern, options) {
|
|
|
3044
3047
|
function trimmed(path7) {
|
|
3045
3048
|
return path7 === "/" ? "" : path7.replace(/\/+$/, "");
|
|
3046
3049
|
}
|
|
3047
|
-
function namesConstant(receiver,
|
|
3048
|
-
|
|
3050
|
+
function namesConstant(receiver, constantName2) {
|
|
3051
|
+
if (receiver.type === "constant") {
|
|
3052
|
+
return receiver.text === constantName2;
|
|
3053
|
+
}
|
|
3054
|
+
return receiver.type === "scope_resolution" && compoundName(receiver) === constantName2;
|
|
3049
3055
|
}
|
|
3050
3056
|
function builtBy(name, method, root, pattern) {
|
|
3051
3057
|
const builders = pattern.receiverBuilders ?? [];
|
|
@@ -3477,6 +3483,12 @@ function rootConstant(node) {
|
|
|
3477
3483
|
}
|
|
3478
3484
|
return rootConstant(receiver);
|
|
3479
3485
|
}
|
|
3486
|
+
function isConstant(node) {
|
|
3487
|
+
return node.type === "constant" || node.type === "scope_resolution";
|
|
3488
|
+
}
|
|
3489
|
+
function constantName(constant2) {
|
|
3490
|
+
return constant2.type === "constant" ? constant2.text : compoundName(constant2);
|
|
3491
|
+
}
|
|
3480
3492
|
var ANCESTRY_PROGRAM = (0, import_datalog2.deriveOnDemand)(
|
|
3481
3493
|
[...import_resolution3.RESOLUTION_RULES, ...import_resolution3.RESOLUTION_QUESTIONS],
|
|
3482
3494
|
import_resolution3.ANSWER_RELATIONS
|
|
@@ -3486,13 +3498,22 @@ function reachesBase(facts, classKey, bases) {
|
|
|
3486
3498
|
(0, import_datalog2.evaluate)(facts, ANCESTRY_PROGRAM.rules);
|
|
3487
3499
|
return facts.lookup("wantedBaseName", 0, classKey).some((row) => bases.includes(String(row[1])));
|
|
3488
3500
|
}
|
|
3489
|
-
function classBehind(facts, constant2) {
|
|
3490
|
-
const
|
|
3501
|
+
function classBehind(facts, file, constant2) {
|
|
3502
|
+
const key = constant2.type === "constant" ? `${file}#${constant2.text}` : nodeId(file, constant2);
|
|
3491
3503
|
const bound = new Set(
|
|
3492
|
-
facts.
|
|
3504
|
+
facts.lookup("binds", 0, key).map((row) => String(row[1]))
|
|
3493
3505
|
);
|
|
3494
3506
|
return bound.size === 1 ? [...bound][0] : void 0;
|
|
3495
3507
|
}
|
|
3508
|
+
function modelPattern(constant2, file, options) {
|
|
3509
|
+
const classKey = classBehind(options.facts, file, constant2);
|
|
3510
|
+
if (classKey === void 0) {
|
|
3511
|
+
return void 0;
|
|
3512
|
+
}
|
|
3513
|
+
return options.patterns.find(
|
|
3514
|
+
(candidate) => reachesBase(options.facts, classKey, candidate.baseClasses)
|
|
3515
|
+
);
|
|
3516
|
+
}
|
|
3496
3517
|
function selectorOf(node) {
|
|
3497
3518
|
const picked = [];
|
|
3498
3519
|
const walk = (current) => {
|
|
@@ -3514,7 +3535,87 @@ function selectorOf(node) {
|
|
|
3514
3535
|
walk(node);
|
|
3515
3536
|
return [...new Set(picked)];
|
|
3516
3537
|
}
|
|
3517
|
-
function
|
|
3538
|
+
function storageEffect(call, model, pattern, kind, selector) {
|
|
3539
|
+
const operation = methodOf(call);
|
|
3540
|
+
return {
|
|
3541
|
+
type: "interaction",
|
|
3542
|
+
binding: (0, import_ir_core.storageBinding)({
|
|
3543
|
+
recognition: "ruby-storage",
|
|
3544
|
+
storageSystem: pattern.storageSystem,
|
|
3545
|
+
scope: pattern.baseClasses[0] ?? "",
|
|
3546
|
+
container: constantName(model)
|
|
3547
|
+
}),
|
|
3548
|
+
callee: call.text,
|
|
3549
|
+
interaction: {
|
|
3550
|
+
class: "storage-access",
|
|
3551
|
+
kind,
|
|
3552
|
+
fields: [],
|
|
3553
|
+
operation,
|
|
3554
|
+
...selector.length > 0 ? { selector } : {}
|
|
3555
|
+
}
|
|
3556
|
+
};
|
|
3557
|
+
}
|
|
3558
|
+
function modelCallEffects(call, file, options) {
|
|
3559
|
+
const constant2 = rootConstant(call);
|
|
3560
|
+
if (constant2 === null) {
|
|
3561
|
+
return [];
|
|
3562
|
+
}
|
|
3563
|
+
const pattern = modelPattern(constant2, file, options);
|
|
3564
|
+
if (pattern === void 0) {
|
|
3565
|
+
return [];
|
|
3566
|
+
}
|
|
3567
|
+
const kind = pattern.writes.includes(methodOf(call)) ? "write" : "read";
|
|
3568
|
+
return [storageEffect(call, constant2, pattern, kind, selectorOf(call))];
|
|
3569
|
+
}
|
|
3570
|
+
function isLoader(node, loader) {
|
|
3571
|
+
if (node === null) {
|
|
3572
|
+
return false;
|
|
3573
|
+
}
|
|
3574
|
+
if (node.type === "identifier") {
|
|
3575
|
+
return node.text === loader.loader;
|
|
3576
|
+
}
|
|
3577
|
+
return node.type === "call" && receiverOf(node) === null && methodOf(node) === loader.loader;
|
|
3578
|
+
}
|
|
3579
|
+
function loaderPick(call, loader) {
|
|
3580
|
+
const method = methodOf(call);
|
|
3581
|
+
const receiver = receiverOf(call);
|
|
3582
|
+
if (receiver === null) {
|
|
3583
|
+
return loader.shortcuts.includes(method) ? call : null;
|
|
3584
|
+
}
|
|
3585
|
+
if (!loader.reads.includes(method) || receiver.type !== "call") {
|
|
3586
|
+
return null;
|
|
3587
|
+
}
|
|
3588
|
+
const picks = methodOf(receiver) === loader.pick && isLoader(receiverOf(receiver), loader);
|
|
3589
|
+
return picks ? receiver : null;
|
|
3590
|
+
}
|
|
3591
|
+
function loaderCallEffects(call, file, options) {
|
|
3592
|
+
const effects = [];
|
|
3593
|
+
for (const loader of options.loaders ?? []) {
|
|
3594
|
+
const pick = loaderPick(call, loader);
|
|
3595
|
+
if (pick === null) {
|
|
3596
|
+
continue;
|
|
3597
|
+
}
|
|
3598
|
+
const args = field(pick, "arguments");
|
|
3599
|
+
for (const argument of args === null ? [] : children3(args)) {
|
|
3600
|
+
if (!isConstant(argument)) {
|
|
3601
|
+
continue;
|
|
3602
|
+
}
|
|
3603
|
+
const pattern = modelPattern(argument, file, options);
|
|
3604
|
+
if (pattern !== void 0) {
|
|
3605
|
+
effects.push(storageEffect(call, argument, pattern, "read", []));
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
return effects;
|
|
3610
|
+
}
|
|
3611
|
+
function effectsOfCall(call, file, options) {
|
|
3612
|
+
const onModel = modelCallEffects(call, file, options);
|
|
3613
|
+
return onModel.length > 0 ? onModel : loaderCallEffects(call, file, options);
|
|
3614
|
+
}
|
|
3615
|
+
function storageClaims(call, file, options) {
|
|
3616
|
+
return options.patterns.length > 0 && effectsOfCall(call, file, options).length > 0;
|
|
3617
|
+
}
|
|
3618
|
+
function storageEffects(calls, file, options) {
|
|
3518
3619
|
if (options.patterns.length === 0) {
|
|
3519
3620
|
return [];
|
|
3520
3621
|
}
|
|
@@ -3530,39 +3631,7 @@ function storageEffects(calls, options) {
|
|
|
3530
3631
|
if (partOfOne.has(call.id)) {
|
|
3531
3632
|
continue;
|
|
3532
3633
|
}
|
|
3533
|
-
|
|
3534
|
-
if (constant2 === null) {
|
|
3535
|
-
continue;
|
|
3536
|
-
}
|
|
3537
|
-
const classKey = classBehind(options.facts, constant2);
|
|
3538
|
-
if (classKey === void 0) {
|
|
3539
|
-
continue;
|
|
3540
|
-
}
|
|
3541
|
-
const pattern = options.patterns.find(
|
|
3542
|
-
(candidate) => reachesBase(options.facts, classKey, candidate.baseClasses)
|
|
3543
|
-
);
|
|
3544
|
-
if (pattern === void 0) {
|
|
3545
|
-
continue;
|
|
3546
|
-
}
|
|
3547
|
-
const operation = methodOf(call);
|
|
3548
|
-
const picked = selectorOf(call);
|
|
3549
|
-
effects.push({
|
|
3550
|
-
type: "interaction",
|
|
3551
|
-
binding: (0, import_ir_core.storageBinding)({
|
|
3552
|
-
recognition: "ruby-storage",
|
|
3553
|
-
storageSystem: pattern.storageSystem,
|
|
3554
|
-
scope: pattern.baseClasses[0] ?? "",
|
|
3555
|
-
container: constant2.text
|
|
3556
|
-
}),
|
|
3557
|
-
callee: call.text,
|
|
3558
|
-
interaction: {
|
|
3559
|
-
class: "storage-access",
|
|
3560
|
-
kind: pattern.writes.includes(operation) ? "write" : "read",
|
|
3561
|
-
fields: [],
|
|
3562
|
-
operation,
|
|
3563
|
-
...picked.length > 0 ? { selector: picked } : {}
|
|
3564
|
-
}
|
|
3565
|
-
});
|
|
3634
|
+
effects.push(...effectsOfCall(call, file, options));
|
|
3566
3635
|
}
|
|
3567
3636
|
return effects;
|
|
3568
3637
|
}
|
|
@@ -3604,7 +3673,7 @@ function typeShapeFromNode(node, ctx) {
|
|
|
3604
3673
|
};
|
|
3605
3674
|
}
|
|
3606
3675
|
if (node.type === "scope_resolution") {
|
|
3607
|
-
const qualified = node
|
|
3676
|
+
const qualified = compoundName(node);
|
|
3608
3677
|
const scalar = ctx.scalars[scalarLookupName(qualified, ctx.scalarNamePrefixes)];
|
|
3609
3678
|
if (scalar !== void 0) {
|
|
3610
3679
|
return scalar;
|
|
@@ -3771,7 +3840,7 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
|
|
|
3771
3840
|
filter,
|
|
3772
3841
|
pattern,
|
|
3773
3842
|
displayPath,
|
|
3774
|
-
bodyOfMethod(filter.method, options)
|
|
3843
|
+
bodyOfMethod(filter.method, filter.file, options)
|
|
3775
3844
|
);
|
|
3776
3845
|
units.push(raw);
|
|
3777
3846
|
options.onReachSeed?.(raw, {
|
|
@@ -3803,7 +3872,7 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
|
|
|
3803
3872
|
info.qualifiedName,
|
|
3804
3873
|
actionName,
|
|
3805
3874
|
method,
|
|
3806
|
-
options.filePath,
|
|
3875
|
+
{ display: options.filePath, absolute: block.file },
|
|
3807
3876
|
options,
|
|
3808
3877
|
around
|
|
3809
3878
|
);
|
|
@@ -3817,10 +3886,10 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
|
|
|
3817
3886
|
}
|
|
3818
3887
|
return units;
|
|
3819
3888
|
}
|
|
3820
|
-
function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actionName, method,
|
|
3889
|
+
function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actionName, method, file, bodyRead, wrappers = []) {
|
|
3821
3890
|
const range = rangeOf(method);
|
|
3822
3891
|
const route = pattern.routeFor(controllerQualifiedName, actionName);
|
|
3823
|
-
const body = bodyOfMethod(method, bodyRead);
|
|
3892
|
+
const body = bodyOfMethod(method, file.absolute, bodyRead);
|
|
3824
3893
|
const perResponse = responseBranches(
|
|
3825
3894
|
method,
|
|
3826
3895
|
pattern,
|
|
@@ -3832,7 +3901,7 @@ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actio
|
|
|
3832
3901
|
name: actionName,
|
|
3833
3902
|
nameKind: "binding",
|
|
3834
3903
|
kind: "handler",
|
|
3835
|
-
file:
|
|
3904
|
+
file: file.display,
|
|
3836
3905
|
range,
|
|
3837
3906
|
span: spanOf(method),
|
|
3838
3907
|
exportName: actionName,
|
|
@@ -3951,12 +4020,12 @@ function branchesFor(body, range) {
|
|
|
3951
4020
|
}
|
|
3952
4021
|
];
|
|
3953
4022
|
}
|
|
3954
|
-
function bodyOfMethod(method, bodyRead = {}) {
|
|
4023
|
+
function bodyOfMethod(method, file, bodyRead = {}) {
|
|
3955
4024
|
const effects = invocationEffects(method, bodyRead.inheritedMethods);
|
|
3956
4025
|
const storage = bodyRead.storage;
|
|
3957
4026
|
const extra = [
|
|
3958
4027
|
...envReadEffects(method),
|
|
3959
|
-
...storage === void 0 ? [] : storageEffects(callsUnder2(method), storage)
|
|
4028
|
+
...storage === void 0 ? [] : storageEffects(callsUnder2(method), file, storage)
|
|
3960
4029
|
];
|
|
3961
4030
|
return {
|
|
3962
4031
|
bodyContent: methodHasStatements(method) ? "statements" : "empty",
|
|
@@ -3987,7 +4056,7 @@ function methodNotSettled(reason, range) {
|
|
|
3987
4056
|
function bodyFromLookup(found, range, subject, nothingThere, bodyRead) {
|
|
3988
4057
|
const table = {
|
|
3989
4058
|
found: (lookup) => ({
|
|
3990
|
-
...bodyOfMethod(lookup.method, bodyRead),
|
|
4059
|
+
...bodyOfMethod(lookup.method, lookup.block.file, bodyRead),
|
|
3991
4060
|
reachSeed: {
|
|
3992
4061
|
file: lookup.block.file,
|
|
3993
4062
|
node: lookup.method,
|
|
@@ -4351,11 +4420,10 @@ function collectFileConstants(filePath, root) {
|
|
|
4351
4420
|
if ((node.type === "constant" || node.type === "scope_resolution") && !isDeclaration(node, parent)) {
|
|
4352
4421
|
const written = writtenName(node);
|
|
4353
4422
|
if (written !== null) {
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
written,
|
|
4357
|
-
|
|
4358
|
-
});
|
|
4423
|
+
const key = node.type === "constant" ? `${filePath}#${node.text}` : nodeId(filePath, node);
|
|
4424
|
+
references.push(
|
|
4425
|
+
written.startsWith("::") ? { key, written: written.slice(2), nesting: [] } : { key, written, nesting }
|
|
4426
|
+
);
|
|
4359
4427
|
}
|
|
4360
4428
|
}
|
|
4361
4429
|
for (const child of children4(node)) {
|
|
@@ -4916,7 +4984,8 @@ async function scanBody(source, ctx, options) {
|
|
|
4916
4984
|
);
|
|
4917
4985
|
if (outcome.kind === "stopped") {
|
|
4918
4986
|
const stopKey = `${outcome.reason}:${callee}`;
|
|
4919
|
-
|
|
4987
|
+
const claimed = options.storage !== void 0 && storageClaims(call, source.file, options.storage);
|
|
4988
|
+
if (!seen.has(stopKey) && !claimed && (0, import_behavioral_ir4.worthRecording)(outcome.reason)) {
|
|
4920
4989
|
seen.add(stopKey);
|
|
4921
4990
|
stops.push({ callee, reason: outcome.reason });
|
|
4922
4991
|
}
|
|
@@ -4947,7 +5016,7 @@ async function scanBody(source, ctx, options) {
|
|
|
4947
5016
|
}
|
|
4948
5017
|
function libraryUnit(target, options) {
|
|
4949
5018
|
const { file, node, name, exportPath } = target;
|
|
4950
|
-
const body = bodyOfMethod(node, options);
|
|
5019
|
+
const body = bodyOfMethod(node, file, options);
|
|
4951
5020
|
const range = rangeOf(node);
|
|
4952
5021
|
return {
|
|
4953
5022
|
identity: {
|
|
@@ -5104,6 +5173,8 @@ async function extractRubyProject(options) {
|
|
|
5104
5173
|
bindEvaluator(db, { files: parsed, definitions });
|
|
5105
5174
|
});
|
|
5106
5175
|
const storagePatterns = options.packs.flatMap((pack) => pack.storage ?? []);
|
|
5176
|
+
const loaderPatterns = options.packs.flatMap((pack) => pack.loaders ?? []);
|
|
5177
|
+
const storage = storagePatterns.length > 0 ? { facts: db, patterns: storagePatterns, loaders: loaderPatterns } : void 0;
|
|
5107
5178
|
const inheritedMethods = inheritedMethodsIn(options.packs);
|
|
5108
5179
|
const displayPathOf = (file) => options.workspaceRoot !== void 0 ? import_node_path4.default.relative(options.workspaceRoot, file) : file;
|
|
5109
5180
|
const seeds = [];
|
|
@@ -5119,7 +5190,7 @@ async function extractRubyProject(options) {
|
|
|
5119
5190
|
filePath: displayPath,
|
|
5120
5191
|
absoluteFile: file,
|
|
5121
5192
|
cache,
|
|
5122
|
-
...
|
|
5193
|
+
...storage === void 0 ? {} : { storage },
|
|
5123
5194
|
inheritedMethods,
|
|
5124
5195
|
displayPathOf,
|
|
5125
5196
|
facts: db,
|
|
@@ -5198,7 +5269,7 @@ async function extractRubyProject(options) {
|
|
|
5198
5269
|
() => reachedFunctions(seeds, {
|
|
5199
5270
|
files: parsed,
|
|
5200
5271
|
displayPathOf,
|
|
5201
|
-
...
|
|
5272
|
+
...storage === void 0 ? {} : { storage },
|
|
5202
5273
|
inheritedMethods
|
|
5203
5274
|
})
|
|
5204
5275
|
);
|
package/dist/index.d.cts
CHANGED
|
@@ -244,6 +244,8 @@ interface RubyPack {
|
|
|
244
244
|
clients?: RbClientCall[];
|
|
245
245
|
/** What the library's own database calls look like. The README says how one is matched. */
|
|
246
246
|
storage?: RbStoragePattern[];
|
|
247
|
+
/** Calls the library gives a project for reading a model through a batching loader, rather than on the model itself. */
|
|
248
|
+
loaders?: RbLoaderPattern[];
|
|
247
249
|
}
|
|
248
250
|
/**
|
|
249
251
|
* The calls a library gives a project for making a request. A method
|
|
@@ -264,7 +266,7 @@ interface RbClientCall {
|
|
|
264
266
|
receiverBuilders?: string[];
|
|
265
267
|
/** The keyword such a builder takes the base URL under, which comes in front of the path of a call on it. */
|
|
266
268
|
builderUrlKeyword?: string;
|
|
267
|
-
/** What the response object gives a caller, so a test on
|
|
269
|
+
/** What the response object gives a caller, so a test on one of its members counts as a test on a status. */
|
|
268
270
|
response?: RbClientResponse;
|
|
269
271
|
/** A call that sends a request object built somewhere else, which is how Net::HTTP sends anything with a body. */
|
|
270
272
|
requestObject?: {
|
|
@@ -304,6 +306,23 @@ interface RbStoragePattern {
|
|
|
304
306
|
/** Which database is behind the connection, which the project settles. */
|
|
305
307
|
storageSystem: "postgresql" | "mysql" | "sqlite";
|
|
306
308
|
}
|
|
309
|
+
/**
|
|
310
|
+
* A loader that batches reads of a model on the caller's behalf, graphql-ruby's
|
|
311
|
+
* dataloader for one. The call that picks what to load is given the model as
|
|
312
|
+
* a constant argument, so the read is recorded against that model the same
|
|
313
|
+
* way a call on the model itself would be, and a storage pattern from another
|
|
314
|
+
* pack in the run says whether the constant is a model at all.
|
|
315
|
+
*/
|
|
316
|
+
interface RbLoaderPattern {
|
|
317
|
+
/** The receiverless call that gives the loader, `dataloader`. */
|
|
318
|
+
loader: string;
|
|
319
|
+
/** The method on the loader that picks a source and its arguments, `with`. */
|
|
320
|
+
pick: string;
|
|
321
|
+
/** The methods on a picked source that perform the read, `load` and `load_all`. */
|
|
322
|
+
reads: string[];
|
|
323
|
+
/** Receiverless calls that pick and read in one, whose arguments include the model, `dataload` and `dataload_record`. */
|
|
324
|
+
shortcuts: string[];
|
|
325
|
+
}
|
|
307
326
|
type RubyDiscoveryPattern = GraphqlObjectFields | ControllerActions;
|
|
308
327
|
/** A class whose ancestry reaches one of `baseClassNames` is a controller, and every instance method it defines directly is one of its actions, bound by `routeFor` to the method and path a project's own routing gives it, or discovered with no boundary binding when `routeFor` finds none. */
|
|
309
328
|
interface ControllerActions {
|
|
@@ -356,7 +375,7 @@ interface RbControllerFilter {
|
|
|
356
375
|
* `rescue_from ActiveRecord::RecordNotFound, with: :not_found`.
|
|
357
376
|
*/
|
|
358
377
|
methodFrom: "argument" | "withKeyword";
|
|
359
|
-
/** Set when the library runs the method only for an action that raised,
|
|
378
|
+
/** Set when the library runs the method only for an action that raised, as `rescue_from` does. */
|
|
360
379
|
onThrow?: boolean;
|
|
361
380
|
/** The name of the call that takes a filter back off, `skip_before_action` for `before_action`. */
|
|
362
381
|
skippedBy?: string;
|
|
@@ -369,7 +388,7 @@ interface RbControllerFilter {
|
|
|
369
388
|
/**
|
|
370
389
|
* One call an action writes to send a response, and where that call takes
|
|
371
390
|
* the status. A call may take it both ways, and then the keyword wins,
|
|
372
|
-
*
|
|
391
|
+
* as it does in Rails for `head :ok, status: :created`.
|
|
373
392
|
*/
|
|
374
393
|
interface RbStatusCall {
|
|
375
394
|
/** The call's own name as an action writes it, `render` for Rails. */
|
|
@@ -436,9 +455,19 @@ interface GraphqlObjectFields {
|
|
|
436
455
|
};
|
|
437
456
|
}
|
|
438
457
|
|
|
458
|
+
/**
|
|
459
|
+
* storage.ts: which calls in a body talk to the database, for Ruby.
|
|
460
|
+
*
|
|
461
|
+
* A pack says which base class the library gives a model, and a call on a
|
|
462
|
+
* class that reaches that base is a database call. The README says why
|
|
463
|
+
* ancestry. A loader pattern from another pack adds calls that are given
|
|
464
|
+
* the model as an argument instead, and those go through the same test.
|
|
465
|
+
*/
|
|
466
|
+
|
|
439
467
|
interface RbStorageOptions {
|
|
440
468
|
readonly facts: Database;
|
|
441
469
|
readonly patterns: readonly RbStoragePattern[];
|
|
470
|
+
readonly loaders?: readonly RbLoaderPattern[];
|
|
442
471
|
}
|
|
443
472
|
|
|
444
473
|
/** The methods a pack declared the library defines itself, so a receiverless call to one of them is not work the project does. */
|
package/dist/index.d.ts
CHANGED
|
@@ -244,6 +244,8 @@ interface RubyPack {
|
|
|
244
244
|
clients?: RbClientCall[];
|
|
245
245
|
/** What the library's own database calls look like. The README says how one is matched. */
|
|
246
246
|
storage?: RbStoragePattern[];
|
|
247
|
+
/** Calls the library gives a project for reading a model through a batching loader, rather than on the model itself. */
|
|
248
|
+
loaders?: RbLoaderPattern[];
|
|
247
249
|
}
|
|
248
250
|
/**
|
|
249
251
|
* The calls a library gives a project for making a request. A method
|
|
@@ -264,7 +266,7 @@ interface RbClientCall {
|
|
|
264
266
|
receiverBuilders?: string[];
|
|
265
267
|
/** The keyword such a builder takes the base URL under, which comes in front of the path of a call on it. */
|
|
266
268
|
builderUrlKeyword?: string;
|
|
267
|
-
/** What the response object gives a caller, so a test on
|
|
269
|
+
/** What the response object gives a caller, so a test on one of its members counts as a test on a status. */
|
|
268
270
|
response?: RbClientResponse;
|
|
269
271
|
/** A call that sends a request object built somewhere else, which is how Net::HTTP sends anything with a body. */
|
|
270
272
|
requestObject?: {
|
|
@@ -304,6 +306,23 @@ interface RbStoragePattern {
|
|
|
304
306
|
/** Which database is behind the connection, which the project settles. */
|
|
305
307
|
storageSystem: "postgresql" | "mysql" | "sqlite";
|
|
306
308
|
}
|
|
309
|
+
/**
|
|
310
|
+
* A loader that batches reads of a model on the caller's behalf, graphql-ruby's
|
|
311
|
+
* dataloader for one. The call that picks what to load is given the model as
|
|
312
|
+
* a constant argument, so the read is recorded against that model the same
|
|
313
|
+
* way a call on the model itself would be, and a storage pattern from another
|
|
314
|
+
* pack in the run says whether the constant is a model at all.
|
|
315
|
+
*/
|
|
316
|
+
interface RbLoaderPattern {
|
|
317
|
+
/** The receiverless call that gives the loader, `dataloader`. */
|
|
318
|
+
loader: string;
|
|
319
|
+
/** The method on the loader that picks a source and its arguments, `with`. */
|
|
320
|
+
pick: string;
|
|
321
|
+
/** The methods on a picked source that perform the read, `load` and `load_all`. */
|
|
322
|
+
reads: string[];
|
|
323
|
+
/** Receiverless calls that pick and read in one, whose arguments include the model, `dataload` and `dataload_record`. */
|
|
324
|
+
shortcuts: string[];
|
|
325
|
+
}
|
|
307
326
|
type RubyDiscoveryPattern = GraphqlObjectFields | ControllerActions;
|
|
308
327
|
/** A class whose ancestry reaches one of `baseClassNames` is a controller, and every instance method it defines directly is one of its actions, bound by `routeFor` to the method and path a project's own routing gives it, or discovered with no boundary binding when `routeFor` finds none. */
|
|
309
328
|
interface ControllerActions {
|
|
@@ -356,7 +375,7 @@ interface RbControllerFilter {
|
|
|
356
375
|
* `rescue_from ActiveRecord::RecordNotFound, with: :not_found`.
|
|
357
376
|
*/
|
|
358
377
|
methodFrom: "argument" | "withKeyword";
|
|
359
|
-
/** Set when the library runs the method only for an action that raised,
|
|
378
|
+
/** Set when the library runs the method only for an action that raised, as `rescue_from` does. */
|
|
360
379
|
onThrow?: boolean;
|
|
361
380
|
/** The name of the call that takes a filter back off, `skip_before_action` for `before_action`. */
|
|
362
381
|
skippedBy?: string;
|
|
@@ -369,7 +388,7 @@ interface RbControllerFilter {
|
|
|
369
388
|
/**
|
|
370
389
|
* One call an action writes to send a response, and where that call takes
|
|
371
390
|
* the status. A call may take it both ways, and then the keyword wins,
|
|
372
|
-
*
|
|
391
|
+
* as it does in Rails for `head :ok, status: :created`.
|
|
373
392
|
*/
|
|
374
393
|
interface RbStatusCall {
|
|
375
394
|
/** The call's own name as an action writes it, `render` for Rails. */
|
|
@@ -436,9 +455,19 @@ interface GraphqlObjectFields {
|
|
|
436
455
|
};
|
|
437
456
|
}
|
|
438
457
|
|
|
458
|
+
/**
|
|
459
|
+
* storage.ts: which calls in a body talk to the database, for Ruby.
|
|
460
|
+
*
|
|
461
|
+
* A pack says which base class the library gives a model, and a call on a
|
|
462
|
+
* class that reaches that base is a database call. The README says why
|
|
463
|
+
* ancestry. A loader pattern from another pack adds calls that are given
|
|
464
|
+
* the model as an argument instead, and those go through the same test.
|
|
465
|
+
*/
|
|
466
|
+
|
|
439
467
|
interface RbStorageOptions {
|
|
440
468
|
readonly facts: Database;
|
|
441
469
|
readonly patterns: readonly RbStoragePattern[];
|
|
470
|
+
readonly loaders?: readonly RbLoaderPattern[];
|
|
442
471
|
}
|
|
443
472
|
|
|
444
473
|
/** The methods a pack declared the library defines itself, so a receiverless call to one of them is not work the project does. */
|
package/dist/index.js
CHANGED
|
@@ -229,7 +229,7 @@ function autoloadDirectories(root) {
|
|
|
229
229
|
// src/scope.ts
|
|
230
230
|
function qualifyConstantRef(node, nesting) {
|
|
231
231
|
if (node.type === "scope_resolution") {
|
|
232
|
-
return node
|
|
232
|
+
return compoundName(node);
|
|
233
233
|
}
|
|
234
234
|
if (node.type === "constant") {
|
|
235
235
|
const innermost = nesting[0] ?? null;
|
|
@@ -239,13 +239,16 @@ function qualifyConstantRef(node, nesting) {
|
|
|
239
239
|
}
|
|
240
240
|
function constantRefCandidates(node, nesting) {
|
|
241
241
|
if (node.type === "scope_resolution") {
|
|
242
|
-
return [node
|
|
242
|
+
return [compoundName(node)];
|
|
243
243
|
}
|
|
244
244
|
if (node.type === "constant") {
|
|
245
245
|
return [...nesting.map((level) => `${level}::${node.text}`), node.text];
|
|
246
246
|
}
|
|
247
247
|
return [];
|
|
248
248
|
}
|
|
249
|
+
function compoundName(node) {
|
|
250
|
+
return node.text.replace(/^::/, "");
|
|
251
|
+
}
|
|
249
252
|
function shadowingClassFor(node, nesting, knownClasses) {
|
|
250
253
|
if (node.type !== "constant") {
|
|
251
254
|
return null;
|
|
@@ -775,7 +778,7 @@ var Lowerer = class {
|
|
|
775
778
|
return [nested];
|
|
776
779
|
}
|
|
777
780
|
/**
|
|
778
|
-
* `unless ok` runs its body when `ok` is false, so it
|
|
781
|
+
* `unless ok` runs its body when `ok` is false, so it lowers to an `if` on
|
|
779
782
|
* the same test with the two arms the other way around. Without the swap
|
|
780
783
|
* a summary would say a branch runs on the test that skips it.
|
|
781
784
|
*/
|
|
@@ -791,7 +794,7 @@ var Lowerer = class {
|
|
|
791
794
|
exitKind: exitKindOf(node)
|
|
792
795
|
};
|
|
793
796
|
}
|
|
794
|
-
/** `render :gone if expired?` gates one statement on one test, so it
|
|
797
|
+
/** `render :gone if expired?` gates one statement on one test, so it lowers to an if with one arm, on whichever side the test puts it. */
|
|
795
798
|
lowerIfModifier(node) {
|
|
796
799
|
const body = field(node, "body");
|
|
797
800
|
const gated = body === null ? [] : [this.lower(body)];
|
|
@@ -3006,8 +3009,11 @@ function receiverPrefix(receiver, method, root, pattern, options) {
|
|
|
3006
3009
|
function trimmed(path7) {
|
|
3007
3010
|
return path7 === "/" ? "" : path7.replace(/\/+$/, "");
|
|
3008
3011
|
}
|
|
3009
|
-
function namesConstant(receiver,
|
|
3010
|
-
|
|
3012
|
+
function namesConstant(receiver, constantName2) {
|
|
3013
|
+
if (receiver.type === "constant") {
|
|
3014
|
+
return receiver.text === constantName2;
|
|
3015
|
+
}
|
|
3016
|
+
return receiver.type === "scope_resolution" && compoundName(receiver) === constantName2;
|
|
3011
3017
|
}
|
|
3012
3018
|
function builtBy(name, method, root, pattern) {
|
|
3013
3019
|
const builders = pattern.receiverBuilders ?? [];
|
|
@@ -3443,6 +3449,12 @@ function rootConstant(node) {
|
|
|
3443
3449
|
}
|
|
3444
3450
|
return rootConstant(receiver);
|
|
3445
3451
|
}
|
|
3452
|
+
function isConstant(node) {
|
|
3453
|
+
return node.type === "constant" || node.type === "scope_resolution";
|
|
3454
|
+
}
|
|
3455
|
+
function constantName(constant2) {
|
|
3456
|
+
return constant2.type === "constant" ? constant2.text : compoundName(constant2);
|
|
3457
|
+
}
|
|
3446
3458
|
var ANCESTRY_PROGRAM = deriveOnDemand2(
|
|
3447
3459
|
[...RESOLUTION_RULES2, ...RESOLUTION_QUESTIONS2],
|
|
3448
3460
|
ANSWER_RELATIONS2
|
|
@@ -3452,13 +3464,22 @@ function reachesBase(facts, classKey, bases) {
|
|
|
3452
3464
|
evaluate2(facts, ANCESTRY_PROGRAM.rules);
|
|
3453
3465
|
return facts.lookup("wantedBaseName", 0, classKey).some((row) => bases.includes(String(row[1])));
|
|
3454
3466
|
}
|
|
3455
|
-
function classBehind(facts, constant2) {
|
|
3456
|
-
const
|
|
3467
|
+
function classBehind(facts, file, constant2) {
|
|
3468
|
+
const key = constant2.type === "constant" ? `${file}#${constant2.text}` : nodeId(file, constant2);
|
|
3457
3469
|
const bound = new Set(
|
|
3458
|
-
facts.
|
|
3470
|
+
facts.lookup("binds", 0, key).map((row) => String(row[1]))
|
|
3459
3471
|
);
|
|
3460
3472
|
return bound.size === 1 ? [...bound][0] : void 0;
|
|
3461
3473
|
}
|
|
3474
|
+
function modelPattern(constant2, file, options) {
|
|
3475
|
+
const classKey = classBehind(options.facts, file, constant2);
|
|
3476
|
+
if (classKey === void 0) {
|
|
3477
|
+
return void 0;
|
|
3478
|
+
}
|
|
3479
|
+
return options.patterns.find(
|
|
3480
|
+
(candidate) => reachesBase(options.facts, classKey, candidate.baseClasses)
|
|
3481
|
+
);
|
|
3482
|
+
}
|
|
3462
3483
|
function selectorOf(node) {
|
|
3463
3484
|
const picked = [];
|
|
3464
3485
|
const walk = (current) => {
|
|
@@ -3480,7 +3501,87 @@ function selectorOf(node) {
|
|
|
3480
3501
|
walk(node);
|
|
3481
3502
|
return [...new Set(picked)];
|
|
3482
3503
|
}
|
|
3483
|
-
function
|
|
3504
|
+
function storageEffect(call, model, pattern, kind, selector) {
|
|
3505
|
+
const operation = methodOf(call);
|
|
3506
|
+
return {
|
|
3507
|
+
type: "interaction",
|
|
3508
|
+
binding: storageBinding({
|
|
3509
|
+
recognition: "ruby-storage",
|
|
3510
|
+
storageSystem: pattern.storageSystem,
|
|
3511
|
+
scope: pattern.baseClasses[0] ?? "",
|
|
3512
|
+
container: constantName(model)
|
|
3513
|
+
}),
|
|
3514
|
+
callee: call.text,
|
|
3515
|
+
interaction: {
|
|
3516
|
+
class: "storage-access",
|
|
3517
|
+
kind,
|
|
3518
|
+
fields: [],
|
|
3519
|
+
operation,
|
|
3520
|
+
...selector.length > 0 ? { selector } : {}
|
|
3521
|
+
}
|
|
3522
|
+
};
|
|
3523
|
+
}
|
|
3524
|
+
function modelCallEffects(call, file, options) {
|
|
3525
|
+
const constant2 = rootConstant(call);
|
|
3526
|
+
if (constant2 === null) {
|
|
3527
|
+
return [];
|
|
3528
|
+
}
|
|
3529
|
+
const pattern = modelPattern(constant2, file, options);
|
|
3530
|
+
if (pattern === void 0) {
|
|
3531
|
+
return [];
|
|
3532
|
+
}
|
|
3533
|
+
const kind = pattern.writes.includes(methodOf(call)) ? "write" : "read";
|
|
3534
|
+
return [storageEffect(call, constant2, pattern, kind, selectorOf(call))];
|
|
3535
|
+
}
|
|
3536
|
+
function isLoader(node, loader) {
|
|
3537
|
+
if (node === null) {
|
|
3538
|
+
return false;
|
|
3539
|
+
}
|
|
3540
|
+
if (node.type === "identifier") {
|
|
3541
|
+
return node.text === loader.loader;
|
|
3542
|
+
}
|
|
3543
|
+
return node.type === "call" && receiverOf(node) === null && methodOf(node) === loader.loader;
|
|
3544
|
+
}
|
|
3545
|
+
function loaderPick(call, loader) {
|
|
3546
|
+
const method = methodOf(call);
|
|
3547
|
+
const receiver = receiverOf(call);
|
|
3548
|
+
if (receiver === null) {
|
|
3549
|
+
return loader.shortcuts.includes(method) ? call : null;
|
|
3550
|
+
}
|
|
3551
|
+
if (!loader.reads.includes(method) || receiver.type !== "call") {
|
|
3552
|
+
return null;
|
|
3553
|
+
}
|
|
3554
|
+
const picks = methodOf(receiver) === loader.pick && isLoader(receiverOf(receiver), loader);
|
|
3555
|
+
return picks ? receiver : null;
|
|
3556
|
+
}
|
|
3557
|
+
function loaderCallEffects(call, file, options) {
|
|
3558
|
+
const effects = [];
|
|
3559
|
+
for (const loader of options.loaders ?? []) {
|
|
3560
|
+
const pick = loaderPick(call, loader);
|
|
3561
|
+
if (pick === null) {
|
|
3562
|
+
continue;
|
|
3563
|
+
}
|
|
3564
|
+
const args = field(pick, "arguments");
|
|
3565
|
+
for (const argument of args === null ? [] : children3(args)) {
|
|
3566
|
+
if (!isConstant(argument)) {
|
|
3567
|
+
continue;
|
|
3568
|
+
}
|
|
3569
|
+
const pattern = modelPattern(argument, file, options);
|
|
3570
|
+
if (pattern !== void 0) {
|
|
3571
|
+
effects.push(storageEffect(call, argument, pattern, "read", []));
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
return effects;
|
|
3576
|
+
}
|
|
3577
|
+
function effectsOfCall(call, file, options) {
|
|
3578
|
+
const onModel = modelCallEffects(call, file, options);
|
|
3579
|
+
return onModel.length > 0 ? onModel : loaderCallEffects(call, file, options);
|
|
3580
|
+
}
|
|
3581
|
+
function storageClaims(call, file, options) {
|
|
3582
|
+
return options.patterns.length > 0 && effectsOfCall(call, file, options).length > 0;
|
|
3583
|
+
}
|
|
3584
|
+
function storageEffects(calls, file, options) {
|
|
3484
3585
|
if (options.patterns.length === 0) {
|
|
3485
3586
|
return [];
|
|
3486
3587
|
}
|
|
@@ -3496,39 +3597,7 @@ function storageEffects(calls, options) {
|
|
|
3496
3597
|
if (partOfOne.has(call.id)) {
|
|
3497
3598
|
continue;
|
|
3498
3599
|
}
|
|
3499
|
-
|
|
3500
|
-
if (constant2 === null) {
|
|
3501
|
-
continue;
|
|
3502
|
-
}
|
|
3503
|
-
const classKey = classBehind(options.facts, constant2);
|
|
3504
|
-
if (classKey === void 0) {
|
|
3505
|
-
continue;
|
|
3506
|
-
}
|
|
3507
|
-
const pattern = options.patterns.find(
|
|
3508
|
-
(candidate) => reachesBase(options.facts, classKey, candidate.baseClasses)
|
|
3509
|
-
);
|
|
3510
|
-
if (pattern === void 0) {
|
|
3511
|
-
continue;
|
|
3512
|
-
}
|
|
3513
|
-
const operation = methodOf(call);
|
|
3514
|
-
const picked = selectorOf(call);
|
|
3515
|
-
effects.push({
|
|
3516
|
-
type: "interaction",
|
|
3517
|
-
binding: storageBinding({
|
|
3518
|
-
recognition: "ruby-storage",
|
|
3519
|
-
storageSystem: pattern.storageSystem,
|
|
3520
|
-
scope: pattern.baseClasses[0] ?? "",
|
|
3521
|
-
container: constant2.text
|
|
3522
|
-
}),
|
|
3523
|
-
callee: call.text,
|
|
3524
|
-
interaction: {
|
|
3525
|
-
class: "storage-access",
|
|
3526
|
-
kind: pattern.writes.includes(operation) ? "write" : "read",
|
|
3527
|
-
fields: [],
|
|
3528
|
-
operation,
|
|
3529
|
-
...picked.length > 0 ? { selector: picked } : {}
|
|
3530
|
-
}
|
|
3531
|
-
});
|
|
3600
|
+
effects.push(...effectsOfCall(call, file, options));
|
|
3532
3601
|
}
|
|
3533
3602
|
return effects;
|
|
3534
3603
|
}
|
|
@@ -3570,7 +3639,7 @@ function typeShapeFromNode(node, ctx) {
|
|
|
3570
3639
|
};
|
|
3571
3640
|
}
|
|
3572
3641
|
if (node.type === "scope_resolution") {
|
|
3573
|
-
const qualified = node
|
|
3642
|
+
const qualified = compoundName(node);
|
|
3574
3643
|
const scalar = ctx.scalars[scalarLookupName(qualified, ctx.scalarNamePrefixes)];
|
|
3575
3644
|
if (scalar !== void 0) {
|
|
3576
3645
|
return scalar;
|
|
@@ -3737,7 +3806,7 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
|
|
|
3737
3806
|
filter,
|
|
3738
3807
|
pattern,
|
|
3739
3808
|
displayPath,
|
|
3740
|
-
bodyOfMethod(filter.method, options)
|
|
3809
|
+
bodyOfMethod(filter.method, filter.file, options)
|
|
3741
3810
|
);
|
|
3742
3811
|
units.push(raw);
|
|
3743
3812
|
options.onReachSeed?.(raw, {
|
|
@@ -3769,7 +3838,7 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
|
|
|
3769
3838
|
info.qualifiedName,
|
|
3770
3839
|
actionName,
|
|
3771
3840
|
method,
|
|
3772
|
-
options.filePath,
|
|
3841
|
+
{ display: options.filePath, absolute: block.file },
|
|
3773
3842
|
options,
|
|
3774
3843
|
around
|
|
3775
3844
|
);
|
|
@@ -3783,10 +3852,10 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
|
|
|
3783
3852
|
}
|
|
3784
3853
|
return units;
|
|
3785
3854
|
}
|
|
3786
|
-
function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actionName, method,
|
|
3855
|
+
function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actionName, method, file, bodyRead, wrappers = []) {
|
|
3787
3856
|
const range = rangeOf(method);
|
|
3788
3857
|
const route = pattern.routeFor(controllerQualifiedName, actionName);
|
|
3789
|
-
const body = bodyOfMethod(method, bodyRead);
|
|
3858
|
+
const body = bodyOfMethod(method, file.absolute, bodyRead);
|
|
3790
3859
|
const perResponse = responseBranches(
|
|
3791
3860
|
method,
|
|
3792
3861
|
pattern,
|
|
@@ -3798,7 +3867,7 @@ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actio
|
|
|
3798
3867
|
name: actionName,
|
|
3799
3868
|
nameKind: "binding",
|
|
3800
3869
|
kind: "handler",
|
|
3801
|
-
file:
|
|
3870
|
+
file: file.display,
|
|
3802
3871
|
range,
|
|
3803
3872
|
span: spanOf(method),
|
|
3804
3873
|
exportName: actionName,
|
|
@@ -3917,12 +3986,12 @@ function branchesFor(body, range) {
|
|
|
3917
3986
|
}
|
|
3918
3987
|
];
|
|
3919
3988
|
}
|
|
3920
|
-
function bodyOfMethod(method, bodyRead = {}) {
|
|
3989
|
+
function bodyOfMethod(method, file, bodyRead = {}) {
|
|
3921
3990
|
const effects = invocationEffects(method, bodyRead.inheritedMethods);
|
|
3922
3991
|
const storage = bodyRead.storage;
|
|
3923
3992
|
const extra = [
|
|
3924
3993
|
...envReadEffects(method),
|
|
3925
|
-
...storage === void 0 ? [] : storageEffects(callsUnder2(method), storage)
|
|
3994
|
+
...storage === void 0 ? [] : storageEffects(callsUnder2(method), file, storage)
|
|
3926
3995
|
];
|
|
3927
3996
|
return {
|
|
3928
3997
|
bodyContent: methodHasStatements(method) ? "statements" : "empty",
|
|
@@ -3953,7 +4022,7 @@ function methodNotSettled(reason, range) {
|
|
|
3953
4022
|
function bodyFromLookup(found, range, subject, nothingThere, bodyRead) {
|
|
3954
4023
|
const table = {
|
|
3955
4024
|
found: (lookup) => ({
|
|
3956
|
-
...bodyOfMethod(lookup.method, bodyRead),
|
|
4025
|
+
...bodyOfMethod(lookup.method, lookup.block.file, bodyRead),
|
|
3957
4026
|
reachSeed: {
|
|
3958
4027
|
file: lookup.block.file,
|
|
3959
4028
|
node: lookup.method,
|
|
@@ -4317,11 +4386,10 @@ function collectFileConstants(filePath, root) {
|
|
|
4317
4386
|
if ((node.type === "constant" || node.type === "scope_resolution") && !isDeclaration(node, parent)) {
|
|
4318
4387
|
const written = writtenName(node);
|
|
4319
4388
|
if (written !== null) {
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
written,
|
|
4323
|
-
|
|
4324
|
-
});
|
|
4389
|
+
const key = node.type === "constant" ? `${filePath}#${node.text}` : nodeId(filePath, node);
|
|
4390
|
+
references.push(
|
|
4391
|
+
written.startsWith("::") ? { key, written: written.slice(2), nesting: [] } : { key, written, nesting }
|
|
4392
|
+
);
|
|
4325
4393
|
}
|
|
4326
4394
|
}
|
|
4327
4395
|
for (const child of children4(node)) {
|
|
@@ -4913,7 +4981,8 @@ async function scanBody(source, ctx, options) {
|
|
|
4913
4981
|
);
|
|
4914
4982
|
if (outcome.kind === "stopped") {
|
|
4915
4983
|
const stopKey = `${outcome.reason}:${callee}`;
|
|
4916
|
-
|
|
4984
|
+
const claimed = options.storage !== void 0 && storageClaims(call, source.file, options.storage);
|
|
4985
|
+
if (!seen.has(stopKey) && !claimed && worthRecording(outcome.reason)) {
|
|
4917
4986
|
seen.add(stopKey);
|
|
4918
4987
|
stops.push({ callee, reason: outcome.reason });
|
|
4919
4988
|
}
|
|
@@ -4944,7 +5013,7 @@ async function scanBody(source, ctx, options) {
|
|
|
4944
5013
|
}
|
|
4945
5014
|
function libraryUnit(target, options) {
|
|
4946
5015
|
const { file, node, name, exportPath } = target;
|
|
4947
|
-
const body = bodyOfMethod(node, options);
|
|
5016
|
+
const body = bodyOfMethod(node, file, options);
|
|
4948
5017
|
const range = rangeOf(node);
|
|
4949
5018
|
return {
|
|
4950
5019
|
identity: {
|
|
@@ -5101,6 +5170,8 @@ async function extractRubyProject(options) {
|
|
|
5101
5170
|
bindEvaluator(db, { files: parsed, definitions });
|
|
5102
5171
|
});
|
|
5103
5172
|
const storagePatterns = options.packs.flatMap((pack) => pack.storage ?? []);
|
|
5173
|
+
const loaderPatterns = options.packs.flatMap((pack) => pack.loaders ?? []);
|
|
5174
|
+
const storage = storagePatterns.length > 0 ? { facts: db, patterns: storagePatterns, loaders: loaderPatterns } : void 0;
|
|
5104
5175
|
const inheritedMethods = inheritedMethodsIn(options.packs);
|
|
5105
5176
|
const displayPathOf = (file) => options.workspaceRoot !== void 0 ? path4.relative(options.workspaceRoot, file) : file;
|
|
5106
5177
|
const seeds = [];
|
|
@@ -5116,7 +5187,7 @@ async function extractRubyProject(options) {
|
|
|
5116
5187
|
filePath: displayPath,
|
|
5117
5188
|
absoluteFile: file,
|
|
5118
5189
|
cache,
|
|
5119
|
-
...
|
|
5190
|
+
...storage === void 0 ? {} : { storage },
|
|
5120
5191
|
inheritedMethods,
|
|
5121
5192
|
displayPathOf,
|
|
5122
5193
|
facts: db,
|
|
@@ -5195,7 +5266,7 @@ async function extractRubyProject(options) {
|
|
|
5195
5266
|
() => reachedFunctions(seeds, {
|
|
5196
5267
|
files: parsed,
|
|
5197
5268
|
displayPathOf,
|
|
5198
|
-
...
|
|
5269
|
+
...storage === void 0 ? {} : { storage },
|
|
5199
5270
|
inheritedMethods
|
|
5200
5271
|
})
|
|
5201
5272
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suss/adapter-ruby",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Read a Ruby codebase with suss and check what each Rails action or graphql-ruby resolver returns against the code that calls it.",
|
|
6
6
|
"keywords": [
|
|
@@ -49,12 +49,12 @@
|
|
|
49
49
|
"typecheck": "tsc --noEmit"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@suss/behavioral-ir": "0.
|
|
53
|
-
"@suss/datalog": "0.
|
|
54
|
-
"@suss/extractor": "0.
|
|
55
|
-
"@suss/ir-core": "0.
|
|
56
|
-
"@suss/resolution": "0.
|
|
57
|
-
"@suss/values": "0.
|
|
52
|
+
"@suss/behavioral-ir": "0.26.0",
|
|
53
|
+
"@suss/datalog": "0.26.0",
|
|
54
|
+
"@suss/extractor": "0.26.0",
|
|
55
|
+
"@suss/ir-core": "0.26.0",
|
|
56
|
+
"@suss/resolution": "0.26.0",
|
|
57
|
+
"@suss/values": "0.26.0",
|
|
58
58
|
"web-tree-sitter": "^0.26.11"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|