diff --git a/src/detect.js b/src/detect.js index 3d64f98..0606794 100644 --- a/src/detect.js +++ b/src/detect.js @@ -80,6 +80,51 @@ async function detectGoMod(root, output) { } } +async function detectCargoToml(root, output) { + const content = await read(root, "Cargo.toml"); + if (!content) return; + const lines = content.split(/\r?\n/); + + for (const [index, line] of lines.entries()) { + const match = line.match(/^\s*rust-version\s*=\s*["']([^"']+)["']/); + if (match) { + add(output, "rust", "Cargo.toml", match[1], index + 1); + } + } +} + +async function detectRustToolchain(root, output) { + const content = await read(root, "rust-toolchain.toml"); + if (!content) return; + const lines = content.split(/\r?\n/); + + for (const [index, line] of lines.entries()) { + const match = line.match(/^\s*channel\s*=\s*["']([^"']+)["']/); + if (match) { + add(output, "rust", "rust-toolchain.toml", match[1], index + 1); + } + } +} + +async function detectGoMod(root, output) { + const content = await read(root, "go.mod"); + if (!content) return; + + const lines = content.split(/\r?\n/); + + for (const [index, line] of lines.entries()) { + const go = line.match(/^\s*go\s+(\S+)/); + if (go) { + add(output, "go", "go.mod", go[1], index + 1); + } + + const toolchain = line.match(/^\s*toolchain\s+go(\S+)/); + if (toolchain) { + add(output, "go", "go.mod", toolchain[1], index + 1); + } + } +} + async function detectToolVersions(root, output) { const content = await read(root, ".tool-versions"); if (!content) return; @@ -115,7 +160,7 @@ async function detectDockerfiles(root, output) { const content = await read(root, entry.name); for (const [index, line] of content.split(/\r?\n/).entries()) { const match = line.match( - /^\s*FROM\s+(?:[^/\s]+\/)?(node|python|golang):([^\s@]+)/i, + /^\s*FROM\s+(?:[^/\s]+\/)?(node|python|golang|rust):([^\s@]+)/i, ); if (match) add( @@ -151,9 +196,11 @@ async function detectGithubActions(root, output) { let remaining = 0; for (const [index, line] of lines.entries()) { - const action = line.match(/uses:\s*actions\/setup-(node|python|go)@/i); + const action = line.match( + /uses:\s*(?:actions\/setup-(node|python|go)|dtolnay\/rust-toolchain)@/i, + ); if (action) { - runtime = action[1].toLowerCase(); + runtime = action[1] ? action[1].toLowerCase() : "rust"; remaining = 12; continue; } @@ -162,7 +209,9 @@ async function detectGithubActions(root, output) { continue; } remaining -= 1; - const declared = line.match(/(?:node-version|python-version|go-version):\s*["']?([^"'#\s]+)/i); + const declared = line.match( + /(?:node-version|python-version|go-version|toolchain):\s*["']?([^"'#\s]+)/i, + ); if (runtime && declared && !declared[1].startsWith("${{")) { add(output, runtime, file, declared[1], index + 1); runtime = null; @@ -178,6 +227,8 @@ export async function detectDeclarations(root) { detectPackageJson(root, output), detectPyproject(root, output), detectGoMod(root, output), + detectCargoToml(root, output), + detectRustToolchain(root, output), detectToolVersions(root, output), detectMise(root, output), detectDockerfiles(root, output), diff --git a/src/format.js b/src/format.js index 70a9273..bb5f3cf 100644 --- a/src/format.js +++ b/src/format.js @@ -6,6 +6,7 @@ const LABELS = { go: "Go", node: "Node.js", python: "Python", + rust: "Rust", }; export function formatHuman(result) { diff --git a/test/detect.test.js b/test/detect.test.js index ec7ab70..d15073d 100644 --- a/test/detect.test.js +++ b/test/detect.test.js @@ -6,17 +6,18 @@ import { inspect } from "../src/index.js"; const here = path.dirname(fileURLToPath(import.meta.url)); -test("finds aligned Node.js and Python declarations", async () => { +test("finds aligned Node.js, golang, Rust, and Python declarations", async () => { const result = await inspect(path.join(here, "fixtures", "aligned")); assert.equal(result.ok, true); assert.deepEqual( result.runtimes.map((runtime) => runtime.runtime), - ["go", "node", "python"], + ["go", "node", "python", "rust"], ); assert.equal(result.runtimes[0].declarations.length, 3); assert.equal(result.runtimes[1].declarations.length, 5); assert.equal(result.runtimes[2].declarations.length, 5); + assert.equal(result.runtimes[3].declarations.length, 4); }); test("finds Docker and project-file drift", async () => { @@ -24,12 +25,20 @@ test("finds Docker and project-file drift", async () => { assert.equal(result.ok, false); assert.deepEqual( result.runtimes.map((runtime) => runtime.runtime), - ["go", "node"], + ["go", "node", "rust"], ); + // Node.js, golang, and Rust are all misaligned + const node = result.runtimes.find((runtime) => runtime.runtime === "node"); + assert.ok(node); + assert.equal(node.ok, false); const go = result.runtimes.find((runtime) => runtime.runtime === "go"); assert.ok(go); assert.equal(go.ok, false); + + const rust = result.runtimes.find((runtime) => runtime.runtime === "rust"); + assert.ok(rust); + assert.equal(rust.ok, false); }); test("returns an empty success for a directory with no declarations", async () => { diff --git a/test/fixtures/aligned/.github/workflows/ci.yml b/test/fixtures/aligned/.github/workflows/ci.yml index d932b85..80d14d9 100644 --- a/test/fixtures/aligned/.github/workflows/ci.yml +++ b/test/fixtures/aligned/.github/workflows/ci.yml @@ -14,3 +14,8 @@ jobs: - uses: actions/setup-go@v5 with: go-version: "1.24" + rust: + steps: + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: "1.81.0" diff --git a/test/fixtures/aligned/Cargo.toml b/test/fixtures/aligned/Cargo.toml new file mode 100644 index 0000000..0331b24 --- /dev/null +++ b/test/fixtures/aligned/Cargo.toml @@ -0,0 +1,5 @@ +[package] +name = "example" +version = "0.1.0" +edition = "2021" +rust-version = "1.81" diff --git a/test/fixtures/aligned/Dockerfile b/test/fixtures/aligned/Dockerfile index 82caf45..837dd2e 100644 --- a/test/fixtures/aligned/Dockerfile +++ b/test/fixtures/aligned/Dockerfile @@ -1,2 +1,3 @@ FROM node:22-alpine AS web FROM python:3.12-slim AS worker +FROM rust:1.81.0 diff --git a/test/fixtures/aligned/rust-toolchain.toml b/test/fixtures/aligned/rust-toolchain.toml new file mode 100644 index 0000000..1de01fa --- /dev/null +++ b/test/fixtures/aligned/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "1.81.0" diff --git a/test/fixtures/drift/Cargo.toml b/test/fixtures/drift/Cargo.toml new file mode 100644 index 0000000..0331b24 --- /dev/null +++ b/test/fixtures/drift/Cargo.toml @@ -0,0 +1,5 @@ +[package] +name = "example" +version = "0.1.0" +edition = "2021" +rust-version = "1.81" diff --git a/test/fixtures/drift/Dockerfile b/test/fixtures/drift/Dockerfile index 67a885b..373bd7c 100644 --- a/test/fixtures/drift/Dockerfile +++ b/test/fixtures/drift/Dockerfile @@ -1,2 +1,3 @@ FROM node:20-alpine FROM golang:1.23 +FROM rust:1.82 diff --git a/test/fixtures/drift/rust-toolchain.toml b/test/fixtures/drift/rust-toolchain.toml new file mode 100644 index 0000000..1de01fa --- /dev/null +++ b/test/fixtures/drift/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "1.81.0"