@descryy/adapter-go 0.1.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/adapter.d.ts +26 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +247 -0
- package/dist/adapter.js.map +1 -0
- package/dist/client.d.ts +114 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +342 -0
- package/dist/client.js.map +1 -0
- package/dist/extract.d.ts +54 -0
- package/dist/extract.d.ts.map +1 -0
- package/dist/extract.js +871 -0
- package/dist/extract.js.map +1 -0
- package/dist/grpc.d.ts +62 -0
- package/dist/grpc.d.ts.map +1 -0
- package/dist/grpc.js +112 -0
- package/dist/grpc.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/modules.d.ts +37 -0
- package/dist/modules.d.ts.map +1 -0
- package/dist/modules.js +83 -0
- package/dist/modules.js.map +1 -0
- package/dist/parse.d.ts +215 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +890 -0
- package/dist/parse.js.map +1 -0
- package/dist/routes.d.ts +92 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +661 -0
- package/dist/routes.js.map +1 -0
- package/dist/tree-sitter-go.wasm +0 -0
- package/package.json +35 -0
package/dist/extract.js
ADDED
|
@@ -0,0 +1,871 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Go files to Canonical IR.
|
|
3
|
+
*
|
|
4
|
+
* ## The package is the unit, and that changes resolution rather than identity
|
|
5
|
+
*
|
|
6
|
+
* Files in one Go package share a namespace completely: an unqualified name in
|
|
7
|
+
* one file resolves to a declaration in another that never mentions it. So the
|
|
8
|
+
* resolver's first scope is not the file but the package, which is the opposite
|
|
9
|
+
* of Java and TypeScript, where a file must import what it uses.
|
|
10
|
+
*
|
|
11
|
+
* Identity is the easy half. A package's import path is a declared name that
|
|
12
|
+
* other code writes verbatim, so `qsp = importPath::Symbol` needs none of the
|
|
13
|
+
* anchor inference TypeScript needs (DEC-047) or the source-set disambiguation
|
|
14
|
+
* Java needs. Go states it.
|
|
15
|
+
*
|
|
16
|
+
* ## What is deliberately not claimed
|
|
17
|
+
*
|
|
18
|
+
* **Interface satisfaction.** Go's is structural: a type implements an interface
|
|
19
|
+
* by having the right methods, and nothing in the source says so. Computing it
|
|
20
|
+
* needs full method-set comparison, which is R3 evidence, and guessing it from
|
|
21
|
+
* name similarity would be exactly the kind of edge that corrupts every layer
|
|
22
|
+
* above. `IMPLEMENTS` is therefore never emitted, and `capabilities()` says so.
|
|
23
|
+
* Embedding *is* explicit and becomes `INHERITS`.
|
|
24
|
+
*
|
|
25
|
+
* `:=` with anything but a composite literal, dot imports, and any receiver that
|
|
26
|
+
* is not a written name all go to the ledger with their reason.
|
|
27
|
+
*/
|
|
28
|
+
import { edgeId, endpointQsp, nodeId, normaliseEndpointPath, symbolQsp, testCaseQsp, } from "@descryy/ir";
|
|
29
|
+
import { isUniverse } from "./parse.js";
|
|
30
|
+
import { endpointFor } from "./client.js";
|
|
31
|
+
export const LANGUAGE = "go";
|
|
32
|
+
const CONFIDENCE = { 0: 0.5, 1: 0.7, 2: 0.9, 3: 0.95 };
|
|
33
|
+
const REASONS = {
|
|
34
|
+
outside: "names a package this run did not analyse — the standard library, a module in the build " +
|
|
35
|
+
"cache, or a directory outside the analysed set. A scope boundary, not an analysis gap.",
|
|
36
|
+
unmodelled: "the package is in the analysed set but declares no such symbol — most often a method " +
|
|
37
|
+
"promoted from an embedded type this run did not read. An analysis gap.",
|
|
38
|
+
untypedReceiver: "the receiver's type is not written down at this reference: a `:=` whose right-hand side " +
|
|
39
|
+
"is not a composite literal, a chained call, or a name declared twice with different types.",
|
|
40
|
+
dotImport: "reached through a dot import, which binds a whole package into this file's scope. Telling " +
|
|
41
|
+
"such a name from a package-local one needs the imported package's declarations.",
|
|
42
|
+
noImportPath: "no go.mod covers this directory, so the package has no import path and nothing else can " +
|
|
43
|
+
"name it. Disclosed rather than given an invented identity.",
|
|
44
|
+
unknownName: "no declaration for this name in the package, its imports, or the analysed set.",
|
|
45
|
+
routeIdCollision: "this route's method and path are identical to one already emitted, so it collapsed onto " +
|
|
46
|
+
"the same API_ROUTE node and this declaration's own SERVES_API edge and location were " +
|
|
47
|
+
"dropped. The endpoint is real; this specific declaration is not the one the graph kept.",
|
|
48
|
+
};
|
|
49
|
+
/** How many hops of `var A = B; var B = C` to follow before giving up. */
|
|
50
|
+
const ALIAS_PASSES = 4;
|
|
51
|
+
/**
|
|
52
|
+
* The namespace a file's declarations belong to.
|
|
53
|
+
*
|
|
54
|
+
* Usually the import path — but not always, and the exception is written down.
|
|
55
|
+
* Go's **external test package** is a second package living in the same
|
|
56
|
+
* directory: `package storage_test` beside `package storage`. It is a distinct
|
|
57
|
+
* namespace that must import its neighbour by path like anybody else, and it is
|
|
58
|
+
* legal for it to redeclare a name the neighbour already has.
|
|
59
|
+
*
|
|
60
|
+
* Treating the directory as the namespace collapsed both `contextDone` and
|
|
61
|
+
* `makeInt64Pointer` on prometheus — `promql/engine.go` and
|
|
62
|
+
* `promql/engine_test.go` each declare one, two different functions with one
|
|
63
|
+
* node id, and the build-constraint disclosure quietly absorbed the collision.
|
|
64
|
+
* That is the same defect the Java source-set anchoring fixed, arriving through
|
|
65
|
+
* a different door.
|
|
66
|
+
*
|
|
67
|
+
* The rule is Go's own, not a heuristic: an external test package exists only in
|
|
68
|
+
* `_test.go` files and only under a package clause ending in `_test`. Its name
|
|
69
|
+
* carries a `:test` suffix, which no import path can contain — so nothing else
|
|
70
|
+
* can ever name these symbols, which is exactly true of them.
|
|
71
|
+
*/
|
|
72
|
+
function namespaceOf(unit) {
|
|
73
|
+
return unit.file.endsWith("_test.go") && unit.packageName.endsWith("_test")
|
|
74
|
+
? `${unit.importPath}:test`
|
|
75
|
+
: unit.importPath;
|
|
76
|
+
}
|
|
77
|
+
export function extract(input) {
|
|
78
|
+
const nodes = [];
|
|
79
|
+
const edges = [];
|
|
80
|
+
const unresolved = [];
|
|
81
|
+
const seenEdge = new Set();
|
|
82
|
+
const scope = input.scope;
|
|
83
|
+
const push = (edge, level) => {
|
|
84
|
+
if (level > input.reached)
|
|
85
|
+
return false;
|
|
86
|
+
if (edge.from === edge.to)
|
|
87
|
+
return true;
|
|
88
|
+
const key = edgeId(edge.from, edge.to, edge.type);
|
|
89
|
+
if (seenEdge.has(key))
|
|
90
|
+
return true;
|
|
91
|
+
seenEdge.add(key);
|
|
92
|
+
edges.push({
|
|
93
|
+
...edge,
|
|
94
|
+
producedBy: input.producedBy,
|
|
95
|
+
resolution: level,
|
|
96
|
+
confidence: CONFIDENCE[level] ?? 0.5,
|
|
97
|
+
});
|
|
98
|
+
return true;
|
|
99
|
+
};
|
|
100
|
+
const ledger = (from, edgeType, rawTarget, unit, line, reason, attrs) => {
|
|
101
|
+
unresolved.push({
|
|
102
|
+
fromNodeId: from,
|
|
103
|
+
edgeType,
|
|
104
|
+
rawTarget: rawTarget.slice(0, 120),
|
|
105
|
+
file: unit.file,
|
|
106
|
+
line,
|
|
107
|
+
producedBy: input.producedBy,
|
|
108
|
+
reason,
|
|
109
|
+
...(attrs === undefined ? {} : { attrs }),
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
// -------------------------------------------------------------------------
|
|
113
|
+
// Nodes
|
|
114
|
+
// -------------------------------------------------------------------------
|
|
115
|
+
const packages = new Map();
|
|
116
|
+
const moduleIdOf = new Map();
|
|
117
|
+
/**
|
|
118
|
+
* Node id -> the other files declaring that symbol under a different build
|
|
119
|
+
* constraint. Attached to the node afterwards, so the graph states that a
|
|
120
|
+
* choice was made rather than appearing to have found only one declaration.
|
|
121
|
+
*/
|
|
122
|
+
const alternates = new Map();
|
|
123
|
+
/** Subtest -> its node id, for the second pass that emits its edges. */
|
|
124
|
+
const subtestIds = new Map();
|
|
125
|
+
/**
|
|
126
|
+
* Two `t.Run` calls with the same literal name under one test are legal and
|
|
127
|
+
* are two different subtests. `occurrence` keeps both instead of collapsing
|
|
128
|
+
* them, exactly as it does for a repeated `it("…")` title.
|
|
129
|
+
*/
|
|
130
|
+
const testOccurrence = new Map();
|
|
131
|
+
const describe = (unit) => unit.buildConstraint === undefined ? unit.file : `${unit.file} (//go:build ${unit.buildConstraint})`;
|
|
132
|
+
for (const unit of input.files) {
|
|
133
|
+
if (unit.importPath === "")
|
|
134
|
+
continue;
|
|
135
|
+
const namespace = namespaceOf(unit);
|
|
136
|
+
let pkg = packages.get(namespace);
|
|
137
|
+
if (pkg === undefined) {
|
|
138
|
+
pkg = { importPath: namespace, types: new Map(), funcs: new Map(), files: [] };
|
|
139
|
+
packages.set(namespace, pkg);
|
|
140
|
+
}
|
|
141
|
+
pkg.files.push(unit);
|
|
142
|
+
// The file is the MODULE, as in every other adapter. Go's importable unit is
|
|
143
|
+
// the package, but a fileless MODULE is not representable — the Normaliser
|
|
144
|
+
// requires a source file for every node type outside DEC-014's three — so
|
|
145
|
+
// the package lives in the qualified symbol path instead of in a node.
|
|
146
|
+
const stem = unit.file.slice(unit.file.lastIndexOf("/") + 1).replace(/\.go$/, "");
|
|
147
|
+
const moduleId = nodeId(scope, "MODULE", symbolQsp(namespace, [stem]), LANGUAGE);
|
|
148
|
+
moduleIdOf.set(unit.file, moduleId);
|
|
149
|
+
nodes.push({
|
|
150
|
+
id: moduleId,
|
|
151
|
+
type: "MODULE",
|
|
152
|
+
name: `${stem}.go`,
|
|
153
|
+
file: unit.file,
|
|
154
|
+
range: { startLine: 1, endLine: 1 },
|
|
155
|
+
language: LANGUAGE,
|
|
156
|
+
producedBy: input.producedBy,
|
|
157
|
+
resolution: 0,
|
|
158
|
+
attrs: unit.hasError ? { parsedWithErrors: true } : {},
|
|
159
|
+
});
|
|
160
|
+
for (const type of unit.types) {
|
|
161
|
+
const id = nodeId(scope, "CLASS", symbolQsp(namespace, [type.name]), LANGUAGE);
|
|
162
|
+
// A name declared twice in one package does not compile — *unless* the two
|
|
163
|
+
// declarations sit behind mutually exclusive build constraints, which is
|
|
164
|
+
// legal and common: `gin` declares `Binding` in both `binding.go`
|
|
165
|
+
// (`!nomsgpack`) and `binding_nomsgpack.go` (`nomsgpack`). Exactly one is
|
|
166
|
+
// ever compiled and this adapter is not told which, so the symbol is one
|
|
167
|
+
// node and the alternatives are disclosed on it.
|
|
168
|
+
const existing = pkg.types.get(type.name);
|
|
169
|
+
if (existing !== undefined) {
|
|
170
|
+
alternates.set(id, [...(alternates.get(id) ?? []), describe(unit)]);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
pkg.types.set(type.name, { id, type, unit });
|
|
174
|
+
nodes.push({
|
|
175
|
+
id,
|
|
176
|
+
type: "CLASS",
|
|
177
|
+
name: type.name,
|
|
178
|
+
file: unit.file,
|
|
179
|
+
range: { startLine: type.startLine, endLine: type.endLine },
|
|
180
|
+
language: LANGUAGE,
|
|
181
|
+
producedBy: input.producedBy,
|
|
182
|
+
resolution: 0,
|
|
183
|
+
attrs: { declarationForm: type.form },
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
for (const fn of unit.funcs) {
|
|
187
|
+
const isCase = fn.isTest;
|
|
188
|
+
const id = isCase
|
|
189
|
+
? nodeId(scope, "TEST_CASE", testCaseQsp(namespace, [], fn.name), LANGUAGE)
|
|
190
|
+
: nodeId(scope, "FUNCTION", symbolQsp(namespace, fn.key.split(".")), LANGUAGE);
|
|
191
|
+
if (pkg.funcs.has(fn.key)) {
|
|
192
|
+
alternates.set(id, [...(alternates.get(id) ?? []), describe(unit)]);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
pkg.funcs.set(fn.key, { id, fn, unit });
|
|
196
|
+
nodes.push({
|
|
197
|
+
id,
|
|
198
|
+
type: isCase ? "TEST_CASE" : "FUNCTION",
|
|
199
|
+
name: fn.name,
|
|
200
|
+
file: unit.file,
|
|
201
|
+
range: { startLine: fn.startLine, endLine: fn.endLine },
|
|
202
|
+
language: LANGUAGE,
|
|
203
|
+
producedBy: input.producedBy,
|
|
204
|
+
resolution: 0,
|
|
205
|
+
attrs: fn.receiver === undefined ? {} : { receiver: fn.receiver },
|
|
206
|
+
});
|
|
207
|
+
// A subtest is a test case in its own right — `go test` names it, runs it
|
|
208
|
+
// and reports it separately — so it is a node, and the test it sits in is
|
|
209
|
+
// its suite. Dropping the suite would drop half of its identity (DEC-011).
|
|
210
|
+
for (const subtest of fn.subtests) {
|
|
211
|
+
const title = `${namespace}::${[...subtest.suite, subtest.name].join("::")}`;
|
|
212
|
+
const occurrence = (testOccurrence.get(title) ?? 0) + 1;
|
|
213
|
+
testOccurrence.set(title, occurrence);
|
|
214
|
+
const subtestId = nodeId(scope, "TEST_CASE", testCaseQsp(namespace, subtest.suite, subtest.name, occurrence), LANGUAGE);
|
|
215
|
+
subtestIds.set(subtest, subtestId);
|
|
216
|
+
nodes.push({
|
|
217
|
+
id: subtestId,
|
|
218
|
+
type: "TEST_CASE",
|
|
219
|
+
name: subtest.name,
|
|
220
|
+
file: unit.file,
|
|
221
|
+
range: { startLine: subtest.startLine, endLine: subtest.endLine },
|
|
222
|
+
language: LANGUAGE,
|
|
223
|
+
producedBy: input.producedBy,
|
|
224
|
+
resolution: 0,
|
|
225
|
+
attrs: { suite: subtest.suite },
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// -------------------------------------------------------------------------
|
|
231
|
+
// Resolution
|
|
232
|
+
// -------------------------------------------------------------------------
|
|
233
|
+
/** A written type name, split into its package qualifier and its own name. */
|
|
234
|
+
const split = (written) => {
|
|
235
|
+
const cut = written.indexOf(".");
|
|
236
|
+
if (cut === -1)
|
|
237
|
+
return { name: written };
|
|
238
|
+
return { qualifier: written.slice(0, cut), name: written.slice(cut + 1) };
|
|
239
|
+
};
|
|
240
|
+
const resolveType = (unit, written) => {
|
|
241
|
+
const { qualifier, name } = split(written);
|
|
242
|
+
if (qualifier === undefined) {
|
|
243
|
+
// The whole package, not just this file — Go's defining scope rule.
|
|
244
|
+
const own = packages.get(namespaceOf(unit))?.types.get(name);
|
|
245
|
+
if (own !== undefined)
|
|
246
|
+
return { declared: own };
|
|
247
|
+
return { reason: REASONS.unknownName };
|
|
248
|
+
}
|
|
249
|
+
const path = unit.imports.get(qualifier);
|
|
250
|
+
if (path === undefined)
|
|
251
|
+
return { reason: REASONS.outside };
|
|
252
|
+
const target = packages.get(path)?.types.get(name);
|
|
253
|
+
return target === undefined ? { reason: REASONS.outside } : { declared: target };
|
|
254
|
+
};
|
|
255
|
+
/**
|
|
256
|
+
* A method on a type, following embedded types the way Go promotes them.
|
|
257
|
+
*
|
|
258
|
+
* Promotion is the one place Go's implicit behaviour is worth following,
|
|
259
|
+
* because embedding is written down: `struct { Base }` says so in the source.
|
|
260
|
+
* Interface satisfaction is not written down anywhere and is not followed.
|
|
261
|
+
*/
|
|
262
|
+
const findMethod = (start, name, depth = 0) => {
|
|
263
|
+
if (depth > 8)
|
|
264
|
+
return undefined;
|
|
265
|
+
const pkg = packages.get(namespaceOf(start.unit));
|
|
266
|
+
const direct = pkg?.funcs.get(`${start.type.name}.${name}`);
|
|
267
|
+
if (direct !== undefined)
|
|
268
|
+
return direct;
|
|
269
|
+
for (const embedded of start.type.embeds) {
|
|
270
|
+
const resolved = resolveType(start.unit, embedded);
|
|
271
|
+
if (!("declared" in resolved))
|
|
272
|
+
continue;
|
|
273
|
+
const found = findMethod(resolved.declared, name, depth + 1);
|
|
274
|
+
if (found !== undefined)
|
|
275
|
+
return found;
|
|
276
|
+
}
|
|
277
|
+
return undefined;
|
|
278
|
+
};
|
|
279
|
+
/** A field on a type, following embedding the way Go promotes it. */
|
|
280
|
+
const findField = (start, name, depth = 0) => {
|
|
281
|
+
if (depth > 8)
|
|
282
|
+
return undefined;
|
|
283
|
+
const direct = start.type.fields.get(name);
|
|
284
|
+
if (direct !== undefined)
|
|
285
|
+
return direct;
|
|
286
|
+
for (const embedded of start.type.embeds) {
|
|
287
|
+
const resolved = resolveType(start.unit, embedded);
|
|
288
|
+
if (!("declared" in resolved))
|
|
289
|
+
continue;
|
|
290
|
+
const found = findField(resolved.declared, name, depth + 1);
|
|
291
|
+
if (found !== undefined)
|
|
292
|
+
return found;
|
|
293
|
+
}
|
|
294
|
+
return undefined;
|
|
295
|
+
};
|
|
296
|
+
/**
|
|
297
|
+
* A package-level value's written type, from any file of the same package.
|
|
298
|
+
*
|
|
299
|
+
* Go's package scope is shared across files completely, so a `var` declared
|
|
300
|
+
* in one file types a reference in another. Searching only the reference's
|
|
301
|
+
* own file would reproduce the wrong scope.
|
|
302
|
+
*/
|
|
303
|
+
const packageVarType = (unit, name) => {
|
|
304
|
+
const pkg = packages.get(namespaceOf(unit));
|
|
305
|
+
for (const file of pkg?.files ?? []) {
|
|
306
|
+
const written = file.packageVars.get(name);
|
|
307
|
+
if (written !== undefined)
|
|
308
|
+
return written;
|
|
309
|
+
}
|
|
310
|
+
return undefined;
|
|
311
|
+
};
|
|
312
|
+
/**
|
|
313
|
+
* The element type of a collection: `[]Order` and `map[string]Order` -> `Order`.
|
|
314
|
+
*
|
|
315
|
+
* Only the two forms that write the element type down. A channel or a generic
|
|
316
|
+
* instantiation is left alone rather than guessed at.
|
|
317
|
+
*/
|
|
318
|
+
const elementOf = (written) => {
|
|
319
|
+
const slice = /^\[\s*\d*\s*\]\s*\*?(.+)$/.exec(written);
|
|
320
|
+
if (slice?.[1] !== undefined)
|
|
321
|
+
return slice[1].trim();
|
|
322
|
+
const map = /^map\[[^\]]*\]\s*\*?(.+)$/.exec(written);
|
|
323
|
+
if (map?.[1] !== undefined)
|
|
324
|
+
return map[1].trim();
|
|
325
|
+
return undefined;
|
|
326
|
+
};
|
|
327
|
+
/**
|
|
328
|
+
* The type of a receiver written as `x`, `x.field`, or `x.field.field`.
|
|
329
|
+
*
|
|
330
|
+
* Every hop reads a declared type: the local's own, then the field's, out of
|
|
331
|
+
* the struct declaration. `enh.lb.Set(…)` and `it.br.readVarint()` are the
|
|
332
|
+
* shape — four of the eleven measured recall misses across gin and prometheus
|
|
333
|
+
* — and Java closed the same gap with the same evidence.
|
|
334
|
+
*/
|
|
335
|
+
const typeOfPath = (unit, scope, path, depth = 0) => {
|
|
336
|
+
const parts = path.split(".");
|
|
337
|
+
const head = parts[0];
|
|
338
|
+
if (head === undefined)
|
|
339
|
+
return { reason: REASONS.untypedReceiver };
|
|
340
|
+
// A function-local name first, then a package-level one. Package scope was
|
|
341
|
+
// never consulted, so `var JSON = jsonBinding{}` followed by `JSON.Bind(…)`
|
|
342
|
+
// read as an untyped receiver even though the type is written two lines up.
|
|
343
|
+
const written = scope.locals.get(head) ?? packageVarType(unit, head);
|
|
344
|
+
let current;
|
|
345
|
+
if (written !== undefined && written !== null) {
|
|
346
|
+
current = resolveType(unit, written);
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
const fromCall = resultTypeOf(unit, scope, head, depth);
|
|
350
|
+
if (fromCall === undefined)
|
|
351
|
+
return { reason: REASONS.untypedReceiver };
|
|
352
|
+
current = fromCall;
|
|
353
|
+
}
|
|
354
|
+
for (const field of parts.slice(1)) {
|
|
355
|
+
if (!("declared" in current))
|
|
356
|
+
return current;
|
|
357
|
+
const fieldType = findField(current.declared, field);
|
|
358
|
+
if (fieldType === undefined || fieldType === null)
|
|
359
|
+
return { reason: REASONS.untypedReceiver };
|
|
360
|
+
current = resolveType(current.declared.unit, fieldType);
|
|
361
|
+
}
|
|
362
|
+
return current;
|
|
363
|
+
};
|
|
364
|
+
/**
|
|
365
|
+
* The type a `x, err := f(…)` binding got from `f`'s declared results.
|
|
366
|
+
*
|
|
367
|
+
* The result type is written in the *callee's* file, so it is resolved against
|
|
368
|
+
* the callee's unit — a bare `Config` there means that package's `Config`.
|
|
369
|
+
* `depth` stops a mutually recursive pair of bindings from looping.
|
|
370
|
+
*/
|
|
371
|
+
const resultTypeOf = (unit, scope, name, depth) => {
|
|
372
|
+
if (depth > 4)
|
|
373
|
+
return undefined;
|
|
374
|
+
const binding = scope.pending.get(name);
|
|
375
|
+
if (binding === undefined)
|
|
376
|
+
return undefined;
|
|
377
|
+
let callee;
|
|
378
|
+
if (binding.receiver === undefined) {
|
|
379
|
+
callee = packages.get(namespaceOf(unit))?.funcs.get(binding.name);
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
const path = unit.imports.get(binding.receiver);
|
|
383
|
+
if (path !== undefined) {
|
|
384
|
+
callee = packages.get(path)?.funcs.get(binding.name);
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
// `x := y.Method(…)` — the callee is itself reached through a value.
|
|
388
|
+
const owner = typeOfPath(unit, scope, binding.receiver, depth + 1);
|
|
389
|
+
if ("declared" in owner)
|
|
390
|
+
callee = findMethod(owner.declared, binding.name);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (callee === undefined)
|
|
394
|
+
return undefined;
|
|
395
|
+
const written = callee.fn.results[binding.index];
|
|
396
|
+
if (written === undefined)
|
|
397
|
+
return undefined;
|
|
398
|
+
return resolveType(callee.unit, written);
|
|
399
|
+
};
|
|
400
|
+
/** A written function name — bare or `pkg.Name` — against the analysed set. */
|
|
401
|
+
const resolveFuncName = (unit, written) => {
|
|
402
|
+
const { qualifier, name } = split(written);
|
|
403
|
+
if (qualifier === undefined)
|
|
404
|
+
return packages.get(namespaceOf(unit))?.funcs.get(name);
|
|
405
|
+
const path = unit.imports.get(qualifier);
|
|
406
|
+
return path === undefined ? undefined : packages.get(path)?.funcs.get(name);
|
|
407
|
+
};
|
|
408
|
+
/**
|
|
409
|
+
* Re-export bindings: `var ValidateOrder = validation.ValidateOrder`.
|
|
410
|
+
*
|
|
411
|
+
* The binding is registered under the *target's* declaration, so a call
|
|
412
|
+
* through it terminates at the definition and no node is created for the
|
|
413
|
+
* binding itself — which is exactly what golden 03 asserts.
|
|
414
|
+
*
|
|
415
|
+
* A real declaration always wins; Go would not compile both anyway. A binding
|
|
416
|
+
* whose target is not a function in the analysed set simply does not bind, and
|
|
417
|
+
* nothing is filed: a package-level `var` is usually an ordinary value, and
|
|
418
|
+
* filing every one of them as a failed edge would claim they were candidates.
|
|
419
|
+
*/
|
|
420
|
+
for (let pass = 0; pass < ALIAS_PASSES; pass += 1) {
|
|
421
|
+
let bound = 0;
|
|
422
|
+
for (const unit of input.files) {
|
|
423
|
+
const pkg = packages.get(namespaceOf(unit));
|
|
424
|
+
if (pkg === undefined)
|
|
425
|
+
continue;
|
|
426
|
+
for (const alias of unit.aliases) {
|
|
427
|
+
if (pkg.funcs.has(alias.name))
|
|
428
|
+
continue;
|
|
429
|
+
const target = resolveFuncName(unit, alias.target);
|
|
430
|
+
if (target === undefined)
|
|
431
|
+
continue;
|
|
432
|
+
pkg.funcs.set(alias.name, target);
|
|
433
|
+
bound += 1;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if (bound === 0)
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
// -------------------------------------------------------------------------
|
|
440
|
+
// Edges
|
|
441
|
+
// -------------------------------------------------------------------------
|
|
442
|
+
/** The caller's half of the HTTP boundary, minted with the routes below. */
|
|
443
|
+
const clientCallSites = [];
|
|
444
|
+
for (const unit of input.files) {
|
|
445
|
+
const moduleId = moduleIdOf.get(unit.file);
|
|
446
|
+
if (moduleId === undefined) {
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
// IMPORTS. Go imports a *package*, and a package is its files — so the edge
|
|
450
|
+
// fans out to each file of the target. That is not an approximation: a file
|
|
451
|
+
// importing a package depends on every declaration in it, which is exactly
|
|
452
|
+
// what blast radius needs to traverse.
|
|
453
|
+
for (const each of unit.importPaths) {
|
|
454
|
+
const target = packages.get(each.path);
|
|
455
|
+
if (target === undefined) {
|
|
456
|
+
ledger(moduleId, "IMPORTS", each.path, unit, each.line, REASONS.outside);
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
for (const file of target.files) {
|
|
460
|
+
const id = moduleIdOf.get(file.file);
|
|
461
|
+
if (id !== undefined)
|
|
462
|
+
push({ from: moduleId, to: id, type: "IMPORTS" }, 1);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
for (const type of unit.types) {
|
|
466
|
+
const declared = packages.get(namespaceOf(unit))?.types.get(type.name);
|
|
467
|
+
if (declared === undefined || declared.unit.file !== unit.file)
|
|
468
|
+
continue;
|
|
469
|
+
for (const embedded of type.embeds) {
|
|
470
|
+
const resolved = resolveType(unit, embedded);
|
|
471
|
+
if ("declared" in resolved) {
|
|
472
|
+
if (!push({ from: declared.id, to: resolved.declared.id, type: "INHERITS" }, 2)) {
|
|
473
|
+
ledger(declared.id, "INHERITS", embedded, unit, type.startLine, REASONS.unknownName);
|
|
474
|
+
}
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
ledger(declared.id, "INHERITS", embedded, unit, type.startLine, resolved.reason);
|
|
478
|
+
}
|
|
479
|
+
for (const ref of type.typeRefs)
|
|
480
|
+
emitTypeRef(declared.id, unit, ref);
|
|
481
|
+
}
|
|
482
|
+
for (const fn of unit.funcs) {
|
|
483
|
+
const declared = packages.get(namespaceOf(unit))?.funcs.get(fn.key);
|
|
484
|
+
if (declared === undefined || declared.unit.file !== unit.file)
|
|
485
|
+
continue;
|
|
486
|
+
emitRefs(declared.id, fn, fn.isTest, unit);
|
|
487
|
+
for (const subtest of fn.subtests) {
|
|
488
|
+
const subtestId = subtestIds.get(subtest);
|
|
489
|
+
if (subtestId !== undefined)
|
|
490
|
+
emitRefs(subtestId, subtest, true, unit);
|
|
491
|
+
}
|
|
492
|
+
// The caller's half of the HTTP boundary — DEC-262's fork, Go's half.
|
|
493
|
+
// Collected here, where the enclosing FUNCTION's own id is finally known,
|
|
494
|
+
// and emitted with the routes below so both halves mint one API_ENDPOINT.
|
|
495
|
+
for (const call of fn.clientCalls)
|
|
496
|
+
clientCallSites.push({ ...call, fromId: declared.id });
|
|
497
|
+
for (const refusal of fn.clientRefusals) {
|
|
498
|
+
ledger(declared.id, "USES_API", refusal.raw, unit, refusal.line, refusal.reason,
|
|
499
|
+
// Unset `refusalClass` stays legal and means unclassified — DEC-242 — so
|
|
500
|
+
// `attrs` is omitted entirely rather than sent with an `undefined` field.
|
|
501
|
+
refusal.refusalClass === undefined
|
|
502
|
+
? undefined
|
|
503
|
+
: {
|
|
504
|
+
blockedBy: refusal.blockedBy,
|
|
505
|
+
refusalClass: refusal.refusalClass,
|
|
506
|
+
...(refusal.argumentKind === undefined ? {} : { argumentKind: refusal.argumentKind }),
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
function emitRefs(from, scope, isTest, unit) {
|
|
512
|
+
for (const ref of scope.refs) {
|
|
513
|
+
if (ref.kind === "type") {
|
|
514
|
+
emitTypeRef(from, unit, ref);
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
if (ref.kind === "member") {
|
|
518
|
+
// `x.Field` where `x` is a variable is a field read, not a dependency
|
|
519
|
+
// on a type. Nothing is emitted and nothing is filed — the ledger
|
|
520
|
+
// records references that could have been edges, and this was never a
|
|
521
|
+
// candidate.
|
|
522
|
+
if (ref.receiver !== undefined && scope.locals.has(ref.receiver))
|
|
523
|
+
continue;
|
|
524
|
+
emitTypeRef(from, unit, ref);
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
emitCall(from, scope, isTest, unit, ref);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
function emitTypeRef(from, unit, ref) {
|
|
531
|
+
const resolved = resolveType(unit, ref.name);
|
|
532
|
+
if ("declared" in resolved) {
|
|
533
|
+
if (!push({ from, to: resolved.declared.id, type: "USES_TYPE" }, 2)) {
|
|
534
|
+
ledger(from, "USES_TYPE", ref.name, unit, ref.line, REASONS.unknownName);
|
|
535
|
+
}
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
ledger(from, "USES_TYPE", ref.name, unit, ref.line, resolved.reason);
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* `T(x)` — a conversion, which Go spells exactly like a call.
|
|
542
|
+
*
|
|
543
|
+
* Safe because Go has one namespace per package: a name that resolves to a
|
|
544
|
+
* declared type cannot also be a function, so this is not a guess between two
|
|
545
|
+
* readings. The local check is the one real hazard — a variable may shadow a
|
|
546
|
+
* type name, and then the call is a call.
|
|
547
|
+
*/
|
|
548
|
+
function emitConversion(from, scope, unit, ref, pkg) {
|
|
549
|
+
if (scope.locals.has(ref.name))
|
|
550
|
+
return false;
|
|
551
|
+
const type = pkg?.types.get(ref.name);
|
|
552
|
+
if (type === undefined)
|
|
553
|
+
return false;
|
|
554
|
+
if (!push({ from, to: type.id, type: "USES_TYPE" }, 2)) {
|
|
555
|
+
ledger(from, "USES_TYPE", ref.raw, unit, ref.line, REASONS.unknownName);
|
|
556
|
+
}
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
/** A method call whose receiver's type the site itself states, or a call result. */
|
|
560
|
+
function emitAgainstOwner(from, unit, ref, edgeType, owner) {
|
|
561
|
+
if (!("declared" in owner)) {
|
|
562
|
+
ledger(from, "CALLS", ref.raw, unit, ref.line, owner.reason);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
const target = findMethod(owner.declared, ref.name);
|
|
566
|
+
if (target === undefined) {
|
|
567
|
+
ledger(from, "CALLS", ref.raw, unit, ref.line, REASONS.unmodelled);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (!push({ from, to: target.id, type: edgeType }, 2)) {
|
|
571
|
+
ledger(from, "CALLS", ref.raw, unit, ref.line, REASONS.unknownName);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
function emitCall(from, scope, isTest, unit, ref) {
|
|
575
|
+
const edgeType = isTest ? "TESTS" : "CALLS";
|
|
576
|
+
const receiver = ref.receiver;
|
|
577
|
+
// `p.(*listPostings).Len()` and `(IndentedJSON{data}).Render(w)` — the site
|
|
578
|
+
// states the receiver's type, so nothing is inferred.
|
|
579
|
+
if (ref.receiverType !== undefined) {
|
|
580
|
+
emitAgainstOwner(from, unit, ref, edgeType, resolveType(unit, ref.receiverType));
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
// `xs[i].M()` — the element type is written in the collection's own
|
|
584
|
+
// declaration, so following it is resolution rather than inference. A
|
|
585
|
+
// collection whose element type is not written down (a channel, a generic
|
|
586
|
+
// instantiation) falls through to the ledger unchanged.
|
|
587
|
+
if (ref.receiverIndex !== undefined) {
|
|
588
|
+
const collection = scope.locals.get(ref.receiverIndex) ?? packageVarType(unit, ref.receiverIndex);
|
|
589
|
+
const element = collection === undefined || collection === null ? undefined : elementOf(collection);
|
|
590
|
+
if (element === undefined) {
|
|
591
|
+
ledger(from, "CALLS", ref.raw, unit, ref.line, REASONS.untypedReceiver);
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
emitAgainstOwner(from, unit, ref, edgeType, resolveType(unit, element));
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
// `engine().RunUnix(file)` — the receiver is a call result, and the result
|
|
598
|
+
// type is written in the callee's signature.
|
|
599
|
+
if (ref.receiverCall !== undefined) {
|
|
600
|
+
const binding = ref.receiverCall;
|
|
601
|
+
let callee;
|
|
602
|
+
if (binding.receiver === undefined) {
|
|
603
|
+
callee = packages.get(namespaceOf(unit))?.funcs.get(binding.name);
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
const path = unit.imports.get(binding.receiver);
|
|
607
|
+
if (path !== undefined)
|
|
608
|
+
callee = packages.get(path)?.funcs.get(binding.name);
|
|
609
|
+
else {
|
|
610
|
+
const owner = typeOfPath(unit, scope, binding.receiver);
|
|
611
|
+
if ("declared" in owner)
|
|
612
|
+
callee = findMethod(owner.declared, binding.name);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
const written = callee?.fn.results[0];
|
|
616
|
+
if (callee === undefined || written === undefined) {
|
|
617
|
+
ledger(from, "CALLS", ref.raw, unit, ref.line, REASONS.untypedReceiver);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
emitAgainstOwner(from, unit, ref, edgeType, resolveType(callee.unit, written));
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
// Unqualified: a package-level function, visible across every file of the
|
|
624
|
+
// package without any import.
|
|
625
|
+
if (receiver === undefined) {
|
|
626
|
+
const own = packages.get(namespaceOf(unit));
|
|
627
|
+
const target = own?.funcs.get(ref.name);
|
|
628
|
+
if (target === undefined) {
|
|
629
|
+
if (emitConversion(from, scope, unit, ref, own))
|
|
630
|
+
return;
|
|
631
|
+
// `len(x)`, `append(…)`, `int64(n)` — the universe block, checked only
|
|
632
|
+
// after the package's own declarations because a package may legally
|
|
633
|
+
// shadow a builtin. Filing these said "no declaration for this name",
|
|
634
|
+
// which is not merely unhelpful but false, about 11,000 times on
|
|
635
|
+
// prometheus. The same fix `baseTypeName` already had, on the other side.
|
|
636
|
+
if (isUniverse(ref.name))
|
|
637
|
+
return;
|
|
638
|
+
ledger(from, "CALLS", ref.raw, unit, ref.line, REASONS.unknownName);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (!push({ from, to: target.id, type: edgeType }, 2)) {
|
|
642
|
+
ledger(from, "CALLS", ref.raw, unit, ref.line, REASONS.unknownName);
|
|
643
|
+
}
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
if (receiver === "(computed)") {
|
|
647
|
+
ledger(from, "CALLS", ref.raw, unit, ref.line, REASONS.untypedReceiver);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
// `pkg.F()` — the receiver is an imported package rather than a value.
|
|
651
|
+
const imported = receiver.includes(".") ? undefined : unit.imports.get(receiver);
|
|
652
|
+
if (imported !== undefined) {
|
|
653
|
+
const other = packages.get(imported);
|
|
654
|
+
const target = other?.funcs.get(ref.name);
|
|
655
|
+
if (target === undefined) {
|
|
656
|
+
if (emitConversion(from, scope, unit, ref, other))
|
|
657
|
+
return;
|
|
658
|
+
ledger(from, "CALLS", ref.raw, unit, ref.line, REASONS.outside);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
if (!push({ from, to: target.id, type: edgeType }, 2)) {
|
|
662
|
+
ledger(from, "CALLS", ref.raw, unit, ref.line, REASONS.unknownName);
|
|
663
|
+
}
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
// `x.M()`, `x.field.M()` — the receiver is a value whose type is written
|
|
667
|
+
// down, here or in the declaration of whatever it was taken from.
|
|
668
|
+
const owner = typeOfPath(unit, scope, receiver);
|
|
669
|
+
if (!("declared" in owner)) {
|
|
670
|
+
ledger(from, "CALLS", ref.raw, unit, ref.line, owner.reason);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
const target = findMethod(owner.declared, ref.name);
|
|
674
|
+
if (target === undefined) {
|
|
675
|
+
ledger(from, "CALLS", ref.raw, unit, ref.line, REASONS.unmodelled);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
if (!push({ from, to: target.id, type: edgeType }, 2)) {
|
|
679
|
+
ledger(from, "CALLS", ref.raw, unit, ref.line, REASONS.unknownName);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
// Disclosure, not decoration: a symbol declared in several build variants has
|
|
683
|
+
// one node, and the node says which files it stood in for.
|
|
684
|
+
const disclosed = nodes.map((node) => {
|
|
685
|
+
const others = alternates.get(node.id);
|
|
686
|
+
if (others === undefined)
|
|
687
|
+
return node;
|
|
688
|
+
return { ...node, attrs: { ...node.attrs, buildConstrainedAlternates: others } };
|
|
689
|
+
});
|
|
690
|
+
// -------------------------------------------------------------------------
|
|
691
|
+
// Routes — DEC-117. Last, because every ledger row is attributed to a MODULE
|
|
692
|
+
// that the pass above has already minted.
|
|
693
|
+
// -------------------------------------------------------------------------
|
|
694
|
+
const routeNodes = new Map();
|
|
695
|
+
const endpointNodes = new Map();
|
|
696
|
+
for (const unit of input.files) {
|
|
697
|
+
const moduleId = moduleIdOf.get(unit.file);
|
|
698
|
+
if (moduleId === undefined)
|
|
699
|
+
continue;
|
|
700
|
+
for (const refusal of unit.routeRefusals) {
|
|
701
|
+
ledger(moduleId, "SERVES_API", refusal.rawTarget, unit, refusal.line, refusal.reason);
|
|
702
|
+
}
|
|
703
|
+
// R2 and no lower. Telling a router from an application is provenance, and a
|
|
704
|
+
// route emitted from the R0 or R1 rung is a claim stronger than the run that
|
|
705
|
+
// produced it — which the Normaliser rejects as RESOLUTION_EXCEEDS_BATCH.
|
|
706
|
+
if (input.reached < 2)
|
|
707
|
+
continue;
|
|
708
|
+
for (const route of unit.routes) {
|
|
709
|
+
const template = normaliseEndpointPath(route.template);
|
|
710
|
+
const name = `${route.method} ${route.template}`;
|
|
711
|
+
// Repo-scoped and template-as-written: which service serves a path is
|
|
712
|
+
// exactly what the join must not erase.
|
|
713
|
+
const routeId = nodeId(scope, "API_ROUTE", name, LANGUAGE);
|
|
714
|
+
if (!routeNodes.has(routeId)) {
|
|
715
|
+
routeNodes.set(routeId, {
|
|
716
|
+
id: routeId,
|
|
717
|
+
type: "API_ROUTE",
|
|
718
|
+
name,
|
|
719
|
+
file: unit.file,
|
|
720
|
+
range: { startLine: route.line, endLine: route.line },
|
|
721
|
+
language: LANGUAGE,
|
|
722
|
+
producedBy: input.producedBy,
|
|
723
|
+
resolution: 2,
|
|
724
|
+
attrs: {
|
|
725
|
+
method: route.method,
|
|
726
|
+
pathTemplate: template,
|
|
727
|
+
rawTemplate: route.template,
|
|
728
|
+
written: route.written,
|
|
729
|
+
framework: route.framework,
|
|
730
|
+
},
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
// A second declaration claiming the same method+path — real (two
|
|
735
|
+
// routers mapped to overlapping prefixes) rather than a bug in this
|
|
736
|
+
// reader, but silent until now: nothing distinguished it from
|
|
737
|
+
// "checked, only one declaration exists". Same write pattern named
|
|
738
|
+
// across five language adapters in `language-problems.md` §0.2 #10,
|
|
739
|
+
// fixed here the way `adapter-python`/`adapter-java` already fixed
|
|
740
|
+
// it — a ledger row on the collision branch, not a change to the
|
|
741
|
+
// underlying node-identity question (`DEC-A-routes-ts-reconciliation`).
|
|
742
|
+
ledger(moduleId, "SERVES_API", name, unit, route.line, REASONS.routeIdCollision);
|
|
743
|
+
}
|
|
744
|
+
// Workspace-scoped, fileless, language-less, and minted with the SAME
|
|
745
|
+
// endpointQsp every other producer uses — DEC-115's one hard constraint.
|
|
746
|
+
// Two producers that mint different ids do not conflict, they silently
|
|
747
|
+
// fail to join.
|
|
748
|
+
const endpointId = nodeId(scope, "API_ENDPOINT", endpointQsp(route.method, route.template), null);
|
|
749
|
+
if (!endpointNodes.has(endpointId)) {
|
|
750
|
+
endpointNodes.set(endpointId, {
|
|
751
|
+
id: endpointId,
|
|
752
|
+
type: "API_ENDPOINT",
|
|
753
|
+
name: `${route.method} ${template}`,
|
|
754
|
+
file: null,
|
|
755
|
+
range: null,
|
|
756
|
+
language: null,
|
|
757
|
+
producedBy: input.producedBy,
|
|
758
|
+
resolution: 2,
|
|
759
|
+
attrs: { method: route.method, pathTemplate: template },
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
push({ from: routeId, to: endpointId, type: "SERVES_API" }, 2);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
// --- gRPC routes — see `grpc.ts`'s own header for the wire-path-is-read, not-
|
|
766
|
+
// invented rationale and the `method: "RPC"` sentinel. Cross-file by nature:
|
|
767
|
+
// a `_FullMethodName` const and the `Register<Service>Server(...)` call site
|
|
768
|
+
// that uses it are almost always in different files, so this pass runs after
|
|
769
|
+
// every unit's own consts have been collected, not per-unit like HTTP routes.
|
|
770
|
+
if (input.reached >= 2) {
|
|
771
|
+
const grpcMethodsByNamespace = new Map();
|
|
772
|
+
for (const unit of input.files) {
|
|
773
|
+
if (unit.grpcMethodConsts.length === 0)
|
|
774
|
+
continue;
|
|
775
|
+
const namespace = namespaceOf(unit);
|
|
776
|
+
let byService = grpcMethodsByNamespace.get(namespace);
|
|
777
|
+
if (byService === undefined) {
|
|
778
|
+
byService = new Map();
|
|
779
|
+
grpcMethodsByNamespace.set(namespace, byService);
|
|
780
|
+
}
|
|
781
|
+
for (const found of unit.grpcMethodConsts) {
|
|
782
|
+
const list = byService.get(found.service) ?? [];
|
|
783
|
+
list.push({ method: found.method, path: found.path });
|
|
784
|
+
byService.set(found.service, list);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
for (const unit of input.files) {
|
|
788
|
+
const moduleId = moduleIdOf.get(unit.file);
|
|
789
|
+
if (moduleId === undefined || unit.grpcRegistrations.length === 0)
|
|
790
|
+
continue;
|
|
791
|
+
for (const registration of unit.grpcRegistrations) {
|
|
792
|
+
const targetNamespace = registration.targetPackageAlias === undefined
|
|
793
|
+
? namespaceOf(unit)
|
|
794
|
+
: unit.imports.get(registration.targetPackageAlias);
|
|
795
|
+
// An unresolved package, or a naming coincidence with no corroborating
|
|
796
|
+
// const in the resolved package, is not evidence — stay silent, the
|
|
797
|
+
// same standard `readCall` in `routes.ts` holds a bare `x.Get(...)` to.
|
|
798
|
+
const methods = targetNamespace === undefined ? undefined : grpcMethodsByNamespace.get(targetNamespace)?.get(registration.service);
|
|
799
|
+
if (methods === undefined || methods.length === 0)
|
|
800
|
+
continue;
|
|
801
|
+
for (const method of methods) {
|
|
802
|
+
const template = normaliseEndpointPath(method.path);
|
|
803
|
+
const name = `RPC ${method.path}`;
|
|
804
|
+
const routeId = nodeId(scope, "API_ROUTE", name, LANGUAGE);
|
|
805
|
+
if (!routeNodes.has(routeId)) {
|
|
806
|
+
routeNodes.set(routeId, {
|
|
807
|
+
id: routeId,
|
|
808
|
+
type: "API_ROUTE",
|
|
809
|
+
name,
|
|
810
|
+
file: unit.file,
|
|
811
|
+
range: { startLine: registration.line, endLine: registration.line },
|
|
812
|
+
language: LANGUAGE,
|
|
813
|
+
producedBy: input.producedBy,
|
|
814
|
+
resolution: 2,
|
|
815
|
+
attrs: {
|
|
816
|
+
method: "RPC",
|
|
817
|
+
pathTemplate: template,
|
|
818
|
+
rawTemplate: method.path,
|
|
819
|
+
written: method.path,
|
|
820
|
+
framework: "grpc",
|
|
821
|
+
},
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
else {
|
|
825
|
+
// Same collision-disclosure fix as the HTTP route loop above —
|
|
826
|
+
// §0.2 #10.
|
|
827
|
+
ledger(moduleId, "SERVES_API", name, unit, registration.line, REASONS.routeIdCollision);
|
|
828
|
+
}
|
|
829
|
+
const endpointId = nodeId(scope, "API_ENDPOINT", endpointQsp("RPC", method.path), null);
|
|
830
|
+
if (!endpointNodes.has(endpointId)) {
|
|
831
|
+
endpointNodes.set(endpointId, {
|
|
832
|
+
id: endpointId,
|
|
833
|
+
type: "API_ENDPOINT",
|
|
834
|
+
name: `RPC ${template}`,
|
|
835
|
+
file: null,
|
|
836
|
+
range: null,
|
|
837
|
+
language: null,
|
|
838
|
+
producedBy: input.producedBy,
|
|
839
|
+
resolution: 2,
|
|
840
|
+
attrs: { method: "RPC", pathTemplate: template },
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
push({ from: routeId, to: endpointId, type: "SERVES_API" }, 2);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
// --- The caller's half -----------------------------------------------------
|
|
849
|
+
//
|
|
850
|
+
// Mints into the SAME `endpointNodes` map, so a call to a route this run also
|
|
851
|
+
// read produces one node with two edges rather than two nodes with one each.
|
|
852
|
+
// R2 and no lower, same reason as routes above: an edge emitted from a rung
|
|
853
|
+
// the run did not reach is a claim stronger than the run that produced it.
|
|
854
|
+
if (input.reached >= 2) {
|
|
855
|
+
for (const call of clientCallSites) {
|
|
856
|
+
const endpoint = endpointFor(scope, call, input.producedBy, normaliseEndpointPath);
|
|
857
|
+
if (!endpointNodes.has(endpoint.id))
|
|
858
|
+
endpointNodes.set(endpoint.id, endpoint);
|
|
859
|
+
push({
|
|
860
|
+
from: call.fromId,
|
|
861
|
+
to: endpoint.id,
|
|
862
|
+
type: "USES_API",
|
|
863
|
+
attrs: { callerKind: call.callerKind, via: call.method },
|
|
864
|
+
}, 2);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
const withRoutes = [...disclosed, ...routeNodes.values(), ...endpointNodes.values()];
|
|
868
|
+
return { nodes: withRoutes, edges, unresolved };
|
|
869
|
+
}
|
|
870
|
+
export { REASONS };
|
|
871
|
+
//# sourceMappingURL=extract.js.map
|