From aa996641632c75482c5106cdd30f8d7c4681e4f2 Mon Sep 17 00:00:00 2001 From: Noah Khomer <108771853+noahkhomer18@users.noreply.github.com> Date: Fri, 13 Nov 2026 14:13:53 -0600 Subject: [PATCH] fix(rust): read a single-quoted Cargo edition too TOML literal strings are single-quoted, so edition = '2021' is as valid as the double-quoted form and some projects and generators write it that way. The pattern only matched double quotes, so those crates reported no edition and therefore no runtime, runtimeLatest or runtimeMajorsBehind. The capture now takes whichever quote opened the value and matches its backreference, so both styles read 2021 and the value must still be digits. Fixes #677 --- src/core-open/scanners/rust-scanner.ts | 4 +- test/cargo-rust-edition.test.ts | 51 ++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 test/cargo-rust-edition.test.ts diff --git a/src/core-open/scanners/rust-scanner.ts b/src/core-open/scanners/rust-scanner.ts index 587e4a3..c26efd8 100644 --- a/src/core-open/scanners/rust-scanner.ts +++ b/src/core-open/scanners/rust-scanner.ts @@ -508,8 +508,8 @@ function parseCargoToml( * edition = "2021" */ function extractRustEdition(content: string): string | undefined { - const match = content.match(/^\s*edition\s*=\s*"(\d+)"/m); - return match?.[1]; + const match = content.match(/^\s*edition\s*=\s*(["'])(\d+)\1/m); + return match?.[2]; } // ── Rust project file names ── diff --git a/test/cargo-rust-edition.test.ts b/test/cargo-rust-edition.test.ts new file mode 100644 index 0000000..a4dff75 --- /dev/null +++ b/test/cargo-rust-edition.test.ts @@ -0,0 +1,51 @@ +/** + * Cargo.toml package edition. TOML basic ("2021") and literal ('2021') + * strings are both valid; the scanner must report the same edition and lag + * for either quote style. + */ +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { CargoCache } from '../src/core-open/scanners/cargo-cache.js'; +import { scanRustProjects } from '../src/core-open/scanners/rust-scanner.js'; +import { Semaphore } from '../src/core-open/utils/semaphore.js'; +import type { ProjectScan } from '../src/core-open/types.js'; + +async function scanEdition(editionLine: string): Promise { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'vg-cargo-edition-')); + try { + fs.writeFileSync( + path.join(root, 'Cargo.toml'), + `[package]\nname = "app"\nversion = "0.1.0"\n${editionLine}\n`, + ); + const projects = await scanRustProjects(root, new CargoCache(new Semaphore(1), undefined, true)); + expect(projects).toHaveLength(1); + return projects[0]!; + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +} + +describe('Cargo package edition', () => { + it('reads a double-quoted edition', async () => { + const project = await scanEdition('edition = "2021"'); + + expect(project.runtime).toBe('2021'); + expect(project.runtimeLatest).toBe('2024'); + expect(project.runtimeMajorsBehind).toBe(1); + }); + + it('reads a single-quoted edition with the same lag as the double-quoted form', async () => { + const project = await scanEdition("edition = '2021'"); + + expect(project.runtime).toBe('2021'); + expect(project.runtimeLatest).toBe('2024'); + expect(project.runtimeMajorsBehind).toBe(1); + }); + + it('ignores a quoted edition that is not a year of digits', async () => { + expect((await scanEdition("edition = 'latest'")).runtime).toBeUndefined(); + expect((await scanEdition('edition = "latest"')).runtime).toBeUndefined(); + }); +});