@suss/adapter-ruby 0.3.2-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/LICENSE +205 -0
- package/README.md +49 -0
- package/dist/index.cjs +927 -0
- package/dist/index.d.cts +256 -0
- package/dist/index.d.ts +256 -0
- package/dist/index.js +869 -0
- package/grammar/NOTICE +28 -0
- package/grammar/README.md +35 -0
- package/grammar/tree-sitter-ruby.wasm +0 -0
- package/package.json +64 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,869 @@
|
|
|
1
|
+
// src/ast.ts
|
|
2
|
+
function rangeOf(node) {
|
|
3
|
+
return {
|
|
4
|
+
start: node.startPosition.row + 1,
|
|
5
|
+
end: node.endPosition.row + 1
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
function field(node, name) {
|
|
9
|
+
return node.childForFieldName(name);
|
|
10
|
+
}
|
|
11
|
+
function bodyStatements(body) {
|
|
12
|
+
return body.namedChildren.filter((child) => child !== null);
|
|
13
|
+
}
|
|
14
|
+
function symbolValue(node) {
|
|
15
|
+
return node.type === "simple_symbol" ? node.text.slice(1) : null;
|
|
16
|
+
}
|
|
17
|
+
function instanceMethodsByName(body) {
|
|
18
|
+
const methods = /* @__PURE__ */ new Map();
|
|
19
|
+
for (const stmt of bodyStatements(body)) {
|
|
20
|
+
if (stmt.type !== "method") {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const name = field(stmt, "name")?.text;
|
|
24
|
+
if (name !== void 0) {
|
|
25
|
+
methods.set(name, stmt);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return methods;
|
|
29
|
+
}
|
|
30
|
+
function bareCallArgumentGroups(body, name) {
|
|
31
|
+
const groups = [];
|
|
32
|
+
for (const stmt of bodyStatements(body)) {
|
|
33
|
+
if (stmt.type !== "call" || field(stmt, "receiver") !== null) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (field(stmt, "method")?.text !== name) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const args = field(stmt, "arguments");
|
|
40
|
+
groups.push(args === null ? [] : bodyStatements(args));
|
|
41
|
+
}
|
|
42
|
+
return groups;
|
|
43
|
+
}
|
|
44
|
+
function methodHasStatements(method) {
|
|
45
|
+
const body = field(method, "body");
|
|
46
|
+
if (body === null) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
if (body.type !== "body_statement") {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return bodyStatements(body).length > 0;
|
|
53
|
+
}
|
|
54
|
+
function hashKeySymbolName(node) {
|
|
55
|
+
return node.type === "hash_key_symbol" ? node.text : null;
|
|
56
|
+
}
|
|
57
|
+
function booleanLiteralValue(node) {
|
|
58
|
+
if (node.type === "true") {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
if (node.type === "false") {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
function readCallArgs(argumentList) {
|
|
67
|
+
const positional = [];
|
|
68
|
+
const keyword = {};
|
|
69
|
+
if (argumentList === null) {
|
|
70
|
+
return { positional, keyword };
|
|
71
|
+
}
|
|
72
|
+
for (const child of bodyStatements(argumentList)) {
|
|
73
|
+
if (child.type === "pair") {
|
|
74
|
+
const keyNode = field(child, "key");
|
|
75
|
+
const valueNode = field(child, "value");
|
|
76
|
+
const name = keyNode !== null ? hashKeySymbolName(keyNode) : null;
|
|
77
|
+
if (name !== null && valueNode !== null) {
|
|
78
|
+
keyword[name] = valueNode;
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
positional.push(child);
|
|
83
|
+
}
|
|
84
|
+
return { positional, keyword };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/constantPath.ts
|
|
88
|
+
import fs from "fs";
|
|
89
|
+
import path from "path";
|
|
90
|
+
function underscoreConstantPath(qualifiedName) {
|
|
91
|
+
return qualifiedName.replaceAll("::", "/").replace(/([A-Z\d]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z\d])([A-Z])/g, "$1_$2").replaceAll("-", "_").toLowerCase();
|
|
92
|
+
}
|
|
93
|
+
var PATH_CONVENTIONS = {
|
|
94
|
+
railsUnderscore: underscoreConstantPath
|
|
95
|
+
};
|
|
96
|
+
function resolveConstantFile(root, qualifiedName, convention) {
|
|
97
|
+
const relative = PATH_CONVENTIONS[convention](qualifiedName);
|
|
98
|
+
const file = path.join(root, `${relative}.rb`);
|
|
99
|
+
return fs.existsSync(file) ? file : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/scope.ts
|
|
103
|
+
function qualifyConstantRef(node, nesting) {
|
|
104
|
+
if (node.type === "scope_resolution") {
|
|
105
|
+
return node.text;
|
|
106
|
+
}
|
|
107
|
+
if (node.type === "constant") {
|
|
108
|
+
const innermost = nesting[0] ?? null;
|
|
109
|
+
return innermost === null ? node.text : `${innermost}::${node.text}`;
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
function shadowingClassFor(node, nesting, knownClasses) {
|
|
114
|
+
if (node.type !== "constant") {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
for (const level of nesting) {
|
|
118
|
+
const candidate = `${level}::${node.text}`;
|
|
119
|
+
if (knownClasses.has(candidate)) {
|
|
120
|
+
return candidate;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return knownClasses.has(node.text) ? node.text : null;
|
|
124
|
+
}
|
|
125
|
+
function walkClasses(root, visit) {
|
|
126
|
+
walkDefinitions(root, (info) => {
|
|
127
|
+
if (info.kind === "class") {
|
|
128
|
+
visit(info);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function walkDefinitions(root, visit) {
|
|
133
|
+
walkBody(bodyStatements(root), [], visit);
|
|
134
|
+
}
|
|
135
|
+
function walkBody(statements, nesting, visit) {
|
|
136
|
+
for (const stmt of statements) {
|
|
137
|
+
if (stmt.type === "class") {
|
|
138
|
+
visitClass(stmt, nesting, visit);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (stmt.type === "module") {
|
|
142
|
+
visitModule(stmt, nesting, visit);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function ownIdentity(nameNode, outerNesting) {
|
|
147
|
+
const qualifiedName = qualifyConstantRef(nameNode, outerNesting);
|
|
148
|
+
if (qualifiedName === null) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
return { qualifiedName, bodyNesting: [qualifiedName, ...outerNesting] };
|
|
152
|
+
}
|
|
153
|
+
function visitClass(node, nesting, visit) {
|
|
154
|
+
const nameNode = field(node, "name");
|
|
155
|
+
const identity = nameNode !== null ? ownIdentity(nameNode, nesting) : null;
|
|
156
|
+
if (identity === null) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const superclassWrapper = field(node, "superclass");
|
|
160
|
+
const superclassExpr = superclassWrapper?.namedChild(0) ?? null;
|
|
161
|
+
const superclassQualifiedName = superclassExpr !== null ? qualifyConstantRef(superclassExpr, nesting) : null;
|
|
162
|
+
const bodyNode = field(node, "body");
|
|
163
|
+
visit({
|
|
164
|
+
node,
|
|
165
|
+
kind: "class",
|
|
166
|
+
qualifiedName: identity.qualifiedName,
|
|
167
|
+
superclassQualifiedName,
|
|
168
|
+
bodyNode,
|
|
169
|
+
bodyNesting: identity.bodyNesting
|
|
170
|
+
});
|
|
171
|
+
if (bodyNode !== null) {
|
|
172
|
+
walkBody(bodyStatements(bodyNode), identity.bodyNesting, visit);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function visitModule(node, nesting, visit) {
|
|
176
|
+
const nameNode = field(node, "name");
|
|
177
|
+
const identity = nameNode !== null ? ownIdentity(nameNode, nesting) : null;
|
|
178
|
+
if (identity === null) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const bodyNode = field(node, "body");
|
|
182
|
+
visit({
|
|
183
|
+
node,
|
|
184
|
+
kind: "module",
|
|
185
|
+
qualifiedName: identity.qualifiedName,
|
|
186
|
+
superclassQualifiedName: null,
|
|
187
|
+
bodyNode,
|
|
188
|
+
bodyNesting: identity.bodyNesting
|
|
189
|
+
});
|
|
190
|
+
if (bodyNode !== null) {
|
|
191
|
+
walkBody(bodyStatements(bodyNode), identity.bodyNesting, visit);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function stripTypeSuffixName(qualifiedName) {
|
|
195
|
+
const shortName = qualifiedName.split("::").at(-1) ?? qualifiedName;
|
|
196
|
+
return shortName.endsWith("Type") ? shortName.slice(0, -4) : shortName;
|
|
197
|
+
}
|
|
198
|
+
var TYPE_NAME_CONVENTIONS = {
|
|
199
|
+
stripTypeSuffix: stripTypeSuffixName
|
|
200
|
+
};
|
|
201
|
+
function graphqlTypeNameFromQualified(qualifiedName, convention) {
|
|
202
|
+
return TYPE_NAME_CONVENTIONS[convention](qualifiedName);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// src/ancestry.ts
|
|
206
|
+
var INCLUDE_CALL = "include";
|
|
207
|
+
var PREPEND_CALL = "prepend";
|
|
208
|
+
var DEFINE_METHOD_CALL = "define_method";
|
|
209
|
+
async function reachDefinition(qualifiedName, lookup) {
|
|
210
|
+
const filePath = resolveConstantFile(
|
|
211
|
+
lookup.root,
|
|
212
|
+
qualifiedName,
|
|
213
|
+
lookup.pathConvention
|
|
214
|
+
);
|
|
215
|
+
const fileRoot = filePath === null ? null : await lookup.parsedFile(filePath);
|
|
216
|
+
if (fileRoot === null) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
const all = [];
|
|
220
|
+
walkDefinitions(fileRoot, (info) => all.push(info));
|
|
221
|
+
const matches = all.filter((info) => info.qualifiedName === qualifiedName);
|
|
222
|
+
if (matches.length === 0) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
const knownClasses = new Set(all.map((info) => info.qualifiedName));
|
|
226
|
+
return matches.map((info) => ({ info, knownClasses }));
|
|
227
|
+
}
|
|
228
|
+
function ancestryOf(name, blocks, lookup) {
|
|
229
|
+
return chainOf(
|
|
230
|
+
{ type: "bodies", name, blocks: [...blocks] },
|
|
231
|
+
lookup,
|
|
232
|
+
/* @__PURE__ */ new Set([name])
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
async function chainOf(self, lookup, active) {
|
|
236
|
+
const superChain = await superclassChain(self, lookup, active);
|
|
237
|
+
const present = new Set(superChain.map((ancestor) => ancestor.name));
|
|
238
|
+
present.add(self.name);
|
|
239
|
+
const mixins = await mixinChain(self, INCLUDE_CALL, lookup, active, present);
|
|
240
|
+
const prepends = await mixinChain(
|
|
241
|
+
self,
|
|
242
|
+
PREPEND_CALL,
|
|
243
|
+
lookup,
|
|
244
|
+
active,
|
|
245
|
+
present
|
|
246
|
+
);
|
|
247
|
+
return [...prepends, self, ...mixins, ...superChain];
|
|
248
|
+
}
|
|
249
|
+
async function superclassChain(self, lookup, active) {
|
|
250
|
+
const superclass = superclassOf(self.blocks);
|
|
251
|
+
if (superclass === null || lookup.ancestryRootClassNames.includes(superclass) || active.has(superclass)) {
|
|
252
|
+
return [];
|
|
253
|
+
}
|
|
254
|
+
return chainFor(superclass, lookup, active);
|
|
255
|
+
}
|
|
256
|
+
async function mixinChain(self, callName, lookup, active, present) {
|
|
257
|
+
let chain = [];
|
|
258
|
+
for (const ref of moduleRefs(self.blocks, callName)) {
|
|
259
|
+
if (present.has(ref.name)) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (!ref.readable) {
|
|
263
|
+
present.add(ref.name);
|
|
264
|
+
chain = [{ type: "unfollowed", name: ref.name }, ...chain];
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const inserted = (await chainFor(ref.name, lookup, active)).filter(
|
|
268
|
+
(ancestor) => !present.has(ancestor.name)
|
|
269
|
+
);
|
|
270
|
+
for (const ancestor of inserted) {
|
|
271
|
+
present.add(ancestor.name);
|
|
272
|
+
}
|
|
273
|
+
chain = [...inserted, ...chain];
|
|
274
|
+
}
|
|
275
|
+
return chain;
|
|
276
|
+
}
|
|
277
|
+
async function chainFor(qualifiedName, lookup, active) {
|
|
278
|
+
const blocks = await reachDefinition(qualifiedName, lookup);
|
|
279
|
+
if (blocks === null) {
|
|
280
|
+
return [{ type: "unfollowed", name: qualifiedName }];
|
|
281
|
+
}
|
|
282
|
+
return chainOf(
|
|
283
|
+
{ type: "bodies", name: qualifiedName, blocks },
|
|
284
|
+
lookup,
|
|
285
|
+
/* @__PURE__ */ new Set([...active, qualifiedName])
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
function moduleRefs(blocks, callName) {
|
|
289
|
+
const refs = [];
|
|
290
|
+
for (const block of blocks) {
|
|
291
|
+
if (block.info.bodyNode === null) {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
for (const group of bareCallArgumentGroups(block.info.bodyNode, callName)) {
|
|
295
|
+
for (const arg of [...group].reverse()) {
|
|
296
|
+
const qualified = qualifyConstantRef(arg, block.info.bodyNesting);
|
|
297
|
+
refs.push(
|
|
298
|
+
qualified !== null ? { name: qualified, readable: true } : { name: arg.text, readable: false }
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return refs;
|
|
304
|
+
}
|
|
305
|
+
function superclassOf(blocks) {
|
|
306
|
+
for (const block of blocks) {
|
|
307
|
+
if (block.info.superclassQualifiedName !== null) {
|
|
308
|
+
return block.info.superclassQualifiedName;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
function methodInAncestry(ancestry, name) {
|
|
314
|
+
for (const entry of ancestry) {
|
|
315
|
+
if (entry.type === "unfollowed") {
|
|
316
|
+
return {
|
|
317
|
+
type: "unsettled",
|
|
318
|
+
reason: `inherited from ${entry.name}, which this run did not read`
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
const found = definitionIn(entry.blocks, name);
|
|
322
|
+
if (found.method !== null) {
|
|
323
|
+
return { type: "found", method: found.method };
|
|
324
|
+
}
|
|
325
|
+
if (found.dynamic) {
|
|
326
|
+
return {
|
|
327
|
+
type: "unsettled",
|
|
328
|
+
reason: "defined with define_method, which this reader does not follow"
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return { type: "none" };
|
|
333
|
+
}
|
|
334
|
+
function definitionIn(blocks, name) {
|
|
335
|
+
let method = null;
|
|
336
|
+
let dynamic = false;
|
|
337
|
+
for (const block of blocks) {
|
|
338
|
+
const body = block.info.bodyNode;
|
|
339
|
+
if (body === null) {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
method = instanceMethodsByName(body).get(name) ?? method;
|
|
343
|
+
dynamic ||= bareCallArgumentGroups(body, DEFINE_METHOD_CALL).length > 0;
|
|
344
|
+
}
|
|
345
|
+
return { method, dynamic };
|
|
346
|
+
}
|
|
347
|
+
function inheritedStatements(ancestry) {
|
|
348
|
+
const statements = [];
|
|
349
|
+
for (const entry of [...ancestry].reverse()) {
|
|
350
|
+
if (entry.type !== "bodies") {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
for (const block of entry.blocks) {
|
|
354
|
+
const body = block.info.bodyNode;
|
|
355
|
+
if (body === null) {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
for (const statement of bodyStatements(body)) {
|
|
359
|
+
statements.push({ block, statement });
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return statements;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// src/discovery.ts
|
|
367
|
+
import { dispatchByType, graphqlResolverBinding } from "@suss/behavioral-ir";
|
|
368
|
+
import { unreadableReading } from "@suss/extractor";
|
|
369
|
+
|
|
370
|
+
// src/typeShape.ts
|
|
371
|
+
function scalarLookupName(qualifiedName, prefixes) {
|
|
372
|
+
for (const prefix of prefixes) {
|
|
373
|
+
if (qualifiedName.startsWith(prefix)) {
|
|
374
|
+
return qualifiedName.slice(prefix.length);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return qualifiedName;
|
|
378
|
+
}
|
|
379
|
+
function typeShapeFromNode(node, ctx) {
|
|
380
|
+
if (node.type === "array") {
|
|
381
|
+
const inner = node.namedChild(0);
|
|
382
|
+
if (inner === null) {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
const items = typeShapeFromNode(inner, ctx);
|
|
386
|
+
return items === null ? null : { type: "array", items };
|
|
387
|
+
}
|
|
388
|
+
if (node.type === "constant") {
|
|
389
|
+
const shadow = shadowingClassFor(node, ctx.nesting, ctx.knownClasses);
|
|
390
|
+
if (shadow !== null) {
|
|
391
|
+
return {
|
|
392
|
+
type: "ref",
|
|
393
|
+
name: graphqlTypeNameFromQualified(shadow, ctx.typeNameConvention)
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
const bareScalar = ctx.scalars[node.text];
|
|
397
|
+
if (bareScalar !== void 0) {
|
|
398
|
+
return bareScalar;
|
|
399
|
+
}
|
|
400
|
+
const qualified = qualifyConstantRef(node, ctx.nesting);
|
|
401
|
+
return qualified === null ? null : {
|
|
402
|
+
type: "ref",
|
|
403
|
+
name: graphqlTypeNameFromQualified(qualified, ctx.typeNameConvention)
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
if (node.type === "scope_resolution") {
|
|
407
|
+
const qualified = node.text;
|
|
408
|
+
const scalar = ctx.scalars[scalarLookupName(qualified, ctx.scalarNamePrefixes)];
|
|
409
|
+
if (scalar !== void 0) {
|
|
410
|
+
return scalar;
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
type: "ref",
|
|
414
|
+
name: graphqlTypeNameFromQualified(qualified, ctx.typeNameConvention)
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// src/discovery.ts
|
|
421
|
+
function createFileCache(parse, readFile) {
|
|
422
|
+
const trees = /* @__PURE__ */ new Map();
|
|
423
|
+
return {
|
|
424
|
+
async get(absPath) {
|
|
425
|
+
const cached = trees.get(absPath);
|
|
426
|
+
if (cached !== void 0) {
|
|
427
|
+
return cached;
|
|
428
|
+
}
|
|
429
|
+
const source = readFile(absPath);
|
|
430
|
+
const tree = source !== null ? await parse(source) : null;
|
|
431
|
+
trees.set(absPath, tree);
|
|
432
|
+
return tree;
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
function fieldReadContext(pattern, cache) {
|
|
437
|
+
return {
|
|
438
|
+
pattern,
|
|
439
|
+
cache,
|
|
440
|
+
lookup: {
|
|
441
|
+
root: pattern.root,
|
|
442
|
+
pathConvention: pattern.pathConvention,
|
|
443
|
+
ancestryRootClassNames: pattern.ancestryRootClassNames,
|
|
444
|
+
parsedFile: (absPath) => cache.get(absPath)
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
function typeContext(scope, pattern) {
|
|
449
|
+
return {
|
|
450
|
+
nesting: scope.nesting,
|
|
451
|
+
knownClasses: scope.knownClasses,
|
|
452
|
+
scalars: pattern.scalars,
|
|
453
|
+
scalarNamePrefixes: pattern.scalarNamePrefixes,
|
|
454
|
+
typeNameConvention: pattern.typeNameConvention
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
async function discoverUnits(root, options) {
|
|
458
|
+
const classes = [];
|
|
459
|
+
walkClasses(root, (info) => classes.push(info));
|
|
460
|
+
const knownClasses = new Set(classes.map((info) => info.qualifiedName));
|
|
461
|
+
const units = [];
|
|
462
|
+
for (const info of classes) {
|
|
463
|
+
const ownBlocks = classes.filter((other) => other.qualifiedName === info.qualifiedName).map((other) => ({ info: other, knownClasses }));
|
|
464
|
+
for (const pack of options.packs) {
|
|
465
|
+
for (const pattern of pack.discovery) {
|
|
466
|
+
units.push(
|
|
467
|
+
...await unitsFor(pattern, pack, info, ownBlocks, options)
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return units;
|
|
473
|
+
}
|
|
474
|
+
function unitsFor(pattern, pack, info, ownBlocks, options) {
|
|
475
|
+
const table = {
|
|
476
|
+
graphqlObjectFields: (p) => graphqlObjectFieldUnits(p, pack, info, ownBlocks, options)
|
|
477
|
+
};
|
|
478
|
+
return dispatchByType(table, pattern);
|
|
479
|
+
}
|
|
480
|
+
async function graphqlObjectFieldUnits(pattern, pack, info, ownBlocks, options) {
|
|
481
|
+
if (info.superclassQualifiedName === null || !pattern.baseClassNames.includes(info.superclassQualifiedName) || info.bodyNode === null) {
|
|
482
|
+
return [];
|
|
483
|
+
}
|
|
484
|
+
const typeName = graphqlTypeNameFromQualified(
|
|
485
|
+
info.qualifiedName,
|
|
486
|
+
pattern.typeNameConvention
|
|
487
|
+
);
|
|
488
|
+
const ctx = fieldReadContext(pattern, options.cache);
|
|
489
|
+
const knownClasses = ownBlocks[0]?.knownClasses ?? /* @__PURE__ */ new Set();
|
|
490
|
+
const scope = { nesting: info.bodyNesting, knownClasses };
|
|
491
|
+
const ancestry = await ancestryOf(info.qualifiedName, ownBlocks, ctx.lookup);
|
|
492
|
+
const declsByName = /* @__PURE__ */ new Map();
|
|
493
|
+
for (const stmt of bodyStatements(info.bodyNode)) {
|
|
494
|
+
const decl = await readFieldCall(stmt, scope, ctx, ancestry);
|
|
495
|
+
if (decl !== null) {
|
|
496
|
+
declsByName.set(decl.fieldName, decl);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return [...declsByName.values()].map(
|
|
500
|
+
(decl) => buildFieldUnit(pack, typeName, decl, options.filePath)
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
function bodyOfMethod(method) {
|
|
504
|
+
return {
|
|
505
|
+
bodyContent: methodHasStatements(method) ? "statements" : "empty",
|
|
506
|
+
readings: []
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
var NO_METHOD_BEHIND_IT = {
|
|
510
|
+
bodyContent: "absent",
|
|
511
|
+
readings: []
|
|
512
|
+
};
|
|
513
|
+
function methodNotSettled(reason, range) {
|
|
514
|
+
return { readings: [unreadableReading(reason, range)] };
|
|
515
|
+
}
|
|
516
|
+
function bodyFromLookup(found, range, subject, nothingThere) {
|
|
517
|
+
const table = {
|
|
518
|
+
found: (lookup) => bodyOfMethod(lookup.method),
|
|
519
|
+
unsettled: (lookup) => methodNotSettled(
|
|
520
|
+
`${subject} could be answered by a method ${lookup.reason}, so whether one exists was not settled here`,
|
|
521
|
+
range
|
|
522
|
+
),
|
|
523
|
+
none: () => nothingThere
|
|
524
|
+
};
|
|
525
|
+
return dispatchByType(table, found);
|
|
526
|
+
}
|
|
527
|
+
async function readFieldCall(stmt, scope, ctx, ancestry) {
|
|
528
|
+
if (stmt.type !== "call" || field(stmt, "receiver") !== null) {
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
if (field(stmt, "method")?.text !== ctx.pattern.fieldCallName) {
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
const callArgs = readCallArgs(field(stmt, "arguments"));
|
|
535
|
+
const nameArg = callArgs.positional[0];
|
|
536
|
+
const symbol = nameArg !== void 0 ? symbolValue(nameArg) : null;
|
|
537
|
+
if (symbol === null) {
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
const read = await readFieldShape(
|
|
541
|
+
symbol,
|
|
542
|
+
callArgs,
|
|
543
|
+
scope,
|
|
544
|
+
ctx,
|
|
545
|
+
ancestry,
|
|
546
|
+
rangeOf(stmt)
|
|
547
|
+
);
|
|
548
|
+
return {
|
|
549
|
+
fieldName: resolvedName(symbol, callArgs, ctx.pattern),
|
|
550
|
+
node: stmt,
|
|
551
|
+
contract: read.contract,
|
|
552
|
+
body: read.body
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
function wiringReference(callArgs, wiringKeywords) {
|
|
556
|
+
for (const keyword of wiringKeywords) {
|
|
557
|
+
const ref = callArgs.keyword[keyword];
|
|
558
|
+
if (ref !== void 0) {
|
|
559
|
+
return ref;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
async function readFieldShape(symbol, callArgs, scope, ctx, ancestry, range) {
|
|
565
|
+
const oneHopRef = wiringReference(callArgs, ctx.pattern.wiringKeywords);
|
|
566
|
+
if (oneHopRef !== null) {
|
|
567
|
+
return readWiredClass(oneHopRef, scope, ctx);
|
|
568
|
+
}
|
|
569
|
+
return {
|
|
570
|
+
contract: literalContract(callArgs, scope, ctx),
|
|
571
|
+
body: bodyFromLookup(
|
|
572
|
+
methodInAncestry(ancestry, symbol),
|
|
573
|
+
range,
|
|
574
|
+
"This field",
|
|
575
|
+
NO_METHOD_BEHIND_IT
|
|
576
|
+
)
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
function literalContract(callArgs, scope, ctx) {
|
|
580
|
+
const typeArg = callArgs.positional[1];
|
|
581
|
+
if (typeArg === void 0) {
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
const returnType = typeShapeFromNode(
|
|
585
|
+
typeArg,
|
|
586
|
+
typeContext(scope, ctx.pattern)
|
|
587
|
+
);
|
|
588
|
+
return returnType === null ? null : { returnType, args: [] };
|
|
589
|
+
}
|
|
590
|
+
async function readWiredClass(ref, scope, ctx) {
|
|
591
|
+
const range = rangeOf(ref);
|
|
592
|
+
const targetQualifiedName = qualifyConstantRef(ref, scope.nesting);
|
|
593
|
+
if (targetQualifiedName === null) {
|
|
594
|
+
return {
|
|
595
|
+
contract: null,
|
|
596
|
+
body: methodNotSettled(
|
|
597
|
+
`This field is wired to ${ref.text}, which is not a constant path this reader follows, so nothing about what it does was read here`,
|
|
598
|
+
range
|
|
599
|
+
)
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
const reached = await reachDefinition(targetQualifiedName, ctx.lookup);
|
|
603
|
+
if (reached === null) {
|
|
604
|
+
return {
|
|
605
|
+
contract: null,
|
|
606
|
+
body: methodNotSettled(
|
|
607
|
+
`This field is wired to ${targetQualifiedName}, which this run did not read, so nothing about what it does was read here`,
|
|
608
|
+
range
|
|
609
|
+
)
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
const ancestry = await ancestryOf(targetQualifiedName, reached, ctx.lookup);
|
|
613
|
+
return {
|
|
614
|
+
contract: readClassContract(ancestry, ctx.pattern),
|
|
615
|
+
body: bodyFromLookup(
|
|
616
|
+
methodInAncestry(ancestry, ctx.pattern.resolverMethodName),
|
|
617
|
+
range,
|
|
618
|
+
`This field's ${targetQualifiedName}`,
|
|
619
|
+
methodNotSettled(
|
|
620
|
+
`This field is wired to ${targetQualifiedName}, which defines no ${ctx.pattern.resolverMethodName} method anywhere in its ancestry, so nothing about what it does was read here`,
|
|
621
|
+
range
|
|
622
|
+
)
|
|
623
|
+
)
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
function readTypeCall(callArgs, scope, pattern, out) {
|
|
627
|
+
const typeArg = callArgs.positional[0];
|
|
628
|
+
if (typeArg !== void 0) {
|
|
629
|
+
out.typeCallShape = typeShapeFromNode(typeArg, typeContext(scope, pattern));
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
function readPayloadFieldCall(callArgs, scope, pattern, out) {
|
|
633
|
+
const nameArg = callArgs.positional[0];
|
|
634
|
+
const symbol = nameArg !== void 0 ? symbolValue(nameArg) : null;
|
|
635
|
+
const typeArg = callArgs.positional[1];
|
|
636
|
+
if (symbol === null || typeArg === void 0) {
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
const name = resolvedName(symbol, callArgs, pattern);
|
|
640
|
+
out.fieldProperties[name] = typeShapeFromNode(
|
|
641
|
+
typeArg,
|
|
642
|
+
typeContext(scope, pattern)
|
|
643
|
+
) ?? { type: "unknown" };
|
|
644
|
+
}
|
|
645
|
+
function readArgumentCall(callArgs, scope, pattern, out) {
|
|
646
|
+
const nameArg = callArgs.positional[0];
|
|
647
|
+
const symbol = nameArg !== void 0 ? symbolValue(nameArg) : null;
|
|
648
|
+
if (symbol === null) {
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
const typeArg = callArgs.positional[1];
|
|
652
|
+
const shape = typeArg !== void 0 ? typeShapeFromNode(typeArg, typeContext(scope, pattern)) : null;
|
|
653
|
+
const requiredNode = callArgs.keyword[pattern.requiredKeyword];
|
|
654
|
+
const required = requiredNode !== void 0 ? booleanLiteralValue(requiredNode) ?? pattern.requiredDefault : pattern.requiredDefault;
|
|
655
|
+
const name = resolvedName(symbol, callArgs, pattern);
|
|
656
|
+
out.args.set(name, {
|
|
657
|
+
name,
|
|
658
|
+
type: shape ?? { type: "unknown" },
|
|
659
|
+
required,
|
|
660
|
+
typeText: typeArg?.text ?? null
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
function classCallHandlers(pattern) {
|
|
664
|
+
return {
|
|
665
|
+
[pattern.typeCallName]: readTypeCall,
|
|
666
|
+
[pattern.fieldCallName]: readPayloadFieldCall,
|
|
667
|
+
[pattern.argumentCallName]: readArgumentCall
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
function readClassContract(ancestry, pattern) {
|
|
671
|
+
const out = {
|
|
672
|
+
typeCallShape: null,
|
|
673
|
+
fieldProperties: {},
|
|
674
|
+
args: /* @__PURE__ */ new Map()
|
|
675
|
+
};
|
|
676
|
+
const handlers = classCallHandlers(pattern);
|
|
677
|
+
for (const { block, statement } of inheritedStatements(ancestry)) {
|
|
678
|
+
if (statement.type !== "call" || field(statement, "receiver") !== null) {
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
const method = field(statement, "method")?.text;
|
|
682
|
+
const handler = method !== void 0 ? handlers[method] : void 0;
|
|
683
|
+
if (handler === void 0) {
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
const scope = {
|
|
687
|
+
nesting: block.info.bodyNesting,
|
|
688
|
+
knownClasses: block.knownClasses
|
|
689
|
+
};
|
|
690
|
+
handler(readCallArgs(field(statement, "arguments")), scope, pattern, out);
|
|
691
|
+
}
|
|
692
|
+
const returnType = out.typeCallShape ?? (Object.keys(out.fieldProperties).length > 0 ? { type: "record", properties: out.fieldProperties } : null);
|
|
693
|
+
return returnType === null ? null : { returnType, args: [...out.args.values()] };
|
|
694
|
+
}
|
|
695
|
+
function resolvedName(symbol, callArgs, pattern) {
|
|
696
|
+
const override = callArgs.keyword[pattern.camelizeKeyword];
|
|
697
|
+
const camelizeThis = override !== void 0 ? booleanLiteralValue(override) ?? pattern.camelizeDefault : pattern.camelizeDefault;
|
|
698
|
+
return camelizeThis ? toCamelCase(symbol) : symbol;
|
|
699
|
+
}
|
|
700
|
+
function toCamelCase(name) {
|
|
701
|
+
const [first, ...rest] = name.split("_");
|
|
702
|
+
return [
|
|
703
|
+
first,
|
|
704
|
+
...rest.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
705
|
+
].join("");
|
|
706
|
+
}
|
|
707
|
+
function buildFieldUnit(pack, typeName, decl, filePath) {
|
|
708
|
+
const parameters = (decl.contract?.args ?? []).map(
|
|
709
|
+
(arg, position) => ({
|
|
710
|
+
name: arg.name,
|
|
711
|
+
position,
|
|
712
|
+
role: "args",
|
|
713
|
+
typeText: arg.typeText
|
|
714
|
+
})
|
|
715
|
+
);
|
|
716
|
+
const graphqlDeclaredContract = decl.contract !== null ? {
|
|
717
|
+
returnType: decl.contract.returnType,
|
|
718
|
+
args: decl.contract.args.map((a) => ({
|
|
719
|
+
name: a.name,
|
|
720
|
+
type: a.type,
|
|
721
|
+
required: a.required
|
|
722
|
+
})),
|
|
723
|
+
provenance: "derived",
|
|
724
|
+
framework: pack.name
|
|
725
|
+
} : void 0;
|
|
726
|
+
return {
|
|
727
|
+
identity: {
|
|
728
|
+
name: `${typeName}.${decl.fieldName}`,
|
|
729
|
+
nameKind: "label",
|
|
730
|
+
kind: "resolver",
|
|
731
|
+
file: filePath,
|
|
732
|
+
range: rangeOf(decl.node),
|
|
733
|
+
exportName: null,
|
|
734
|
+
exportPath: null
|
|
735
|
+
},
|
|
736
|
+
boundaryBinding: graphqlResolverBinding({
|
|
737
|
+
transport: pack.protocol,
|
|
738
|
+
recognition: pack.name,
|
|
739
|
+
typeName,
|
|
740
|
+
fieldName: decl.fieldName
|
|
741
|
+
}),
|
|
742
|
+
parameters,
|
|
743
|
+
branches: [],
|
|
744
|
+
...decl.body.bodyContent !== void 0 ? { bodyContent: decl.body.bodyContent } : {},
|
|
745
|
+
dependencyCalls: [],
|
|
746
|
+
declaredContract: null,
|
|
747
|
+
...decl.body.readings.length > 0 ? { readings: decl.body.readings } : {},
|
|
748
|
+
...graphqlDeclaredContract !== void 0 ? { graphqlDeclaredContract } : {}
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// src/facts.ts
|
|
753
|
+
function unitKey(filePath, range, name) {
|
|
754
|
+
return `${filePath}:${range.start}-${range.end}#${name}`;
|
|
755
|
+
}
|
|
756
|
+
function emitEntryFact(db, filePath, range, name) {
|
|
757
|
+
db.add("entry", [unitKey(filePath, range, name)]);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// src/parser.ts
|
|
761
|
+
import fs2 from "fs";
|
|
762
|
+
import path2 from "path";
|
|
763
|
+
import { fileURLToPath } from "url";
|
|
764
|
+
import { Language, Parser } from "web-tree-sitter";
|
|
765
|
+
var GRAMMAR_PATH = path2.join(
|
|
766
|
+
path2.dirname(fileURLToPath(import.meta.url)),
|
|
767
|
+
"..",
|
|
768
|
+
"grammar",
|
|
769
|
+
"tree-sitter-ruby.wasm"
|
|
770
|
+
);
|
|
771
|
+
var languagePromise = null;
|
|
772
|
+
function loadLanguage() {
|
|
773
|
+
languagePromise ??= (async () => {
|
|
774
|
+
if (!fs2.existsSync(GRAMMAR_PATH)) {
|
|
775
|
+
throw new Error(
|
|
776
|
+
`Ruby grammar not found at ${GRAMMAR_PATH}. See grammar/README.md.`
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
await Parser.init();
|
|
780
|
+
return Language.load(GRAMMAR_PATH);
|
|
781
|
+
})();
|
|
782
|
+
return languagePromise;
|
|
783
|
+
}
|
|
784
|
+
async function parseRuby(source) {
|
|
785
|
+
const language = await loadLanguage();
|
|
786
|
+
const parser = new Parser();
|
|
787
|
+
parser.setLanguage(language);
|
|
788
|
+
try {
|
|
789
|
+
const tree = parser.parse(source);
|
|
790
|
+
if (tree === null) {
|
|
791
|
+
throw new Error("tree-sitter produced no tree for this source");
|
|
792
|
+
}
|
|
793
|
+
return tree;
|
|
794
|
+
} finally {
|
|
795
|
+
parser.delete();
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// src/project.ts
|
|
800
|
+
import fs3 from "fs";
|
|
801
|
+
import path3 from "path";
|
|
802
|
+
import { Database } from "@suss/datalog";
|
|
803
|
+
import { assembleSummary } from "@suss/extractor";
|
|
804
|
+
async function extractRubyProject(options) {
|
|
805
|
+
const db = new Database();
|
|
806
|
+
const summaries = [];
|
|
807
|
+
const cache = createFileCache(
|
|
808
|
+
(source) => parseRuby(source).then((tree) => tree.rootNode),
|
|
809
|
+
(absPath) => fs3.existsSync(absPath) ? fs3.readFileSync(absPath, "utf8") : null
|
|
810
|
+
);
|
|
811
|
+
for (const file of options.files) {
|
|
812
|
+
const root = await cache.get(file);
|
|
813
|
+
if (root === null) {
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
const displayPath = options.workspaceRoot !== void 0 ? path3.relative(options.workspaceRoot, file) : file;
|
|
817
|
+
const rawUnits = await discoverUnits(root, {
|
|
818
|
+
packs: options.packs,
|
|
819
|
+
filePath: displayPath,
|
|
820
|
+
cache
|
|
821
|
+
});
|
|
822
|
+
for (const raw of rawUnits) {
|
|
823
|
+
const summary = assembleSummary(raw, { gapHandling: "permissive" });
|
|
824
|
+
summary.confidence = { source: "inferred_static", level: "low" };
|
|
825
|
+
summaries.push(summary);
|
|
826
|
+
emitEntryFact(db, file, raw.identity.range, raw.identity.name);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
return { summaries, facts: db };
|
|
830
|
+
}
|
|
831
|
+
var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set(["vendor", "node_modules", "tmp", ".git"]);
|
|
832
|
+
function findRubyFiles(root) {
|
|
833
|
+
const found = [];
|
|
834
|
+
const walk = (dir) => {
|
|
835
|
+
for (const entry of fs3.readdirSync(dir, { withFileTypes: true })) {
|
|
836
|
+
if (entry.isDirectory()) {
|
|
837
|
+
if (!SKIPPED_DIRECTORIES.has(entry.name)) {
|
|
838
|
+
walk(path3.join(dir, entry.name));
|
|
839
|
+
}
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
if (entry.isFile() && entry.name.endsWith(".rb")) {
|
|
843
|
+
found.push(path3.join(dir, entry.name));
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
walk(root);
|
|
848
|
+
return found.sort();
|
|
849
|
+
}
|
|
850
|
+
export {
|
|
851
|
+
ancestryOf,
|
|
852
|
+
createFileCache,
|
|
853
|
+
discoverUnits,
|
|
854
|
+
emitEntryFact,
|
|
855
|
+
extractRubyProject,
|
|
856
|
+
findRubyFiles,
|
|
857
|
+
graphqlTypeNameFromQualified,
|
|
858
|
+
inheritedStatements,
|
|
859
|
+
methodInAncestry,
|
|
860
|
+
parseRuby,
|
|
861
|
+
qualifyConstantRef,
|
|
862
|
+
reachDefinition,
|
|
863
|
+
resolveConstantFile,
|
|
864
|
+
typeShapeFromNode,
|
|
865
|
+
underscoreConstantPath,
|
|
866
|
+
unitKey,
|
|
867
|
+
walkClasses,
|
|
868
|
+
walkDefinitions
|
|
869
|
+
};
|