diff --git a/detect/detect_test.go b/detect/detect_test.go index 74ab89c..0d0d709 100644 --- a/detect/detect_test.go +++ b/detect/detect_test.go @@ -658,6 +658,125 @@ func TestNodeProject(t *testing.T) { } } +func TestNodeRustNativeExtensions(t *testing.T) { + t.Run("projects", func(t *testing.T) { + tests := []struct { + name string + fixture string + tool string + }{ + {name: "napi-rs", fixture: "../testdata/napi-rs-project", tool: "napi-rs"}, + {name: "Neon", fixture: "../testdata/neon-project", tool: "Neon"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := runOn(t, tt.fixture) + assertToolDetectedWithConfidence(t, r, "native_extension", tt.tool, brief.ConfidenceHigh) + }) + } + }) + + t.Run("signals", func(t *testing.T) { + tests := []struct { + name string + files map[string]string + tool string + }{ + { + name: "napi-rs npm dependency", + files: map[string]string{ + "package.json": `{"name":"example","devDependencies":{"@napi-rs/cli":"^3.0.0"}}`, + }, + tool: "napi-rs", + }, + { + name: "napi-rs build script", + files: map[string]string{ + "package.json": `{"name":"example","scripts":{"build":"napi build --platform --release"}}`, + }, + tool: "napi-rs", + }, + { + name: "napi crate", + files: map[string]string{ + "package.json": `{"name":"example"}`, + "Cargo.toml": "[package]\nname = \"example\"\nversion = \"0.1.0\"\n[dependencies]\nnapi = \"3\"\n", + }, + tool: "napi-rs", + }, + { + name: "napi-derive crate", + files: map[string]string{ + "package.json": `{"name":"example"}`, + "Cargo.toml": "[package]\nname = \"example\"\nversion = \"0.1.0\"\n[dependencies]\nnapi-derive = \"3\"\n", + }, + tool: "napi-rs", + }, + { + name: "napi-build crate", + files: map[string]string{ + "package.json": `{"name":"example"}`, + "Cargo.toml": "[package]\nname = \"example\"\nversion = \"0.1.0\"\n[build-dependencies]\nnapi-build = \"2\"\n", + }, + tool: "napi-rs", + }, + { + name: "Neon npm dependency", + files: map[string]string{ + "package.json": `{"name":"example","devDependencies":{"@neon-rs/cli":"^0.1.82"}}`, + }, + tool: "Neon", + }, + { + name: "Neon Cargo dependency", + files: map[string]string{ + "package.json": `{"name":"example"}`, + "Cargo.toml": "[package]\nname = \"example\"\nversion = \"0.1.0\"\n[dependencies.neon]\nversion = \"1.1\"\n", + }, + tool: "Neon", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir := t.TempDir() + for path, content := range tt.files { + writeProjectFile(t, dir, path, content) + } + r := runOn(t, dir) + assertToolDetectedWithConfidence(t, r, "native_extension", tt.tool, brief.ConfidenceHigh) + }) + } + }) + + t.Run("near miss", func(t *testing.T) { + dir := t.TempDir() + writeProjectFile( + t, + dir, + "package.json", + `{"name":"near-miss","dependencies":{"napi":"1","neon":"1"},"scripts":{"palette":"echo neon"}}`, + ) + writeProjectFile( + t, + dir, + "Cargo.toml", + "[package]\nname = \"near-miss\"\nversion = \"0.1.0\"\n[dependencies]\nother-napi = \"1\"\nultraviolet-neon = \"1\"\n", + ) + writeProjectFile(t, dir, "README.md", "napi-rs uses the napi build command.\n") + + r := runOn(t, dir) + for _, name := range []string{"napi-rs", "Neon"} { + if slices.ContainsFunc(r.Tools["native_extension"], func(d brief.Detection) bool { + return d.Name == name + }) { + t.Errorf("prose mention should not detect %s", name) + } + } + }) +} + func TestPythonProject(t *testing.T) { engine := New(loadKB(t), "../testdata/python-project") r, err := engine.Run() diff --git a/knowledge/node/napi-rs.toml b/knowledge/node/napi-rs.toml new file mode 100644 index 0000000..02371f2 --- /dev/null +++ b/knowledge/node/napi-rs.toml @@ -0,0 +1,37 @@ +[tool] +name = "napi-rs" +category = "native_extension" +homepage = "https://napi.rs/" +docs = "https://napi.rs/docs/introduction/getting-started" +repo = "https://github.com/napi-rs/napi-rs" +description = "Builds Node-API addons written in Rust" + +# Match the Cargo crates in Cargo.toml rather than through the global dependency +# cache so same-named npm packages do not trigger this definition. +[detect] +dependencies = ["@napi-rs/cli"] +ecosystems = ["node"] + +[detect.file_contains] +"package.json" = ["napi build"] +"Cargo.toml" = [ + "\nnapi =", + "\nnapi=", + "\nnapi-derive =", + "\nnapi-derive=", + "\nnapi-build =", + "\nnapi-build=", + "[dependencies.napi]", + "[dependencies.napi-derive]", + "[build-dependencies.napi-build]", +] + +[commands] +run = "npx napi build --platform --release" +alternatives = ["npx napi build"] + +[config] +files = ["package.json", "Cargo.toml", "build.rs"] + +[taxonomy] +role = ["native-extension"] diff --git a/knowledge/node/neon.toml b/knowledge/node/neon.toml new file mode 100644 index 0000000..9b3c60b --- /dev/null +++ b/knowledge/node/neon.toml @@ -0,0 +1,26 @@ +[tool] +name = "Neon" +category = "native_extension" +homepage = "https://neon-rs.dev/" +docs = "https://neon-rs.dev/docs/hello-world/" +repo = "https://github.com/neon-bindings/neon" +description = "Builds Node.js native addons written in Rust" + +# Match the Cargo crate in Cargo.toml rather than through the global dependency +# cache so a same-named npm package does not trigger this definition. +[detect] +dependencies = ["@neon-rs/cli"] +ecosystems = ["node"] + +[detect.file_contains] +"Cargo.toml" = ["\nneon =", "\nneon=", "[dependencies.neon]"] + +[commands] +run = "npm run build" +alternatives = ["npm run build -- --release"] + +[config] +files = ["package.json", "Cargo.toml"] + +[taxonomy] +role = ["native-extension"] diff --git a/testdata/napi-rs-project/Cargo.toml b/testdata/napi-rs-project/Cargo.toml new file mode 100644 index 0000000..56be32f --- /dev/null +++ b/testdata/napi-rs-project/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "napi-rs-project" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +napi = { version = "3", features = ["napi8"] } +napi-derive = "3" + +[build-dependencies] +napi-build = "2" diff --git a/testdata/napi-rs-project/package.json b/testdata/napi-rs-project/package.json new file mode 100644 index 0000000..cdc9ca8 --- /dev/null +++ b/testdata/napi-rs-project/package.json @@ -0,0 +1,10 @@ +{ + "name": "napi-rs-project", + "version": "0.1.0", + "scripts": { + "build": "napi build --platform --release" + }, + "devDependencies": { + "@napi-rs/cli": "^3.0.0" + } +} diff --git a/testdata/napi-rs-project/src/lib.rs b/testdata/napi-rs-project/src/lib.rs new file mode 100644 index 0000000..ace84d3 --- /dev/null +++ b/testdata/napi-rs-project/src/lib.rs @@ -0,0 +1 @@ +pub fn fixture() {} diff --git a/testdata/neon-project/Cargo.toml b/testdata/neon-project/Cargo.toml new file mode 100644 index 0000000..6dfecc4 --- /dev/null +++ b/testdata/neon-project/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "neon-project" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies.neon] +version = "1.1" +features = ["napi-8"] diff --git a/testdata/neon-project/package.json b/testdata/neon-project/package.json new file mode 100644 index 0000000..2fc1cb1 --- /dev/null +++ b/testdata/neon-project/package.json @@ -0,0 +1,12 @@ +{ + "name": "neon-project", + "version": "0.1.0", + "scripts": { + "cargo-build": "cargo build --message-format=json-render-diagnostics > cargo.log", + "postcargo-build": "neon dist < cargo.log", + "build": "npm run cargo-build -- --release" + }, + "devDependencies": { + "@neon-rs/cli": "^0.1.82" + } +} diff --git a/testdata/neon-project/src/lib.rs b/testdata/neon-project/src/lib.rs new file mode 100644 index 0000000..ace84d3 --- /dev/null +++ b/testdata/neon-project/src/lib.rs @@ -0,0 +1 @@ +pub fn fixture() {}