From 852390f5fae353a37f89d4290bcb71eb8801a91b Mon Sep 17 00:00:00 2001 From: Will-thom <116388885+Will-thom@users.noreply.github.com> Date: Tue, 26 May 2026 17:02:24 -0300 Subject: [PATCH] feat(scanner): detect dotnet web and test tooling --- README.md | 1 + docs/detectors.md | 4 ++- src/scanner.mjs | 33 +++++++++++++++++-- .../Fixture.Api.Tests.csproj | 6 ++++ test/scanner.test.mjs | 5 +++ 5 files changed, 46 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c3d6406..5d82c33 100644 --- a/README.md +++ b/README.md @@ -423,6 +423,7 @@ Current detectors cover common JavaScript/TypeScript, Python, Ruby, PHP, C#, Jav - package manager: npm, pnpm, yarn, bun, pip, bundler, composer, dotnet, maven, gradle, cargo, go - commands: install, dev, start, build, test, lint, format, backend install, local services +- .NET tooling: ASP.NET Core, xUnit, NUnit, MSTest, Microsoft.NET.Test.Sdk - docs: README, architecture docs, ADR directories, existing agent docs - CI: GitHub Actions workflows - monorepos: npm/pnpm workspaces, Turborepo, Nx, Lerna, Rush diff --git a/docs/detectors.md b/docs/detectors.md index 85f9eb8..38f1e76 100644 --- a/docs/detectors.md +++ b/docs/detectors.md @@ -12,7 +12,7 @@ This page summarizes the current detector coverage. Claims here should stay alig | Python | `pyproject.toml`, `requirements.txt`, `setup.py`, `tests/` | FastAPI, Django, Flask, Pytest, Ruff | `python3 -m pip install ...`, `python3 -m pytest`, Ruff lint/format | | Ruby | `Gemfile`, `config/application.rb`, `bin/rails` | Rails | `bundle install`, `bin/rails server`, `bin/rails test` | | PHP | `composer.json`, `artisan`, `app/Http/Kernel.php` | Laravel | `composer install`, `php artisan serve`, `php artisan test` | -| C# / .NET | `*.csproj`, `*.sln`, `global.json`, `Program.cs` | .NET | `dotnet restore`, `dotnet build`, `dotnet test` | +| C# / .NET | `*.csproj`, `*.sln`, `global.json`, `Program.cs`, `Microsoft.NET.Sdk.Web`, common test package references | .NET, ASP.NET Core, xUnit, NUnit, MSTest, .NET Test SDK | `dotnet restore`, `dotnet build`, `dotnet test` | | Java / Kotlin | `pom.xml`, `mvnw`, `build.gradle`, `build.gradle.kts`, `gradlew`, Spring Boot plugins/dependencies | Spring Boot | `./mvnw package`, `./mvnw test`, `./gradlew build`, `./gradlew test` | | Rust | `Cargo.toml` | Actix, Axum, Rocket as Rust Web | `cargo build`, `cargo test`, `cargo clippy --all-targets --all-features`, `cargo fmt` | | Go | `go.mod` | Gin | `go build ./...`, `go test ./...`, `go vet ./...`, `gofmt -w .` | @@ -32,6 +32,8 @@ This page summarizes the current detector coverage. Claims here should stay alig | Rails | `Gemfile` containing `rails`, `config/application.rb`, `bin/rails` | | Laravel | `composer.json` with `laravel/framework`, `artisan`, `app/Http/Kernel.php` | | .NET | `*.csproj`, `*.sln`, `global.json`, `Program.cs` | +| ASP.NET Core | `Microsoft.NET.Sdk.Web`, `WebApplication.CreateBuilder`, or minimal API route mapping in `Program.cs` | +| .NET test tooling | `PackageReference` entries for `Microsoft.NET.Test.Sdk`, xUnit, NUnit, or MSTest | | Spring Boot | Spring Boot references in `pom.xml`, `build.gradle`, or `build.gradle.kts` | | Docker Compose | Compose files named `docker-compose.yml`, `docker-compose.yaml`, `compose.yml`, or `compose.yaml` | diff --git a/src/scanner.mjs b/src/scanner.mjs index 0dab02e..85c1711 100644 --- a/src/scanner.mjs +++ b/src/scanner.mjs @@ -60,6 +60,7 @@ export async function scanRepo(root = process.cwd(), options = {}) { const mergedGradleBuild = [gradleBuild, gradleBuildKts].filter(Boolean).join("\n"); const languageCounts = countLanguages(files); + const dotnetFiles = await readDotnetFiles(absoluteRoot, files); const packageManager = await detectPackageManager(absoluteRoot, files); const detectedCommands = await detectCommands({ root: absoluteRoot, @@ -90,7 +91,7 @@ export async function scanRepo(root = process.cwd(), options = {}) { name: detectName({ packageJson, pyproject, cargoToml, goMod, pomXml, composerJson, files, root: absoluteRoot }), languages: languageCounts, primaryLanguage: languageCounts[0]?.name || "Unknown", - frameworks: detectFrameworks({ packageJson, pyproject, cargoToml, goMod, pomXml, gradleBuild: mergedGradleBuild, gemfile, composerJson, files }), + frameworks: detectFrameworks({ packageJson, pyproject, cargoToml, goMod, pomXml, gradleBuild: mergedGradleBuild, gemfile, composerJson, files, dotnetFiles }), monorepo: detectMonorepo({ packageJson, pnpmWorkspace, files }), packageManager, commands, @@ -273,7 +274,7 @@ function detectName({ packageJson, pyproject, cargoToml, goMod, pomXml, composer return path.basename(root); } -function detectFrameworks({ packageJson, pyproject, cargoToml, goMod, pomXml, gradleBuild, gemfile, composerJson, files }) { +function detectFrameworks({ packageJson, pyproject, cargoToml, goMod, pomXml, gradleBuild, gemfile, composerJson, files, dotnetFiles = [] }) { const deps = { ...(packageJson?.dependencies || {}), ...(packageJson?.devDependencies || {}), @@ -317,6 +318,8 @@ function detectFrameworks({ packageJson, pyproject, cargoToml, goMod, pomXml, gr if (hasLaravelSignals({ composerJson, files })) names.push("Laravel"); if (hasSpringBootSignals({ pomXml, gradleBuild })) names.push("Spring Boot"); if (hasDotnetSignals(files)) names.push(".NET"); + if (hasAspNetCoreSignals(dotnetFiles)) names.push("ASP.NET Core"); + names.push(...detectDotnetTestTools(dotnetFiles)); if (/actix|axum|rocket/i.test(cargoToml)) names.push("Rust Web"); if (/github\.com\/gin-gonic\/gin/i.test(goMod)) names.push("Gin"); if (hasNextAppRouter) names.push("Next.js App Router"); @@ -334,6 +337,32 @@ function hasDotnetSignals(files) { )); } +async function readDotnetFiles(root, files) { + const dotnetFiles = files.filter((file) => file.endsWith(".csproj") || /(^|\/)Program\.cs$/.test(file)); + return Promise.all(dotnetFiles.map(async (file) => ({ + file, + text: await readTextIfExists(path.join(root, file)), + }))); +} + +function hasAspNetCoreSignals(dotnetFiles) { + return dotnetFiles.some(({ file, text }) => ( + file.endsWith(".csproj") && / item.text).join("\n"); + const tools = []; + if (/ item.endsWith(extension)); return file ? path.basename(file, extension) : ""; diff --git a/test/fixtures/dotnet-app/tests/Fixture.Api.Tests/Fixture.Api.Tests.csproj b/test/fixtures/dotnet-app/tests/Fixture.Api.Tests/Fixture.Api.Tests.csproj index 68af787..596e026 100644 --- a/test/fixtures/dotnet-app/tests/Fixture.Api.Tests/Fixture.Api.Tests.csproj +++ b/test/fixtures/dotnet-app/tests/Fixture.Api.Tests/Fixture.Api.Tests.csproj @@ -3,4 +3,10 @@ net9.0 true + + + + + + diff --git a/test/scanner.test.mjs b/test/scanner.test.mjs index 1b11830..c23a631 100644 --- a/test/scanner.test.mjs +++ b/test/scanner.test.mjs @@ -117,6 +117,11 @@ test("scan detects .NET and C# project signals", async () => { assert.equal(profile.primaryLanguage, "C#"); assert.equal(profile.packageManager, "dotnet"); assert.ok(profile.frameworks.includes(".NET")); + assert.ok(profile.frameworks.includes("ASP.NET Core")); + assert.ok(profile.frameworks.includes(".NET Test SDK")); + assert.ok(profile.frameworks.includes("xUnit")); + assert.ok(profile.frameworks.includes("NUnit")); + assert.ok(profile.frameworks.includes("MSTest")); assert.equal(profile.commands.install, "dotnet restore"); assert.equal(profile.commands.build, "dotnet build"); assert.equal(profile.commands.test, "dotnet test");