@groma/scanner-go 0.1.1 → 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,6 +1,6 @@
1
1
  {
2
2
  "name": "@groma/scanner-go",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Go project evidence through Go language tooling.",
5
5
  "private": false,
6
6
  "type": "module",
package/src/index.js CHANGED
@@ -24,9 +24,12 @@ var excluded = new Set([
24
24
  "groma",
25
25
  ".groma"
26
26
  ]);
27
- async function projectFiles(root, matches) {
27
+ async function repositoryFiles(root, matches) {
28
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) && !file.split("/").some((part) => excluded.has(part)) && existsSync(path.join(root, file))))].sort();
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)));
30
33
  }
31
34
 
32
35
  // plugins/scanners/project-scanner.ts
@@ -35,6 +38,116 @@ import path3 from "path";
35
38
  // plugins/scanners/observations.ts
36
39
  import path2 from "path";
37
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
+ }
150
+
38
151
  // packages/scanner/src/index.ts
39
152
  function compare(...values) {
40
153
  return values.join("\x00");
@@ -78,7 +191,9 @@ function createScanObservation(input) {
78
191
  scanner: input.scanner,
79
192
  roots,
80
193
  files,
194
+ ...sourceUnits(input.sourceUnits, filePaths),
81
195
  ...operationEvidence(input, filePaths),
196
+ ...httpEvidence(input.httpEndpoints, input.httpRequests, input.operations && new Set(input.operations.map((operation) => operation.id))),
82
197
  diagnostics: [...new Map(input.diagnostics.map((diagnostic) => [
83
198
  diagnosticKey(diagnostic),
84
199
  diagnostic
@@ -87,6 +202,23 @@ function createScanObservation(input) {
87
202
  })
88
203
  };
89
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
+ }
90
222
  function validateRoots(input) {
91
223
  const roots = uniqueBy(input, (root) => root.id, "root id");
92
224
  const byId = new Map(roots.map((root) => [root.id, root]));
@@ -117,22 +249,6 @@ function diagnosticLocation(value) {
117
249
  ...value.line === undefined ? {} : { line: Number(value.line) }
118
250
  };
119
251
  }
120
- function object(value, label) {
121
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
122
- throw new Error(`${label} must be an object`);
123
- }
124
- return value;
125
- }
126
- function string(value, label) {
127
- if (typeof value !== "string")
128
- throw new Error(`${label} must be a string`);
129
- return value;
130
- }
131
- function array(value, label) {
132
- if (!Array.isArray(value))
133
- throw new Error(`${label} must be an array`);
134
- return value;
135
- }
136
252
  function parseScanObservation(source) {
137
253
  const value = object(JSON.parse(source), "observation");
138
254
  if (value.schemaVersion !== 1)
@@ -146,6 +262,9 @@ function parseScanObservation(source) {
146
262
  engineVersion: string(scanner.engineVersion, "scanner.engineVersion")
147
263
  },
148
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")))),
149
268
  roots: array(value.roots, "roots").map((entry) => {
150
269
  const root = object(entry, "root");
151
270
  return {
@@ -296,6 +415,9 @@ function relocateObservation(observation, directory) {
296
415
  ...observation,
297
416
  roots: observation.roots.map((root) => ({ ...root, ...root.file ? { file: file(root.file) } : {} })),
298
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
+ },
299
421
  operations: observation.operations?.map((operation) => ({ ...operation, file: file(operation.file) })),
300
422
  invocations: observation.invocations?.map((call) => ({
301
423
  ...call,
@@ -312,6 +434,9 @@ function combineObservations(parts) {
312
434
  const operations = [];
313
435
  const invocations = [];
314
436
  const diagnostics = [];
437
+ const sourceUnits = [];
438
+ const httpEndpoints = [];
439
+ const httpRequests = [];
315
440
  for (const { key, observation } of parts) {
316
441
  const id = (value) => JSON.stringify([key, value]);
317
442
  roots.push(...observation.roots.map((root) => ({ ...root, id: id(root.id), ...root.parent ? { parent: id(root.parent) } : {} })));
@@ -323,7 +448,10 @@ function combineObservations(parts) {
323
448
  }
324
449
  operations.push(...(observation.operations ?? []).map((operation) => ({ ...operation, id: id(operation.id) })));
325
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) })));
326
453
  diagnostics.push(...observation.diagnostics);
454
+ sourceUnits.push(...observation.sourceUnits ?? []);
327
455
  }
328
456
  return createScanObservation({
329
457
  scanner: parts[0].observation.scanner,
@@ -331,7 +459,10 @@ function combineObservations(parts) {
331
459
  files: [...files.values()],
332
460
  operations,
333
461
  invocations,
334
- diagnostics
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 } : {}
335
466
  });
336
467
  }
337
468
 
@@ -365,9 +496,9 @@ import { access, realpath } from "fs/promises";
365
496
  import path4 from "path";
366
497
  import { fileURLToPath } from "url";
367
498
  var packagedWorker = fileURLToPath(new URL(`../dist/${process.platform}-${process.arch}/worker${process.platform === "win32" ? ".exe" : ""}`, import.meta.url));
368
- function run(command, args, root, env = process.env) {
499
+ function run(command, args, root, { env = process.env, input } = {}) {
369
500
  return new Promise((resolve, reject) => {
370
- execFile2(command, args, {
501
+ const child = execFile2(command, args, {
371
502
  cwd: root,
372
503
  env,
373
504
  encoding: "utf8",
@@ -380,50 +511,51 @@ function run(command, args, root, env = process.env) {
380
511
  else
381
512
  resolve(stdout);
382
513
  });
514
+ if (input !== undefined)
515
+ child.stdin?.end(input);
383
516
  });
384
517
  }
385
- async function checkGoReadiness(repositoryRoot, options = {}) {
386
- const root = await realpath(repositoryRoot);
518
+ async function workerPath(options) {
387
519
  const worker = options.worker ?? packagedWorker;
388
520
  try {
389
521
  await access(worker);
390
522
  } catch {
391
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.");
392
524
  }
393
- const env = { ...process.env, GOTOOLCHAIN: "local", GOPROXY: "off", GOSUMDB: "off" };
394
- let context;
395
- try {
396
- context = JSON.parse(await run(options.go ?? "go", ["env", "-json", "GOMOD", "GOWORK", "GOROOT"], root, env));
397
- } catch (error) {
398
- 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}`);
399
- }
400
- if (context.GOMOD !== path4.join(root, "go.mod")) {
401
- throw new Error("GO_PROJECT_SCOPE: The selected project must contain go.mod.");
402
- }
403
- env.PATH = `${path4.join(context.GOROOT, "bin")}${path4.delimiter}${process.env.PATH ?? ""}`;
404
- try {
405
- await run(options.go ?? "go", ["list", "-mod=readonly", "-deps", "./..."], root, env);
406
- } catch (error) {
407
- 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}`);
408
- }
409
- 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) }));
410
536
  }
411
537
  async function scanGoSource(repositoryRoot, options = {}) {
412
- const { root, worker, env } = await checkGoReadiness(repositoryRoot, options);
538
+ const { root, worker } = await checkGoReadiness(repositoryRoot, options);
413
539
  try {
414
- return parseScanObservation(await run(worker, [root], root, env));
540
+ return parseScanObservation(await run(worker, [root], root));
415
541
  } catch (error) {
416
- 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}`);
417
543
  }
418
544
  }
419
545
 
420
546
  // 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
+ }
421
551
  var scanner = {
422
552
  id: "go",
423
553
  watch: { include: ["**/*.go", "**/go.mod", "**/go.sum", "**/go.work"], exclude: [] },
554
+ listSourceFiles: goSources,
424
555
  checkReadiness: async (root) => {
425
556
  await checkGoReadiness(root);
426
557
  },
558
+ readCodeStructure: readGoCodeStructure,
427
559
  scan: scanGoSource
428
560
  };
429
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))));