Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/detect-roslynator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@taskless/cli": patch
---

`taskless detect` now recognizes C# repositories and the Roslynator, StyleCop and SonarAnalyzer analyzers. C# comes from a `.csproj`. Roslynator comes from its NuGet packages in an MSBuild file, `packages.config` or `dotnet-tools.json`, its keys in `.editorconfig` or `.globalconfig`, a `.roslynatorconfig`, or a CI or hook command that runs it. StyleCop and SonarAnalyzer are each reported as their own linter, from their NuGet package, their config file, or their diagnostic ids.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-11-06
45 changes: 45 additions & 0 deletions openspec/changes/archive/2026-11-06-detect-roslynator/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
## Why

`taskless detect` has no notion of C#. A .NET repository reports no language and no linter, so `init`, `onboard` and the `route` recipe treat it as an unlinted project of unknown language, even though ast-grep rules work there through `CSharp`. Roslynator is a common Roslyn analyzer set, and because it is configured through `.editorconfig` and installed as a NuGet package, none of detect's existing readers can see it.

## What Changes

- `detect` reports `C#` among `languages` when a `.csproj` exists anywhere in the walk.
- A new `roslynator` linter signal, per https://josefpihrt.github.io/docs/roslynator/configuration, read from:
- NuGet references to `Roslynator.Analyzers`, `Roslynator.Formatting.Analyzers`, `Roslynator.CodeAnalysis.Analyzers` and `Roslynator.DotNet.Cli`, in any `.csproj`, `.props` or `.targets` file, a `packages.config`, or a `dotnet-tools.json`.
- Roslynator's keys in `.editorconfig` or `.globalconfig`: `roslynator_*`, `dotnet_diagnostic.RCS*`, and `dotnet_analyzer_diagnostic.category-roslynator.*`.
- A committed `.roslynatorconfig`.
- Root CI and hook commands that run or install it.
- `stylecop` and `sonaranalyzer` signals, separate from `roslynator` and from each other, per the StyleCop Analyzers configuration doc and the sonar-dotnet README. Each is read from its NuGet package, its own config file (`stylecop.json`, `.stylecop.json`, `SonarLint.xml`), and its `SA` or `S` diagnostic ids. Neither has CLI invocation evidence: both run inside the build.
- `configKeys` joins `LinterSignal`: keys in a shared config file, which any later analyzer configured through `.editorconfig` can reuse.
- `bin` and `obj` join the directories the walk prunes, as .NET build output.

Nothing here is **BREAKING**. Pre-1.0, an added linter signal is a `patch`.

## Non-goals

- **A combined `roslyn` linter.** People know the tool they installed, not the platform under it, so each analyzer keeps its own name.
- **The SDK's own `CA` rules.** They ship with the toolchain, so they need signals of their own, as clippy did.
- **`customRules` for Roslynator.** It has no custom-rule mechanism of its own; a house analyzer is a Roslyn concern, independent of Roslynator.
- **A `create-legacy-rule` recipe.** Without one the legacy ladder does not apply, so a rule request in a Roslynator repository routes to a Taskless rule, as it does for rubocop and clippy.
- **The telemetry `languageStack` dimension.** It probes exact root paths and a project file has no fixed name, so it does not report C#.

## Capabilities

### New Capabilities

None.

### Modified Capabilities

- `cli-detect`: new requirements cover C# and Roslynator detection, and separate StyleCop and SonarAnalyzer linters.

## Impact

- `packages/cli/src/detect/scan.ts`: NuGet readers, `configKeys`, the signals, and two pruned directories.
- `packages/cli/src/schemas/detect.ts`, `packages/cli/src/agent/detect.md`: the evidence description gains config keys.
- `packages/cli/test/detect.test.ts`: positive and negative cases per signal.

## Delivery shape

**Single PR.** One signal and its readers, tests, spec delta and archive fit one reviewable diff and are safe in production on their own.
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
## ADDED Requirements

### Requirement: Detect recognizes C# and Roslynator

`detect` SHALL report `C#` among `languages` when a `.csproj` file exists in the scanned tree. A `.sln` file SHALL NOT mark C#, because a solution can hold projects in any language.

`detect` SHALL report `roslynator` when any of these exists:

- A NuGet reference to `Roslynator.Analyzers`, `Roslynator.Formatting.Analyzers`, `Roslynator.CodeAnalysis.Analyzers`, or `Roslynator.DotNet.Cli`, matched case-insensitively, as a `PackageReference` or `GlobalPackageReference` in a `.csproj`, `.props`, or `.targets` file, an entry in a `packages.config`, or a tool in a `dotnet-tools.json`.
- A key in `.editorconfig` or `.globalconfig` that begins `roslynator_`, `dotnet_diagnostic.rcs` followed by a digit, or `dotnet_analyzer_diagnostic.category-roslynator.`, matched case-insensitively.
- A `.roslynatorconfig` file.
- A root CI or hook command that runs `roslynator analyze`, `fix`, or `format`, or names `roslynator.dotnet.cli`.

A key in a comment, a reference in an XML comment, and a `PackageVersion` entry SHALL NOT be evidence: central package management can pin a package no project uses. Files under `bin` or `obj` SHALL NOT be read. A repository being C# SHALL NOT by itself report `roslynator`.

#### Scenario: A C# repository with no Roslynator

- **WHEN** a repository has a `.csproj` and nothing references or configures Roslynator
- **THEN** `detect --json` SHALL report `C#` among `languages` and SHALL NOT report `roslynator`

#### Scenario: Roslynator installed from a shared MSBuild file

- **WHEN** `Directory.Build.props` has a `PackageReference` to `Roslynator.Analyzers`
- **THEN** `detect --json` SHALL report `roslynator` with a dependency marker naming that file
- **AND** SHALL report `C#` among `languages`

#### Scenario: Roslynator configured in EditorConfig

- **WHEN** `.editorconfig` sets `dotnet_diagnostic.RCS1044.severity`
- **THEN** `detect --json` SHALL report `roslynator` with evidence naming the file and the key

#### Scenario: Roslynator run from CI

- **WHEN** a root GitHub Actions workflow runs `roslynator analyze`
- **THEN** `detect --json` SHALL report `roslynator` with evidence naming the workflow file and the command

#### Scenario: A central version pin is not evidence

- **WHEN** the only mention of Roslynator is a `PackageVersion` in `Directory.Packages.props`
- **THEN** `detect --json` SHALL NOT report `roslynator`

#### Scenario: A commented-out signal is not evidence

- **WHEN** the only mention of Roslynator is an XML comment in a project file or a comment in `.editorconfig`
- **THEN** `detect --json` SHALL NOT report `roslynator`

#### Scenario: Another analyzer's diagnostic id is not evidence

- **WHEN** `.editorconfig` configures only `CA` or `SA` diagnostic ids
- **THEN** `detect --json` SHALL NOT report `roslynator`

### Requirement: Detect keeps Roslyn analyzers as separate linters

`detect` SHALL report each Roslyn analyzer under its own name and SHALL NOT report a combined `roslyn` linter. Being a C# repository SHALL NOT by itself report any of them.

`detect` SHALL report `stylecop` from a `StyleCop.Analyzers` NuGet reference, a `stylecop.json` or `.stylecop.json` file, or a `dotnet_diagnostic.sa` key followed by a digit in `.editorconfig` or `.globalconfig`.

`detect` SHALL report `sonaranalyzer` from a `SonarAnalyzer.CSharp` NuGet reference, a `SonarLint.xml` file, or a `dotnet_diagnostic.s` key followed by a digit in `.editorconfig` or `.globalconfig`.

NuGet references and config keys SHALL be read as they are for `roslynator`.

#### Scenario: Analyzers sharing one EditorConfig

- **WHEN** one `.editorconfig` configures `RCS`, `SA` and `S` diagnostic ids
- **THEN** `detect --json` SHALL report `roslynator`, `stylecop` and `sonaranalyzer` as three linters

#### Scenario: StyleCop configured by its own file

- **WHEN** a repository has a `stylecop.json`
- **THEN** `detect --json` SHALL report `stylecop` with that path as evidence, and no other analyzer

#### Scenario: SonarAnalyzer installed as a package

- **WHEN** a project file has a `PackageReference` to `SonarAnalyzer.CSharp`
- **THEN** `detect --json` SHALL report `sonaranalyzer` with a dependency marker naming that file

#### Scenario: An SDK analyzer id is not evidence

- **WHEN** `.editorconfig` configures only `CA` diagnostic ids
- **THEN** `detect --json` SHALL NOT report any of these analyzers
12 changes: 12 additions & 0 deletions openspec/changes/archive/2026-11-06-detect-roslynator/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
## 1. Implementation

- [x] 1.1 Read https://josefpihrt.github.io/docs/roslynator/configuration for the config files and keys, and the Roslynator and NuGet package pages for package ids and the CLI command.
- [x] 1.2 Add the NuGet readers, the `C#` language mark, `configKeys`, the `roslynator`, `stylecop` and `sonaranalyzer` signals and the `bin`/`obj` pruning to `packages/cli/src/detect/scan.ts`.
- [x] 1.3 Add positive and negative cases to `packages/cli/test/detect.test.ts`.
- [x] 1.4 Describe config-key evidence in `packages/cli/src/schemas/detect.ts` and the `detect` recipe.
- [x] 1.5 Add `.changeset/detect-roslynator.md` (`patch`).

## 2. Verification

- [x] 2.1 `pnpm typecheck`, `pnpm lint`, and `pnpm --filter @taskless/cli test` pass.
- [x] 2.2 `pnpm openspec validate --strict`, then archive.
79 changes: 79 additions & 0 deletions openspec/specs/cli-detect/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,3 +296,82 @@ Optional extensions shipped with the tool, third-party plugin packages, presets,

- **WHEN** the repository configures Ruff, however extensively
- **THEN** `detect --json` SHALL report a custom-rule count of zero for Ruff

### Requirement: Detect recognizes C# and Roslynator

`detect` SHALL report `C#` among `languages` when a `.csproj` file exists in the scanned tree. A `.sln` file SHALL NOT mark C#, because a solution can hold projects in any language.

`detect` SHALL report `roslynator` when any of these exists:

- A NuGet reference to `Roslynator.Analyzers`, `Roslynator.Formatting.Analyzers`, `Roslynator.CodeAnalysis.Analyzers`, or `Roslynator.DotNet.Cli`, matched case-insensitively, as a `PackageReference` or `GlobalPackageReference` in a `.csproj`, `.props`, or `.targets` file, an entry in a `packages.config`, or a tool in a `dotnet-tools.json`.
- A key in `.editorconfig` or `.globalconfig` that begins `roslynator_`, `dotnet_diagnostic.rcs` followed by a digit, or `dotnet_analyzer_diagnostic.category-roslynator.`, matched case-insensitively.
- A `.roslynatorconfig` file.
- A root CI or hook command that runs `roslynator analyze`, `fix`, or `format`, or names `roslynator.dotnet.cli`.

A key in a comment, a reference in an XML comment, and a `PackageVersion` entry SHALL NOT be evidence: central package management can pin a package no project uses. Files under `bin` or `obj` SHALL NOT be read. A repository being C# SHALL NOT by itself report `roslynator`.

#### Scenario: A C# repository with no Roslynator

- **WHEN** a repository has a `.csproj` and nothing references or configures Roslynator
- **THEN** `detect --json` SHALL report `C#` among `languages` and SHALL NOT report `roslynator`

#### Scenario: Roslynator installed from a shared MSBuild file

- **WHEN** `Directory.Build.props` has a `PackageReference` to `Roslynator.Analyzers`
- **THEN** `detect --json` SHALL report `roslynator` with a dependency marker naming that file
- **AND** SHALL report `C#` among `languages`

#### Scenario: Roslynator configured in EditorConfig

- **WHEN** `.editorconfig` sets `dotnet_diagnostic.RCS1044.severity`
- **THEN** `detect --json` SHALL report `roslynator` with evidence naming the file and the key

#### Scenario: Roslynator run from CI

- **WHEN** a root GitHub Actions workflow runs `roslynator analyze`
- **THEN** `detect --json` SHALL report `roslynator` with evidence naming the workflow file and the command

#### Scenario: A central version pin is not evidence

- **WHEN** the only mention of Roslynator is a `PackageVersion` in `Directory.Packages.props`
- **THEN** `detect --json` SHALL NOT report `roslynator`

#### Scenario: A commented-out signal is not evidence

- **WHEN** the only mention of Roslynator is an XML comment in a project file or a comment in `.editorconfig`
- **THEN** `detect --json` SHALL NOT report `roslynator`

#### Scenario: Another analyzer's diagnostic id is not evidence

- **WHEN** `.editorconfig` configures only `CA` or `SA` diagnostic ids
- **THEN** `detect --json` SHALL NOT report `roslynator`

### Requirement: Detect keeps Roslyn analyzers as separate linters

`detect` SHALL report each Roslyn analyzer under its own name and SHALL NOT report a combined `roslyn` linter. Being a C# repository SHALL NOT by itself report any of them.

`detect` SHALL report `stylecop` from a `StyleCop.Analyzers` NuGet reference, a `stylecop.json` or `.stylecop.json` file, or a `dotnet_diagnostic.sa` key followed by a digit in `.editorconfig` or `.globalconfig`.

`detect` SHALL report `sonaranalyzer` from a `SonarAnalyzer.CSharp` NuGet reference, a `SonarLint.xml` file, or a `dotnet_diagnostic.s` key followed by a digit in `.editorconfig` or `.globalconfig`.

NuGet references and config keys SHALL be read as they are for `roslynator`.

#### Scenario: Analyzers sharing one EditorConfig

- **WHEN** one `.editorconfig` configures `RCS`, `SA` and `S` diagnostic ids
- **THEN** `detect --json` SHALL report `roslynator`, `stylecop` and `sonaranalyzer` as three linters

#### Scenario: StyleCop configured by its own file

- **WHEN** a repository has a `stylecop.json`
- **THEN** `detect --json` SHALL report `stylecop` with that path as evidence, and no other analyzer

#### Scenario: SonarAnalyzer installed as a package

- **WHEN** a project file has a `PackageReference` to `SonarAnalyzer.CSharp`
- **THEN** `detect --json` SHALL report `sonaranalyzer` with a dependency marker naming that file

#### Scenario: An SDK analyzer id is not evidence

- **WHEN** `.editorconfig` configures only `CA` diagnostic ids
- **THEN** `detect --json` SHALL NOT report any of these analyzers
3 changes: 2 additions & 1 deletion packages/cli/src/agent/detect.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Topic: detect (CLI v%(CLI_VERSION)s / topic v4)
# Topic: detect (CLI v%(CLI_VERSION)s / topic v5)

%(ONE_COMMAND_PER_CALL)s

Expand Down Expand Up @@ -54,6 +54,7 @@ routing flow reads `detect` to decide where a new rule should live.
- `linters`: each has a `name` and `evidence`: a config-file path,
a table or section marker in a manifest (`pyproject.toml
[tool.ruff]`, `Cargo.toml [lints.clippy]`, `setup.cfg [flake8]`),
a key in a shared config (`.editorconfig (roslynator_use_var)`),
a dependency marker from the language's package file, or a root CI
or hook config that runs the linter (`.github/workflows/ci.yml
(cargo clippy)`). Not every entry is a path. A linter that only
Expand Down
Loading
Loading