@groma/scanner-rust 0.1.0 → 0.1.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/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@groma/scanner-rust",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Rust project evidence through established semantic 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",
@@ -42,8 +42,7 @@
42
42
  }
43
43
  ],
44
44
  "compatibility": {
45
- "groma": "^0.3.0",
46
- "technologyVersions": {}
45
+ "groma": "^0.3.0"
47
46
  }
48
47
  }
49
48
  }
package/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // plugins/scanners/rust/src/index.ts
2
2
  import { fileURLToPath } from "node:url";
3
- import path2 from "node:path";
3
+ import path3 from "node:path";
4
4
 
5
5
  // packages/scanner/src/index.ts
6
6
  function compare(...values) {
@@ -256,12 +256,73 @@ function sourcePosition(position) {
256
256
  return { position: Number(position) };
257
257
  }
258
258
 
259
+ // plugins/scanners/observations.ts
260
+ function combineObservations(parts) {
261
+ if (!parts.length)
262
+ return;
263
+ const roots = [];
264
+ const files = new Map;
265
+ const operations = [];
266
+ const invocations = [];
267
+ const diagnostics = [];
268
+ for (const { key, observation } of parts) {
269
+ const id = (value) => JSON.stringify([key, value]);
270
+ roots.push(...observation.roots.map((root) => ({ ...root, id: id(root.id), ...root.parent ? { parent: id(root.parent) } : {} })));
271
+ for (const source of observation.files) {
272
+ const combined = files.get(source.file) ?? { file: source.file, roots: [], symbols: [] };
273
+ combined.roots.push(...source.roots.map(id));
274
+ combined.symbols.push(...source.symbols.map((symbol) => ({ ...symbol, id: id(symbol.id) })));
275
+ files.set(source.file, combined);
276
+ }
277
+ operations.push(...(observation.operations ?? []).map((operation) => ({ ...operation, id: id(operation.id) })));
278
+ invocations.push(...(observation.invocations ?? []).map((call) => ({ ...call, source: id(call.source), targets: call.targets.map(id) })));
279
+ diagnostics.push(...observation.diagnostics);
280
+ }
281
+ return createScanObservation({
282
+ scanner: parts[0].observation.scanner,
283
+ roots,
284
+ files: [...files.values()],
285
+ operations,
286
+ invocations,
287
+ diagnostics
288
+ });
289
+ }
290
+
259
291
  // plugins/scanners/rust/src/project.ts
260
- import { execFile } from "node:child_process";
292
+ import { execFile as execFile2 } from "node:child_process";
261
293
  import { access } from "node:fs/promises";
294
+ import path2 from "node:path";
295
+ import { promisify as promisify2 } from "node:util";
296
+
297
+ // plugins/scanners/projects.ts
298
+ import { execFile } from "node:child_process";
299
+ import { existsSync } from "node:fs";
262
300
  import path from "node:path";
263
301
  import { promisify } from "node:util";
264
302
  var execute = promisify(execFile);
303
+ var excluded = new Set([
304
+ ".git",
305
+ "node_modules",
306
+ "vendor",
307
+ "target",
308
+ "dist",
309
+ "build",
310
+ "bin",
311
+ "obj",
312
+ ".gradle",
313
+ ".angular",
314
+ "coverage",
315
+ "generated",
316
+ "groma",
317
+ ".groma"
318
+ ]);
319
+ async function projectFiles(root, matches) {
320
+ const { stdout } = await execute("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { maxBuffer: 64 * 1024 * 1024 });
321
+ return [...new Set(stdout.split("\x00").filter((file) => file && matches(file) && !file.split("/").some((part) => excluded.has(part)) && existsSync(path.join(root, file))))].sort();
322
+ }
323
+
324
+ // plugins/scanners/rust/src/project.ts
325
+ var execute2 = promisify2(execFile2);
265
326
  async function exists(file) {
266
327
  try {
267
328
  await access(file);
@@ -272,59 +333,81 @@ async function exists(file) {
272
333
  }
273
334
  function manifestAt(root, settings) {
274
335
  if (settings.manifest === undefined)
275
- return path.join(root, "Cargo.toml");
336
+ return path2.join(root, "Cargo.toml");
276
337
  if (typeof settings.manifest !== "string") {
277
338
  throw new Error("RUST_PROJECT_SELECTION: Set settings.manifest on the rust entry in scanners.json to a Cargo.toml path.");
278
339
  }
279
- return path.resolve(root, settings.manifest);
340
+ return path2.resolve(root, settings.manifest);
280
341
  }
281
- async function readRustProject(root, settings, options) {
342
+ async function readRustProject(root, settings, options, workspace = false) {
282
343
  const manifest = manifestAt(root, settings);
283
344
  if (!await exists(manifest)) {
284
345
  throw new Error("RUST_PROJECT_MISSING: Select an existing Cargo.toml with settings.manifest on the rust entry in scanners.json.");
285
346
  }
286
347
  let metadata;
287
348
  try {
288
- const result = await execute(options.cargo ?? "cargo", ["metadata", "--format-version", "1", "--offline", "--locked", "--manifest-path", manifest], { cwd: root, maxBuffer: 64 * 1024 * 1024 });
349
+ const result = await execute2(options.cargo ?? "cargo", ["metadata", "--format-version", "1", "--offline", "--locked", "--manifest-path", manifest], { cwd: path2.dirname(manifest), maxBuffer: 64 * 1024 * 1024 });
289
350
  metadata = JSON.parse(result.stdout);
290
351
  } catch (error) {
291
352
  throw new Error(`RUST_CARGO_NOT_READY: Install the project's Rust toolchain, then run cargo fetch --manifest-path "${manifest}" to prepare its lockfile and dependencies. ${error}`);
292
353
  }
293
354
  const packageAtManifest = metadata.packages.find((pkg) => pkg.manifest_path === manifest);
294
- const packages = packageAtManifest ? [packageAtManifest] : metadata.packages.filter((pkg) => metadata.workspace_members.includes(pkg.id));
355
+ const packages = packageAtManifest && !workspace ? [packageAtManifest] : metadata.packages.filter((pkg) => metadata.workspace_members.includes(pkg.id));
295
356
  const targets = packages.flatMap((pkg) => pkg.targets).filter((target) => target.kind.some((kind) => kind === "lib" || kind === "bin")).map((target) => target.src_path).sort();
296
357
  if (!targets.length)
297
358
  throw new Error("RUST_TARGET_MISSING: The selected Cargo project needs a library or binary target.");
298
- return { root, manifest, name: packageAtManifest?.name ?? path.basename(path.dirname(manifest)), targets };
359
+ return { root, manifest, name: packageAtManifest?.name ?? path2.basename(path2.dirname(manifest)), targets };
299
360
  }
300
361
  async function checkRustToolchain(root, options) {
301
362
  try {
302
- await execute(options.cargo ?? "cargo", ["--version"], { cwd: root });
303
- const result = await execute(options.rustc ?? "rustc", ["--print", "sysroot"], { cwd: root });
304
- const library = path.join(result.stdout.trim(), "lib/rustlib/src/rust/library");
363
+ await execute2(options.cargo ?? "cargo", ["--version"], { cwd: root });
364
+ const result = await execute2(options.rustc ?? "rustc", ["--print", "sysroot"], { cwd: root });
365
+ const library = path2.join(result.stdout.trim(), "lib/rustlib/src/rust/library");
305
366
  if (!await exists(library))
306
367
  throw new Error("Rust standard library sources are missing.");
307
368
  } catch (error) {
308
369
  throw new Error(`RUST_TOOLCHAIN_MISSING: Install the project's Rust toolchain with Cargo and rustc, and add its standard library sources with rustup component add rust-src. ${error}`);
309
370
  }
310
371
  }
372
+ async function rustProjects(root, settings, options = {}) {
373
+ if (settings.manifest !== undefined)
374
+ return [manifestAt(root, settings)];
375
+ const selected = new Set;
376
+ const covered = new Set;
377
+ for (const file of await projectFiles(root, (file) => path2.posix.basename(file) === "Cargo.toml")) {
378
+ const manifest = path2.resolve(root, file);
379
+ if (covered.has(manifest))
380
+ continue;
381
+ const { stdout } = await execute2(options.cargo ?? "cargo", ["metadata", "--no-deps", "--format-version", "1", "--offline", "--locked", "--manifest-path", manifest], { cwd: path2.dirname(manifest), maxBuffer: 64 * 1024 * 1024 });
382
+ const metadata = JSON.parse(stdout);
383
+ selected.add(path2.join(metadata.workspace_root, "Cargo.toml"));
384
+ for (const pkg of metadata.packages)
385
+ if (metadata.workspace_members.includes(pkg.id))
386
+ covered.add(pkg.manifest_path);
387
+ covered.add(path2.join(metadata.workspace_root, "Cargo.toml"));
388
+ }
389
+ return [...selected].sort();
390
+ }
311
391
 
312
392
  // plugins/scanners/rust/src/index.ts
313
393
  var executable = fileURLToPath(new URL(`../dist/bin/${process.platform}-${process.arch}/groma-rust-scanner${process.platform === "win32" ? ".exe" : ""}`, import.meta.url));
314
- async function checkRustReadiness(repositoryRoot, settings = {}, options = {}) {
315
- const root = path2.resolve(repositoryRoot);
394
+ async function checkRustReadiness(repositoryRoot, settings = {}, options = {}, workspace = false) {
395
+ const root = path3.resolve(repositoryRoot);
316
396
  const worker = options.worker ?? executable;
317
397
  if (!await exists(worker)) {
318
398
  throw new Error("RUST_WORKER_MISSING: Install the packaged Rust scanner, or build it with bun plugins/scanners/rust/build.ts.");
319
399
  }
320
- await checkRustToolchain(root, options);
321
- return { input: await readRustProject(root, settings, options), worker };
400
+ await checkRustToolchain(path3.dirname(path3.resolve(root, typeof settings.manifest === "string" ? settings.manifest : "Cargo.toml")), options);
401
+ return { input: await readRustProject(root, settings, options, workspace), worker };
322
402
  }
323
403
  async function scanRustSource(repositoryRoot, settings = {}, options = {}) {
324
404
  const { input, worker } = await checkRustReadiness(repositoryRoot, settings, options);
405
+ return runRust(input, worker);
406
+ }
407
+ async function runRust(input, worker) {
325
408
  try {
326
- const pending = execute(worker, [], {
327
- cwd: input.root,
409
+ const pending = execute2(worker, [], {
410
+ cwd: path3.dirname(input.manifest),
328
411
  encoding: "utf8",
329
412
  timeout: 120000,
330
413
  maxBuffer: 64 * 1024 * 1024,
@@ -351,10 +434,23 @@ var scanner = {
351
434
  ],
352
435
  exclude: ["**/target/**", "**/node_modules/**", "**/.git/**", "**/vendor/**", "**/dist/**"]
353
436
  },
354
- checkReadiness: async (root, settings) => {
355
- await checkRustReadiness(root, settings);
437
+ checkReadiness: async (root, settings = {}) => {
438
+ const projects = await rustProjects(root, settings);
439
+ if (!projects.length)
440
+ throw new Error("RUST_PROJECT_MISSING: No Cargo.toml was found.");
441
+ for (const manifest of projects)
442
+ await checkRustReadiness(root, { ...settings, manifest }, {}, settings.manifest === undefined);
356
443
  },
357
- scan: scanRustSource
444
+ scan: async (root, settings = {}) => {
445
+ const parts = [];
446
+ for (const manifest of await rustProjects(root, settings)) {
447
+ const options = { ...settings, manifest };
448
+ const { input, worker } = await checkRustReadiness(root, options, {}, settings.manifest === undefined);
449
+ const observation = await runRust(input, worker);
450
+ parts.push({ key: path3.relative(root, manifest).split(path3.sep).join("/"), observation });
451
+ }
452
+ return combineObservations(parts);
453
+ }
358
454
  };
359
455
  var src_default = scanner;
360
456
  export {