@groma/scanner-go 0.1.0 → 0.1.2

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.
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@groma/scanner-go",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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",
10
9
  "linux",
11
- "darwin"
10
+ "darwin",
11
+ "win32"
12
12
  ],
13
13
  "cpu": [
14
14
  "arm64",
@@ -35,10 +35,7 @@
35
35
  }
36
36
  ],
37
37
  "compatibility": {
38
- "groma": "^0.3.0",
39
- "technologyVersions": {
40
- "go": "1.20.0"
41
- }
38
+ "groma": "^0.3.0"
42
39
  }
43
40
  }
44
41
  }
package/src/index.js CHANGED
@@ -1,9 +1,152 @@
1
1
  // @bun
2
- // plugins/scanners/go/src/adapter.ts
2
+ // plugins/scanners/go/src/index.ts
3
+ import path5 from "path";
4
+
5
+ // plugins/scanners/projects.ts
3
6
  import { execFile } from "child_process";
4
- import { access, realpath } from "fs/promises";
7
+ import { existsSync } from "fs";
5
8
  import path from "path";
6
- import { fileURLToPath } from "url";
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
+ }
34
+
35
+ // plugins/scanners/project-scanner.ts
36
+ import path3 from "path";
37
+
38
+ // plugins/scanners/observations.ts
39
+ import path2 from "path";
40
+
41
+ // packages/scanner/src/values.ts
42
+ function object(value, label) {
43
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
44
+ throw new Error(`${label} must be an object`);
45
+ }
46
+ return value;
47
+ }
48
+ function string(value, label) {
49
+ if (typeof value !== "string")
50
+ throw new Error(`${label} must be a string`);
51
+ return value;
52
+ }
53
+ function array(value, label) {
54
+ if (!Array.isArray(value))
55
+ throw new Error(`${label} must be an array`);
56
+ return value;
57
+ }
58
+
59
+ // packages/scanner/src/http.ts
60
+ var pathText = /^[A-Za-z0-9\-._~!$&'()*+,;=:@%]+$/;
61
+ var methodToken = /^[A-Z][A-Z-]*$/;
62
+ function fail(message) {
63
+ throw new Error(message);
64
+ }
65
+ function text(value, label) {
66
+ if (typeof value !== "string" || !pathText.test(value))
67
+ fail(`${label} must be nonempty URL path characters`);
68
+ return value;
69
+ }
70
+ function method(value, label, anyMethod) {
71
+ if (typeof value !== "string" || !(methodToken.test(value) || anyMethod && value === "*")) {
72
+ fail(`${label} must be an uppercase HTTP method${anyMethod ? " or *" : ""}`);
73
+ }
74
+ return value;
75
+ }
76
+ function operation(value, operations) {
77
+ if (typeof value !== "string" || !operations.has(value))
78
+ fail("HTTP fact references unknown operation");
79
+ return value;
80
+ }
81
+ function endpointSegment(value, last) {
82
+ const segment = object(value, "HTTP endpoint segment");
83
+ if (segment.kind === "literal")
84
+ return { kind: "literal", value: text(segment.value, "HTTP literal segment") };
85
+ if (segment.kind !== "parameter" && segment.kind !== "catch-all")
86
+ fail(`unknown HTTP endpoint segment kind: ${String(segment.kind)}`);
87
+ if (segment.kind === "catch-all" && !last)
88
+ fail("an HTTP catch-all segment must be last");
89
+ for (const flag of ["optional", "constrained"]) {
90
+ if (segment[flag] !== undefined && typeof segment[flag] !== "boolean")
91
+ fail(`HTTP segment ${flag} must be a boolean`);
92
+ }
93
+ return {
94
+ kind: segment.kind,
95
+ name: text(segment.name, "HTTP segment name"),
96
+ ...segment.optional ? { optional: true } : {},
97
+ ...segment.constrained ? { constrained: true } : {}
98
+ };
99
+ }
100
+ function requestSegment(value) {
101
+ const segment = object(value, "HTTP request segment");
102
+ if (segment.kind === "literal")
103
+ return { kind: "literal", value: text(segment.value, "HTTP literal segment") };
104
+ if (segment.kind === "dynamic" || segment.kind === "unknown")
105
+ return { kind: segment.kind };
106
+ return fail(`unknown HTTP request segment kind: ${String(segment.kind)}`);
107
+ }
108
+ function order(value) {
109
+ const fact = object(value, "HTTP endpoint order");
110
+ const application = string(fact.application, "HTTP endpoint order application");
111
+ if (application === "")
112
+ fail("HTTP endpoint order application must be nonempty");
113
+ const position = fact.position;
114
+ if (typeof position !== "number" || !Number.isInteger(position) || position < 0) {
115
+ fail("HTTP endpoint order position must be a nonnegative integer");
116
+ }
117
+ return { application, position };
118
+ }
119
+ function endpoint(value, operations) {
120
+ const fact = object(value, "HTTP endpoint");
121
+ const path = array(fact.path, "HTTP endpoint path");
122
+ return {
123
+ operation: operation(fact.operation, operations),
124
+ method: method(fact.method, "HTTP endpoint method", true),
125
+ path: path.map((segment, index) => endpointSegment(segment, index === path.length - 1)),
126
+ ...fact.order === undefined ? {} : { order: order(fact.order) }
127
+ };
128
+ }
129
+ function request(value, operations) {
130
+ const fact = object(value, "HTTP request");
131
+ if (fact.configured !== undefined && typeof fact.configured !== "boolean")
132
+ fail("HTTP request configured must be a boolean");
133
+ return {
134
+ operation: operation(fact.operation, operations),
135
+ ...fact.method === undefined ? {} : { method: method(fact.method, "HTTP request method", false) },
136
+ ...fact.configured ? { configured: true } : {},
137
+ path: array(fact.path, "HTTP request path").map(requestSegment)
138
+ };
139
+ }
140
+ function httpEvidence(endpoints, requests, operations) {
141
+ if (endpoints === undefined && requests === undefined)
142
+ return {};
143
+ if (operations === undefined)
144
+ fail("HTTP facts require operation declarations");
145
+ return {
146
+ ...endpoints === undefined ? {} : { httpEndpoints: array(endpoints, "httpEndpoints").map((fact) => endpoint(fact, operations)) },
147
+ ...requests === undefined ? {} : { httpRequests: array(requests, "httpRequests").map((fact) => request(fact, operations)) }
148
+ };
149
+ }
7
150
 
8
151
  // packages/scanner/src/index.ts
9
152
  function compare(...values) {
@@ -48,7 +191,9 @@ function createScanObservation(input) {
48
191
  scanner: input.scanner,
49
192
  roots,
50
193
  files,
194
+ ...sourceUnits(input.sourceUnits, filePaths),
51
195
  ...operationEvidence(input, filePaths),
196
+ ...httpEvidence(input.httpEndpoints, input.httpRequests, input.operations && new Set(input.operations.map((operation) => operation.id))),
52
197
  diagnostics: [...new Map(input.diagnostics.map((diagnostic) => [
53
198
  diagnosticKey(diagnostic),
54
199
  diagnostic
@@ -57,6 +202,23 @@ function createScanObservation(input) {
57
202
  })
58
203
  };
59
204
  }
205
+ function sourceUnits(input, paths) {
206
+ if (input === undefined)
207
+ return {};
208
+ const units = array(input, "sourceUnits").map((entry) => {
209
+ const unit = object(entry, "source unit");
210
+ const primary = string(unit.primary, "source unit.primary");
211
+ const files = [...new Set(array(unit.files, "source unit.files").map((file) => string(file, "source unit.file")))].sort();
212
+ if (!files.includes(primary))
213
+ throw new Error(`source unit omits primary file: ${primary}`);
214
+ for (const file of files) {
215
+ if (!paths.has(file))
216
+ throw new Error(`source unit references unknown file: ${file}`);
217
+ }
218
+ return { primary, files };
219
+ });
220
+ return { sourceUnits: [...new Map(units.map((unit) => [compare(unit.primary, ...unit.files), unit])).values()].sort((a, b) => compare(a.primary, ...a.files).localeCompare(compare(b.primary, ...b.files))) };
221
+ }
60
222
  function validateRoots(input) {
61
223
  const roots = uniqueBy(input, (root) => root.id, "root id");
62
224
  const byId = new Map(roots.map((root) => [root.id, root]));
@@ -87,22 +249,6 @@ function diagnosticLocation(value) {
87
249
  ...value.line === undefined ? {} : { line: Number(value.line) }
88
250
  };
89
251
  }
90
- function object(value, label) {
91
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
92
- throw new Error(`${label} must be an object`);
93
- }
94
- return value;
95
- }
96
- function string(value, label) {
97
- if (typeof value !== "string")
98
- throw new Error(`${label} must be a string`);
99
- return value;
100
- }
101
- function array(value, label) {
102
- if (!Array.isArray(value))
103
- throw new Error(`${label} must be an array`);
104
- return value;
105
- }
106
252
  function parseScanObservation(source) {
107
253
  const value = object(JSON.parse(source), "observation");
108
254
  if (value.schemaVersion !== 1)
@@ -116,6 +262,9 @@ function parseScanObservation(source) {
116
262
  engineVersion: string(scanner.engineVersion, "scanner.engineVersion")
117
263
  },
118
264
  ...parseOperations(value),
265
+ httpEndpoints: value.httpEndpoints,
266
+ httpRequests: value.httpRequests,
267
+ ...sourceUnits(value.sourceUnits, new Set(array(value.files, "files").map((entry) => string(object(entry, "file").file, "file.file")))),
119
268
  roots: array(value.roots, "roots").map((entry) => {
120
269
  const root = object(entry, "root");
121
270
  return {
@@ -259,11 +408,97 @@ function sourcePosition(position) {
259
408
  return { position: Number(position) };
260
409
  }
261
410
 
411
+ // plugins/scanners/observations.ts
412
+ function relocateObservation(observation, directory) {
413
+ const file = (value) => path2.posix.normalize(path2.posix.join(directory, value));
414
+ return {
415
+ ...observation,
416
+ roots: observation.roots.map((root) => ({ ...root, ...root.file ? { file: file(root.file) } : {} })),
417
+ files: observation.files.map((source) => ({ ...source, file: file(source.file) })),
418
+ ...observation.sourceUnits === undefined ? {} : {
419
+ sourceUnits: observation.sourceUnits.map((unit) => ({ primary: file(unit.primary), files: unit.files.map(file) }))
420
+ },
421
+ operations: observation.operations?.map((operation) => ({ ...operation, file: file(operation.file) })),
422
+ invocations: observation.invocations?.map((call) => ({
423
+ ...call,
424
+ ...call.binding ? { binding: { ...call.binding, file: file(call.binding.file) } } : {}
425
+ })),
426
+ diagnostics: observation.diagnostics.map((item) => ({ ...item, ...item.file ? { file: file(item.file) } : {} }))
427
+ };
428
+ }
429
+ function combineObservations(parts) {
430
+ if (!parts.length)
431
+ return;
432
+ const roots = [];
433
+ const files = new Map;
434
+ const operations = [];
435
+ const invocations = [];
436
+ const diagnostics = [];
437
+ const sourceUnits = [];
438
+ const httpEndpoints = [];
439
+ const httpRequests = [];
440
+ for (const { key, observation } of parts) {
441
+ const id = (value) => JSON.stringify([key, value]);
442
+ roots.push(...observation.roots.map((root) => ({ ...root, id: id(root.id), ...root.parent ? { parent: id(root.parent) } : {} })));
443
+ for (const source of observation.files) {
444
+ const combined = files.get(source.file) ?? { file: source.file, roots: [], symbols: [] };
445
+ combined.roots.push(...source.roots.map(id));
446
+ combined.symbols.push(...source.symbols.map((symbol) => ({ ...symbol, id: id(symbol.id) })));
447
+ files.set(source.file, combined);
448
+ }
449
+ operations.push(...(observation.operations ?? []).map((operation) => ({ ...operation, id: id(operation.id) })));
450
+ invocations.push(...(observation.invocations ?? []).map((call) => ({ ...call, source: id(call.source), targets: call.targets.map(id) })));
451
+ httpEndpoints.push(...(observation.httpEndpoints ?? []).map((endpoint) => ({ ...endpoint, operation: id(endpoint.operation) })));
452
+ httpRequests.push(...(observation.httpRequests ?? []).map((request) => ({ ...request, operation: id(request.operation) })));
453
+ diagnostics.push(...observation.diagnostics);
454
+ sourceUnits.push(...observation.sourceUnits ?? []);
455
+ }
456
+ return createScanObservation({
457
+ scanner: parts[0].observation.scanner,
458
+ roots,
459
+ files: [...files.values()],
460
+ operations,
461
+ invocations,
462
+ diagnostics,
463
+ ...parts.some((part) => part.observation.sourceUnits !== undefined) ? { sourceUnits } : {},
464
+ ...parts.some((part) => part.observation.httpEndpoints !== undefined) ? { httpEndpoints } : {},
465
+ ...parts.some((part) => part.observation.httpRequests !== undefined) ? { httpRequests } : {}
466
+ });
467
+ }
468
+
469
+ // plugins/scanners/project-scanner.ts
470
+ function projectScanner(scanner, select) {
471
+ return {
472
+ ...scanner,
473
+ checkReadiness: async (root, settings = {}) => {
474
+ const projects = await select(root, settings);
475
+ if (!projects.length)
476
+ throw new Error(`${scanner.id}: No supported project declaration was found.`);
477
+ for (const project of projects)
478
+ await scanner.checkReadiness?.(project, settings);
479
+ },
480
+ scan: async (root, settings = {}) => {
481
+ const parts = [];
482
+ for (const project of await select(root, settings)) {
483
+ const observation = await scanner.scan(project, settings);
484
+ const key = path3.relative(root, project).split(path3.sep).join("/");
485
+ if (observation)
486
+ parts.push({ key, observation: relocateObservation(observation, key) });
487
+ }
488
+ return combineObservations(parts);
489
+ }
490
+ };
491
+ }
492
+
262
493
  // plugins/scanners/go/src/adapter.ts
494
+ import { execFile as execFile2 } from "child_process";
495
+ import { access, realpath } from "fs/promises";
496
+ import path4 from "path";
497
+ import { fileURLToPath } from "url";
263
498
  var packagedWorker = fileURLToPath(new URL(`../dist/${process.platform}-${process.arch}/worker${process.platform === "win32" ? ".exe" : ""}`, import.meta.url));
264
- function run(command, args, root, env = process.env) {
499
+ function run(command, args, root, { env = process.env, input } = {}) {
265
500
  return new Promise((resolve, reject) => {
266
- execFile(command, args, {
501
+ const child = execFile2(command, args, {
267
502
  cwd: root,
268
503
  env,
269
504
  encoding: "utf8",
@@ -276,55 +511,54 @@ function run(command, args, root, env = process.env) {
276
511
  else
277
512
  resolve(stdout);
278
513
  });
514
+ if (input !== undefined)
515
+ child.stdin?.end(input);
279
516
  });
280
517
  }
281
- async function checkGoReadiness(repositoryRoot, options = {}) {
282
- const root = await realpath(repositoryRoot);
518
+ async function workerPath(options) {
283
519
  const worker = options.worker ?? packagedWorker;
284
520
  try {
285
521
  await access(worker);
286
522
  } catch {
287
523
  throw new Error("GO_WORKER_MISSING: Install the packaged Go scanner for this platform, or build it with bun plugins/scanners/go/build.ts.");
288
524
  }
289
- const env = { ...process.env, GOTOOLCHAIN: "local", GOPROXY: "off", GOSUMDB: "off" };
290
- let context;
291
- try {
292
- context = JSON.parse(await run(options.go ?? "go", ["env", "-json", "GOMOD", "GOWORK", "GOROOT"], root, env));
293
- } catch (error) {
294
- throw new Error(`GO_TOOLCHAIN_MISSING: Install the project's Go toolchain and add its bin directory to PATH (Go 1.27.1 for the qualified example). ${error}`);
295
- }
296
- if (context.GOWORK && context.GOWORK !== "off") {
297
- throw new Error("GO_PROJECT_SCOPE: This scanner supports one root module. Select the supported module with GOWORK=off; workspace analysis is not qualified.");
298
- }
299
- if (context.GOMOD !== path.join(root, "go.mod")) {
300
- throw new Error("GO_PROJECT_SCOPE: Run Groma at the supported Go module root containing go.mod.");
301
- }
302
- env.PATH = `${path.join(context.GOROOT, "bin")}${path.delimiter}${process.env.PATH ?? ""}`;
303
- try {
304
- await run(options.go ?? "go", ["list", "-mod=readonly", "-deps", "./..."], root, env);
305
- } catch (error) {
306
- throw new Error(`GO_PROJECT_PREPARATION: Prepare dependencies and generated source with the project's documented build commands (go mod download for module dependencies), then retry. Scan does not download tools or modules. ${error}`);
307
- }
308
- return { root, worker, env };
525
+ return worker;
526
+ }
527
+ async function checkGoReadiness(repositoryRoot, options = {}) {
528
+ const root = await realpath(repositoryRoot);
529
+ const worker = await workerPath(options);
530
+ await access(path4.join(root, "go.mod"));
531
+ return { root, worker };
532
+ }
533
+ async function readGoCodeStructure(repositoryRoot, references, options = {}) {
534
+ const worker = await workerPath(options);
535
+ return JSON.parse(await run(worker, ["outline", repositoryRoot], repositoryRoot, { input: JSON.stringify(references) }));
309
536
  }
310
537
  async function scanGoSource(repositoryRoot, options = {}) {
311
- const { root, worker, env } = await checkGoReadiness(repositoryRoot, options);
538
+ const { root, worker } = await checkGoReadiness(repositoryRoot, options);
312
539
  try {
313
- return parseScanObservation(await run(worker, [root], root, env));
540
+ return parseScanObservation(await run(worker, [root], root));
314
541
  } catch (error) {
315
- throw new Error(`GO_COMPILATION_FAILED: No observation was produced. Correct compilation errors with the project's Go toolchain and prepare its dependencies and generated source. ${error}`);
542
+ throw new Error(`GO_SOURCE_INVALID: No observation was produced. Check Go source syntax and module declarations. ${error}`);
316
543
  }
317
544
  }
318
545
 
319
546
  // plugins/scanners/go/src/index.ts
320
- var src_default = {
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
+ var scanner = {
321
552
  id: "go",
322
553
  watch: { include: ["**/*.go", "**/go.mod", "**/go.sum", "**/go.work"], exclude: [] },
554
+ listSourceFiles: goSources,
323
555
  checkReadiness: async (root) => {
324
556
  await checkGoReadiness(root);
325
557
  },
558
+ readCodeStructure: readGoCodeStructure,
326
559
  scan: scanGoSource
327
560
  };
561
+ var src_default = projectScanner(scanner, async (root) => (await projectFiles(root, (file) => path5.posix.basename(file) === "go.mod")).map((file) => path5.dirname(path5.join(root, file))));
328
562
  export {
329
563
  src_default as default
330
564
  };