@groma/scanner-go 0.1.3 → 0.2.1
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/darwin-arm64/worker +0 -0
- package/dist/linux-arm64/worker +0 -0
- package/dist/linux-x64/worker +0 -0
- package/dist/win32-arm64/worker.exe +0 -0
- package/dist/win32-x64/worker.exe +0 -0
- package/package.json +14 -7
- package/src/index.js +108 -73
package/dist/darwin-arm64/worker
CHANGED
|
Binary file
|
package/dist/linux-arm64/worker
CHANGED
|
Binary file
|
package/dist/linux-x64/worker
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,23 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@groma/scanner-go",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Go project evidence through Go language tooling.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"os": [
|
|
9
|
+
"win32",
|
|
9
10
|
"linux",
|
|
10
|
-
"darwin"
|
|
11
|
-
"win32"
|
|
11
|
+
"darwin"
|
|
12
12
|
],
|
|
13
13
|
"cpu": [
|
|
14
|
-
"
|
|
15
|
-
"
|
|
14
|
+
"x64",
|
|
15
|
+
"arm64"
|
|
16
16
|
],
|
|
17
17
|
"groma": {
|
|
18
18
|
"scanner": {
|
|
19
19
|
"id": "go",
|
|
20
20
|
"entry": "./src/index.js",
|
|
21
|
+
"include": [
|
|
22
|
+
"**/*.go",
|
|
23
|
+
"**/go.mod"
|
|
24
|
+
],
|
|
25
|
+
"exclude": [
|
|
26
|
+
"vendor/"
|
|
27
|
+
],
|
|
21
28
|
"discovery": {
|
|
22
29
|
"technologies": [
|
|
23
30
|
"go"
|
|
@@ -35,14 +42,14 @@
|
|
|
35
42
|
}
|
|
36
43
|
],
|
|
37
44
|
"compatibility": {
|
|
38
|
-
"groma": ">=0.
|
|
45
|
+
"groma": ">=0.5.0"
|
|
39
46
|
}
|
|
40
47
|
}
|
|
41
48
|
}
|
|
42
49
|
},
|
|
43
50
|
"repository": {
|
|
44
51
|
"type": "git",
|
|
45
|
-
"url": "https://github.com/MrLesk/
|
|
52
|
+
"url": "https://github.com/MrLesk/groma.md.git"
|
|
46
53
|
},
|
|
47
54
|
"publishConfig": {
|
|
48
55
|
"access": "public"
|
package/src/index.js
CHANGED
|
@@ -1,42 +1,12 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// plugins/scanners/go/src/index.ts
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
// plugins/scanners/projects.ts
|
|
6
|
-
import { execFile } from "child_process";
|
|
7
|
-
import { existsSync } from "fs";
|
|
8
|
-
import path from "path";
|
|
9
|
-
import { promisify } from "util";
|
|
10
|
-
var execute = promisify(execFile);
|
|
11
|
-
var excluded = new Set([
|
|
12
|
-
".git",
|
|
13
|
-
"node_modules",
|
|
14
|
-
"vendor",
|
|
15
|
-
"target",
|
|
16
|
-
"dist",
|
|
17
|
-
"build",
|
|
18
|
-
"bin",
|
|
19
|
-
"obj",
|
|
20
|
-
".gradle",
|
|
21
|
-
".angular",
|
|
22
|
-
"coverage",
|
|
23
|
-
"generated",
|
|
24
|
-
"groma",
|
|
25
|
-
".groma"
|
|
26
|
-
]);
|
|
27
|
-
async function repositoryFiles(root, matches) {
|
|
28
|
-
const { stdout } = await execute("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { maxBuffer: 64 * 1024 * 1024 });
|
|
29
|
-
return [...new Set(stdout.split("\x00").filter((file) => file && matches(file) && existsSync(path.join(root, file))))].sort();
|
|
30
|
-
}
|
|
31
|
-
function projectFiles(root, matches) {
|
|
32
|
-
return repositoryFiles(root, (file) => matches(file) && !file.split("/").some((part) => excluded.has(part)));
|
|
33
|
-
}
|
|
3
|
+
import path4 from "path";
|
|
34
4
|
|
|
35
5
|
// plugins/scanners/project-scanner.ts
|
|
36
|
-
import
|
|
6
|
+
import path2 from "path";
|
|
37
7
|
|
|
38
8
|
// plugins/scanners/observations.ts
|
|
39
|
-
import
|
|
9
|
+
import path from "path";
|
|
40
10
|
|
|
41
11
|
// packages/scanner/src/values.ts
|
|
42
12
|
function object(value, label) {
|
|
@@ -154,13 +124,13 @@ function compare(...values) {
|
|
|
154
124
|
}
|
|
155
125
|
function uniqueBy(values, key, label) {
|
|
156
126
|
const seen = new Set;
|
|
157
|
-
|
|
158
|
-
|
|
127
|
+
const keyed = values.map((value) => ({ id: key(value), value }));
|
|
128
|
+
for (const { id } of keyed) {
|
|
159
129
|
if (seen.has(id))
|
|
160
130
|
throw new Error(`duplicate ${label}: ${id}`);
|
|
161
131
|
seen.add(id);
|
|
162
132
|
}
|
|
163
|
-
return
|
|
133
|
+
return keyed.sort((left, right) => left.id.localeCompare(right.id)).map((entry) => entry.value);
|
|
164
134
|
}
|
|
165
135
|
function createScanObservation(input) {
|
|
166
136
|
const roots = validateRoots(input.roots);
|
|
@@ -171,9 +141,7 @@ function createScanObservation(input) {
|
|
|
171
141
|
symbols: [...new Map(file.symbols.map((symbol) => [
|
|
172
142
|
compare(symbol.id, symbol.kind),
|
|
173
143
|
symbol
|
|
174
|
-
]))
|
|
175
|
-
return compare(left.id, left.kind).localeCompare(compare(right.id, right.kind));
|
|
176
|
-
})
|
|
144
|
+
]))].sort(([left], [right]) => left.localeCompare(right)).map(([, symbol]) => symbol)
|
|
177
145
|
})), (file) => file.file, "file path");
|
|
178
146
|
const filePaths = new Set(files.map((file) => file.file));
|
|
179
147
|
for (const file of files) {
|
|
@@ -184,24 +152,46 @@ function createScanObservation(input) {
|
|
|
184
152
|
throw new Error(`file references unknown root: ${root}`);
|
|
185
153
|
}
|
|
186
154
|
}
|
|
187
|
-
for (const diagnostic of input.diagnostics)
|
|
188
|
-
diagnosticLocation(diagnostic);
|
|
189
155
|
return {
|
|
190
156
|
schemaVersion: 1,
|
|
191
157
|
scanner: input.scanner,
|
|
192
158
|
roots,
|
|
193
159
|
files,
|
|
194
160
|
...sourceUnits(input.sourceUnits, filePaths),
|
|
161
|
+
...entryPoints(input.entryPoints, filePaths),
|
|
195
162
|
...operationEvidence(input, filePaths),
|
|
196
163
|
...httpEvidence(input.httpEndpoints, input.httpRequests, input.operations && new Set(input.operations.map((operation) => operation.id))),
|
|
197
|
-
diagnostics:
|
|
198
|
-
diagnosticKey(diagnostic),
|
|
199
|
-
diagnostic
|
|
200
|
-
])).values()].sort((left, right) => {
|
|
201
|
-
return diagnosticKey(left).localeCompare(diagnosticKey(right));
|
|
202
|
-
})
|
|
164
|
+
diagnostics: normalizeScanDiagnostics(input.diagnostics)
|
|
203
165
|
};
|
|
204
166
|
}
|
|
167
|
+
function normalizeScanDiagnostics(diagnostics) {
|
|
168
|
+
for (const diagnostic of diagnostics)
|
|
169
|
+
diagnosticLocation(diagnostic);
|
|
170
|
+
return [...new Map(diagnostics.map((diagnostic) => [diagnosticKey(diagnostic), diagnostic]))].sort(([left], [right]) => left.localeCompare(right)).map(([, diagnostic]) => diagnostic);
|
|
171
|
+
}
|
|
172
|
+
function entryPoints(input, paths) {
|
|
173
|
+
if (input === undefined)
|
|
174
|
+
return {};
|
|
175
|
+
const entries = array(input, "entryPoints").map((value) => {
|
|
176
|
+
const entry = object(value, "entry point");
|
|
177
|
+
const files = [...new Set(array(entry.files, "entry point.files").map((file) => string(file, "entry point.file")))].sort();
|
|
178
|
+
for (const file of files) {
|
|
179
|
+
if (!paths.has(file))
|
|
180
|
+
throw new Error(`entry point references unknown file: ${file}`);
|
|
181
|
+
}
|
|
182
|
+
const file = string(entry.file, "entry point.file");
|
|
183
|
+
if (!files.includes(file))
|
|
184
|
+
throw new Error(`entry point must include its own source file: ${file}`);
|
|
185
|
+
return {
|
|
186
|
+
file,
|
|
187
|
+
declaration: string(entry.declaration, "entry point.declaration"),
|
|
188
|
+
name: string(entry.name, "entry point.name"),
|
|
189
|
+
files
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
const key = (entry) => compare(entry.file, entry.declaration, entry.name, ...entry.files);
|
|
193
|
+
return { entryPoints: [...new Map(entries.map((entry) => [key(entry), entry])).values()].sort((a, b) => key(a).localeCompare(key(b))) };
|
|
194
|
+
}
|
|
205
195
|
function sourceUnits(input, paths) {
|
|
206
196
|
if (input === undefined)
|
|
207
197
|
return {};
|
|
@@ -264,6 +254,7 @@ function parseScanObservation(source) {
|
|
|
264
254
|
...parseOperations(value),
|
|
265
255
|
httpEndpoints: value.httpEndpoints,
|
|
266
256
|
httpRequests: value.httpRequests,
|
|
257
|
+
entryPoints: value.entryPoints,
|
|
267
258
|
...sourceUnits(value.sourceUnits, new Set(array(value.files, "files").map((entry) => string(object(entry, "file").file, "file.file")))),
|
|
268
259
|
roots: array(value.roots, "roots").map((entry) => {
|
|
269
260
|
const root = object(entry, "root");
|
|
@@ -376,10 +367,14 @@ function operationTokens(operation) {
|
|
|
376
367
|
const endLine = Number(operation.endLine);
|
|
377
368
|
if (endLine < startLine)
|
|
378
369
|
throw new Error("operation.endLine must be at or after startLine");
|
|
370
|
+
const tokens = array(operation.tokens, "operation.tokens");
|
|
371
|
+
tokens.forEach((token) => {
|
|
372
|
+
string(token, "operation.token");
|
|
373
|
+
});
|
|
379
374
|
return {
|
|
380
375
|
startLine,
|
|
381
376
|
endLine,
|
|
382
|
-
tokens
|
|
377
|
+
tokens
|
|
383
378
|
};
|
|
384
379
|
}
|
|
385
380
|
function validateOperationTokens(operation) {
|
|
@@ -410,7 +405,7 @@ function sourcePosition(position) {
|
|
|
410
405
|
|
|
411
406
|
// plugins/scanners/observations.ts
|
|
412
407
|
function relocateObservation(observation, directory) {
|
|
413
|
-
const file = (value) =>
|
|
408
|
+
const file = (value) => path.posix.normalize(path.posix.join(directory, value));
|
|
414
409
|
return {
|
|
415
410
|
...observation,
|
|
416
411
|
roots: observation.roots.map((root) => ({ ...root, ...root.file ? { file: file(root.file) } : {} })),
|
|
@@ -418,11 +413,20 @@ function relocateObservation(observation, directory) {
|
|
|
418
413
|
...observation.sourceUnits === undefined ? {} : {
|
|
419
414
|
sourceUnits: observation.sourceUnits.map((unit) => ({ primary: file(unit.primary), files: unit.files.map(file) }))
|
|
420
415
|
},
|
|
416
|
+
...observation.entryPoints === undefined ? {} : {
|
|
417
|
+
entryPoints: observation.entryPoints.map((entry) => ({
|
|
418
|
+
...entry,
|
|
419
|
+
file: file(entry.file),
|
|
420
|
+
declaration: file(entry.declaration),
|
|
421
|
+
files: entry.files.map(file)
|
|
422
|
+
}))
|
|
423
|
+
},
|
|
421
424
|
operations: observation.operations?.map((operation) => ({ ...operation, file: file(operation.file) })),
|
|
422
425
|
invocations: observation.invocations?.map((call) => ({
|
|
423
426
|
...call,
|
|
424
427
|
...call.binding ? { binding: { ...call.binding, file: file(call.binding.file) } } : {}
|
|
425
428
|
})),
|
|
429
|
+
httpEndpoints: observation.httpEndpoints?.map((endpoint) => endpoint.order === undefined ? endpoint : { ...endpoint, order: { ...endpoint.order, application: file(endpoint.order.application) } }),
|
|
426
430
|
diagnostics: observation.diagnostics.map((item) => ({ ...item, ...item.file ? { file: file(item.file) } : {} }))
|
|
427
431
|
};
|
|
428
432
|
}
|
|
@@ -435,6 +439,7 @@ function combineObservations(parts) {
|
|
|
435
439
|
const invocations = [];
|
|
436
440
|
const diagnostics = [];
|
|
437
441
|
const sourceUnits = [];
|
|
442
|
+
const entryPoints = [];
|
|
438
443
|
const httpEndpoints = [];
|
|
439
444
|
const httpRequests = [];
|
|
440
445
|
for (const { key, observation } of parts) {
|
|
@@ -452,6 +457,7 @@ function combineObservations(parts) {
|
|
|
452
457
|
httpRequests.push(...(observation.httpRequests ?? []).map((request) => ({ ...request, operation: id(request.operation) })));
|
|
453
458
|
diagnostics.push(...observation.diagnostics);
|
|
454
459
|
sourceUnits.push(...observation.sourceUnits ?? []);
|
|
460
|
+
entryPoints.push(...observation.entryPoints ?? []);
|
|
455
461
|
}
|
|
456
462
|
return createScanObservation({
|
|
457
463
|
scanner: parts[0].observation.scanner,
|
|
@@ -461,29 +467,36 @@ function combineObservations(parts) {
|
|
|
461
467
|
invocations,
|
|
462
468
|
diagnostics,
|
|
463
469
|
...parts.some((part) => part.observation.sourceUnits !== undefined) ? { sourceUnits } : {},
|
|
470
|
+
...parts.some((part) => part.observation.entryPoints !== undefined) ? { entryPoints } : {},
|
|
464
471
|
...parts.some((part) => part.observation.httpEndpoints !== undefined) ? { httpEndpoints } : {},
|
|
465
472
|
...parts.some((part) => part.observation.httpRequests !== undefined) ? { httpRequests } : {}
|
|
466
473
|
});
|
|
467
474
|
}
|
|
468
475
|
|
|
469
476
|
// plugins/scanners/project-scanner.ts
|
|
477
|
+
function within(root, project, files) {
|
|
478
|
+
const key = path2.relative(root, project).split(path2.sep).join("/");
|
|
479
|
+
if (key === "")
|
|
480
|
+
return { key, files: [...files] };
|
|
481
|
+
return { key, files: files.flatMap((file) => file.startsWith(`${key}/`) ? [file.slice(key.length + 1)] : []) };
|
|
482
|
+
}
|
|
470
483
|
function projectScanner(scanner, select) {
|
|
471
484
|
return {
|
|
472
485
|
...scanner,
|
|
473
|
-
checkReadiness: async (root, settings
|
|
474
|
-
const projects = await select(root, settings);
|
|
486
|
+
checkReadiness: async (root, settings, files) => {
|
|
487
|
+
const projects = await select(root, settings, files);
|
|
475
488
|
if (!projects.length)
|
|
476
489
|
throw new Error(`${scanner.id}: No supported project declaration was found.`);
|
|
477
490
|
for (const project of projects)
|
|
478
|
-
await scanner.checkReadiness?.(project, settings);
|
|
491
|
+
await scanner.checkReadiness?.(project, settings, within(root, project, files).files);
|
|
479
492
|
},
|
|
480
|
-
scan: async (root, settings
|
|
493
|
+
scan: async (root, settings, files) => {
|
|
481
494
|
const parts = [];
|
|
482
|
-
for (const project of await select(root, settings)) {
|
|
483
|
-
const
|
|
484
|
-
const
|
|
495
|
+
for (const project of await select(root, settings, files)) {
|
|
496
|
+
const local = within(root, project, files);
|
|
497
|
+
const observation = await scanner.scan(project, settings, local.files);
|
|
485
498
|
if (observation)
|
|
486
|
-
parts.push({ key, observation: relocateObservation(observation, key) });
|
|
499
|
+
parts.push({ key: local.key, observation: relocateObservation(observation, local.key) });
|
|
487
500
|
}
|
|
488
501
|
return combineObservations(parts);
|
|
489
502
|
}
|
|
@@ -491,14 +504,42 @@ function projectScanner(scanner, select) {
|
|
|
491
504
|
}
|
|
492
505
|
|
|
493
506
|
// plugins/scanners/go/src/adapter.ts
|
|
494
|
-
import { execFile
|
|
507
|
+
import { execFile } from "child_process";
|
|
495
508
|
import { access, realpath } from "fs/promises";
|
|
496
|
-
import path4 from "path";
|
|
497
509
|
import { fileURLToPath } from "url";
|
|
510
|
+
|
|
511
|
+
// plugins/scanners/go/src/sources.ts
|
|
512
|
+
import path3 from "path";
|
|
513
|
+
function ignoredByGo(file) {
|
|
514
|
+
return file.endsWith("_test.go") || file.split("/").some((part) => part === "testdata" || part.startsWith(".") || part.startsWith("_"));
|
|
515
|
+
}
|
|
516
|
+
function goFiles(files) {
|
|
517
|
+
const read = files.filter((file) => !ignoredByGo(file));
|
|
518
|
+
return {
|
|
519
|
+
modules: read.filter((file) => path3.posix.basename(file) === "go.mod").map((file) => path3.posix.dirname(file)),
|
|
520
|
+
sources: read.filter((file) => file.endsWith(".go"))
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
function moduleOf(file, modules) {
|
|
524
|
+
return modules.filter((module) => module === "." || file.startsWith(`${module}/`)).sort((left, right) => right.length - left.length)[0];
|
|
525
|
+
}
|
|
526
|
+
function goModules(files) {
|
|
527
|
+
return goFiles(files).modules;
|
|
528
|
+
}
|
|
529
|
+
function goSources(candidates) {
|
|
530
|
+
const { modules, sources } = goFiles(candidates);
|
|
531
|
+
return sources.filter((file) => moduleOf(file, modules) !== undefined);
|
|
532
|
+
}
|
|
533
|
+
function moduleSources(files) {
|
|
534
|
+
const { modules, sources } = goFiles(files);
|
|
535
|
+
return sources.filter((file) => moduleOf(file, modules) === ".");
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// plugins/scanners/go/src/adapter.ts
|
|
498
539
|
var packagedWorker = fileURLToPath(new URL(`../dist/${process.platform}-${process.arch}/worker${process.platform === "win32" ? ".exe" : ""}`, import.meta.url));
|
|
499
540
|
function run(command, args, root, { env = process.env, input } = {}) {
|
|
500
541
|
return new Promise((resolve, reject) => {
|
|
501
|
-
const child =
|
|
542
|
+
const child = execFile(command, args, {
|
|
502
543
|
cwd: root,
|
|
503
544
|
env,
|
|
504
545
|
encoding: "utf8",
|
|
@@ -524,41 +565,35 @@ async function workerPath(options) {
|
|
|
524
565
|
}
|
|
525
566
|
return worker;
|
|
526
567
|
}
|
|
527
|
-
async function checkGoReadiness(
|
|
528
|
-
const root = await realpath(
|
|
568
|
+
async function checkGoReadiness(moduleRoot, options = {}) {
|
|
569
|
+
const root = await realpath(moduleRoot);
|
|
529
570
|
const worker = await workerPath(options);
|
|
530
|
-
await access(path4.join(root, "go.mod"));
|
|
531
571
|
return { root, worker };
|
|
532
572
|
}
|
|
533
573
|
async function readGoCodeStructure(repositoryRoot, references, options = {}) {
|
|
534
574
|
const worker = await workerPath(options);
|
|
535
575
|
return JSON.parse(await run(worker, ["outline", repositoryRoot], repositoryRoot, { input: JSON.stringify(references) }));
|
|
536
576
|
}
|
|
537
|
-
async function scanGoSource(
|
|
538
|
-
const { root, worker } = await checkGoReadiness(
|
|
577
|
+
async function scanGoSource(moduleRoot, options, files) {
|
|
578
|
+
const { root, worker } = await checkGoReadiness(moduleRoot, options);
|
|
539
579
|
try {
|
|
540
|
-
return parseScanObservation(await run(worker, [root], root));
|
|
580
|
+
return parseScanObservation(await run(worker, [root], root, { input: JSON.stringify(moduleSources(files)) }));
|
|
541
581
|
} catch (error) {
|
|
542
582
|
throw new Error(`GO_SOURCE_INVALID: No observation was produced. Check Go source syntax and module declarations. ${error}`);
|
|
543
583
|
}
|
|
544
584
|
}
|
|
545
585
|
|
|
546
586
|
// plugins/scanners/go/src/index.ts
|
|
547
|
-
async function goSources(root) {
|
|
548
|
-
const modules = (await projectFiles(root, (file) => path5.posix.basename(file) === "go.mod")).map((file) => path5.posix.dirname(file));
|
|
549
|
-
return projectFiles(root, (file) => file.endsWith(".go") && !file.endsWith("_test.go") && !file.split("/").some((part) => part === "testdata" || part.startsWith(".")) && modules.some((module) => module === "." || file.startsWith(`${module}/`)));
|
|
550
|
-
}
|
|
551
587
|
var scanner = {
|
|
552
588
|
id: "go",
|
|
553
|
-
|
|
554
|
-
listSourceFiles: goSources,
|
|
589
|
+
listSourceFiles: async (_root, _settings, candidates) => goSources(candidates),
|
|
555
590
|
checkReadiness: async (root) => {
|
|
556
591
|
await checkGoReadiness(root);
|
|
557
592
|
},
|
|
558
593
|
readCodeStructure: readGoCodeStructure,
|
|
559
594
|
scan: scanGoSource
|
|
560
595
|
};
|
|
561
|
-
var src_default = projectScanner(scanner, async (root
|
|
596
|
+
var src_default = projectScanner(scanner, async (root, _settings, files) => goModules(files).map((module) => path4.join(root, module)));
|
|
562
597
|
export {
|
|
563
598
|
src_default as default
|
|
564
599
|
};
|