diff --git a/README.md b/README.md index 835d6a1..800f9bd 100644 --- a/README.md +++ b/README.md @@ -104,13 +104,18 @@ PureSharp uses Roslyn's standard `.editorconfig` mechanism for configuring diagn | Diagnostic ID | Category | Title | Default Severity | |---|---|---|---| -| **RT0001** | Purity | Static mutable field access | Error | -| **RT0002** | Purity | Non-pure method call | Error | -| **RT0003** | Purity | I/O operation | Error | -| **LVP0001** | Purity | Reassignment to immutable local variable | Error | -| **LVP0002** | Purity | Immutable local variable missing initializer | Error | -| **LVP0003** | Naming | Naming suggestion for effectively immutable variables | Warning | -| **FIF0001** | FluentIf | FluentIf chain not terminated with .Else() | Error | +| **RT0001** | Purity | Static field access in [PureMethod] | Error | +| **RT0002** | Purity | Non-pure method call in [PureMethod] | Error | +| **RT0003** | Purity | I/O operation in [PureMethod] | Error | +| **LVP0001** | Purity | Reassignment to immutable local variable prohibited | Error | +| **LVP0002** | Purity | Mandatory initialization of immutable local variable | Error | +| **LVP0003** | Naming | Suggestion to apply naming convention for immutable local variable | Warning | +| **FIF0001** | FluentIf | FluentIf chain termination check | Error | + +The complete diagnostic contract — full message text, target conditions, the v1.0 public +contract surface, ID naming and category policy, and the compatibility rules for adding +or changing a diagnostic — is defined in [`docs/DIAGNOSTICS.md`](docs/DIAGNOSTICS.md), +which is the single source of truth. ### Configuration Example diff --git a/docs/DIAGNOSTICS.md b/docs/DIAGNOSTICS.md new file mode 100644 index 0000000..4cb03d7 --- /dev/null +++ b/docs/DIAGNOSTICS.md @@ -0,0 +1,241 @@ +# PureSharp Diagnostic Catalog (SSOT) + +**This document is the single source of truth for PureSharp's diagnostic contract.** + +Where this catalog, the analyzer source, and the README disagree, this catalog defines +the intent and the other two must be corrected to match it. + +- Applies to: PureSharp v1.0.0 +- Diagnostics are configured through Roslyn's standard `.editorconfig` mechanism + (`dotnet_diagnostic..severity`). See the README for configuration examples. + +## Catalog + +Every diagnostic PureSharp can report is listed here. There are no unlisted diagnostics. + +### RT — Referential Transparency + +Emitted by `ReferentialTransparencyAnalyzer`. These apply **only inside methods marked +with `[PureMethod]`**. + +| Field | Value | +|---|---| +| **ID** | `RT0001` | +| **Category** | `Purity` | +| **Default severity** | `Error` | +| **Enabled by default** | Yes | +| **Title** | Static field access in `[PureMethod]` | +| **Message** | Cannot access static, non-readonly field `'{1}'` within `[PureMethod]` method `'{0}'` | +| **Description** | Referentially transparent methods must not access static mutable state. | +| **Target** | Field reference where the field is `static` and neither `readonly` nor `const` | + +| Field | Value | +|---|---| +| **ID** | `RT0002` | +| **Category** | `Purity` | +| **Default severity** | `Error` | +| **Enabled by default** | Yes | +| **Title** | Non-pure method call in `[PureMethod]` | +| **Message** | Cannot call non-pure method `'{1}'` within `[PureMethod]` method `'{0}'` | +| **Description** | Referentially transparent methods can only call methods marked with the `[PureMethod]` attribute or known pure types. | +| **Target** | Invocation of a method that is neither `[PureMethod]`-marked nor on a known-pure type | + +| Field | Value | +|---|---| +| **ID** | `RT0003` | +| **Category** | `Purity` | +| **Default severity** | `Error` | +| **Enabled by default** | Yes | +| **Title** | I/O operation in `[PureMethod]` | +| **Message** | Cannot perform I/O operations within `[PureMethod]` method `'{0}'` | +| **Description** | Referentially transparent methods must not perform I/O operations such as logging, file access, or network access. | +| **Target** | Invocation or property access on an I/O type | + +### LVP — Local Variable Purity + +`LVP0001` and `LVP0002` are emitted by `LocalVariablePurityAnalyzer`; +`LVP0003` is emitted by `ImmutableNamingSuggestionAnalyzer`. + +An **immutable local** is a local variable whose name begins with `_`. The single +underscore `_` is the discard symbol and is never treated as an immutable local. + +| Field | Value | +|---|---| +| **ID** | `LVP0001` | +| **Category** | `Purity` | +| **Default severity** | `Error` | +| **Enabled by default** | Yes | +| **Title** | Reassignment to immutable local variable prohibited | +| **Message** | Cannot reassign a value to immutable local variable `'{0}'` | +| **Description** | Local variables starting with an underscore (`_`) are treated as immutable, and reassignment after declaration is prohibited. | +| **Target** | Assignment, compound assignment, or increment/decrement targeting an immutable local | + +| Field | Value | +|---|---| +| **ID** | `LVP0002` | +| **Category** | `Purity` | +| **Default severity** | `Error` | +| **Enabled by default** | Yes | +| **Title** | Mandatory initialization of immutable local variable | +| **Message** | Immutable local variable `'{0}'` must be initialized at the time of declaration | +| **Description** | Local variables starting with an underscore (`_`) are treated as immutable and must be assigned an initial value at declaration. | +| **Target** | Declaration of an immutable local with no initializer | + +| Field | Value | +|---|---| +| **ID** | `LVP0003` | +| **Category** | `Naming` | +| **Default severity** | `Warning` | +| **Enabled by default** | Yes | +| **Title** | Suggestion to apply naming convention for immutable local variable | +| **Message** | Local variable `'{0}'` is effectively immutable. Consider starting its name with an underscore `'_'`. | +| **Description** | Local variables that are never reassigned can have their immutability explicitly shown by starting the name with an underscore. | +| **Target** | A local variable that is never reassigned but whose name does not begin with `_` | + +### FIF — FluentIf + +Emitted by `FluentIfAnalyzer`. + +| Field | Value | +|---|---| +| **ID** | `FIF0001` | +| **Category** | `FluentIf` | +| **Default severity** | `Error` | +| **Enabled by default** | Yes | +| **Title** | FluentIf chain termination check | +| **Message** | FluentIf chain must be terminated with `'.Else(...)'` | +| **Description** | Method chains starting with `Fluent.If()` must always end with `.Else()`. | +| **Target** | A `Fluent.If()` chain not terminated by `.Else(...)` | + +## Summary table + +| ID | Category | Default severity | Analyzer | v1.0 public contract | +|---|---|---|---|---| +| `RT0001` | Purity | Error | `ReferentialTransparencyAnalyzer` | Yes | +| `RT0002` | Purity | Error | `ReferentialTransparencyAnalyzer` | Yes | +| `RT0003` | Purity | Error | `ReferentialTransparencyAnalyzer` | Yes | +| `LVP0001` | Purity | Error | `LocalVariablePurityAnalyzer` | Yes | +| `LVP0002` | Purity | Error | `LocalVariablePurityAnalyzer` | Yes | +| `LVP0003` | Naming | Warning | `ImmutableNamingSuggestionAnalyzer` | Yes | +| `FIF0001` | FluentIf | Error | `FluentIfAnalyzer` | Yes | + +## v1.0 public contract + +All seven diagnostics above are **part of the v1.0.0 public contract**. For each of them, +the following are contract surface that consumers may depend on: + +- The **ID** string +- The **category** +- The **default severity** +- The **fact that the diagnostic exists and is enabled by default** + +The following are **not** contract surface and may change in any release: + +- The exact wording of the title, message, and description (including translations) +- The internal analyzer class that emits the diagnostic +- The precise implementation strategy used to detect the condition + +Message wording is deliberately excluded so that clarity improvements and localization +work are not breaking changes. Consumers must match on the diagnostic **ID**, never on +message text. + +## ID naming convention + +``` + +``` + +- **PREFIX** — a short uppercase prefix identifying the rule family: + | Prefix | Family | + |---|---| + | `RT` | Referential Transparency — purity of `[PureMethod]` bodies | + | `LVP` | Local Variable Purity — immutable local variable rules | + | `FIF` | FluentIf — `Fluent.If()` chain rules | +- **NNNN** — a zero-padded four-digit number, allocated sequentially within the family + starting at `0001`. + +Rules: + +- A prefix identifies the **rule family**, not the analyzer class. `LVP0003` is emitted + by `ImmutableNamingSuggestionAnalyzer` but belongs to the LVP family because it is a + local-variable purity concern. +- Numbers are **never reused**, including after a diagnostic is removed. +- A new rule family requires a new prefix and an entry in the table above. + +## Category policy + +| Category | Meaning | +|---|---| +| `Purity` | The code violates a purity or immutability guarantee. Correctness-affecting. | +| `Naming` | The code is correct, but a naming convention would express intent better. Advisory. | +| `FluentIf` | The code misuses the FluentIf API surface. Correctness-affecting. | + +Guidance: + +- `Purity` and `FluentIf` diagnostics default to `Error` — they indicate the code does + not satisfy a guarantee PureSharp is asked to enforce. +- `Naming` diagnostics default to `Warning` or lower — they are suggestions and must + never block a build by default. +- A diagnostic's category must not change once the diagnostic is part of a released + public contract; see the compatibility rules below. + +## Compatibility rules + +### Adding a new diagnostic + +Adding a diagnostic that is **enabled by default** can fail a build that previously +succeeded, so it is treated as a breaking change. + +- Allocate the next unused number in the family; never reuse a retired number. +- Add it to this catalog and to the README table in the same change. +- Add positive and negative tests in the same change. +- Prefer introducing a new default-`Error` diagnostic only in a **major** release. + In a minor release, introduce it as `Warning` or `isEnabledByDefault: false` first. + +### Changing an existing diagnostic + +| Change | Allowed in | Notes | +|---|---|---| +| Title / message / description wording | Any release | Not contract surface. Consumers must not match on text. | +| Translation updates | Any release | Same as above. | +| Widening detection (more code now reported) | Major release | Can break a previously passing build. | +| Narrowing detection (less code now reported) | Minor release | Does not break builds; may weaken a guarantee, so document it. | +| Raising default severity | Major release | Can break a previously passing build. | +| Lowering default severity | Minor release | Document the reduced guarantee. | +| Changing category | Major release | Category is contract surface; it appears in consumer tooling. | +| Changing ID | Never | Retire the old ID and allocate a new one instead. | + +### Removing a diagnostic + +- Removal is a **major**-release change. +- The retired ID stays listed in this catalog under a "Retired" heading with the version + in which it was removed, and its number is never reused. +- Consumers suppressing the retired ID in `.editorconfig` must not start failing; an + unknown `dotnet_diagnostic` entry is ignored by Roslyn, so no migration is required. + +### Change checklist + +Any change touching a `DiagnosticDescriptor` must update, in the same commit: + +- [ ] The descriptor in the analyzer source +- [ ] This catalog +- [ ] The README diagnostic table (and translated READMEs where they carry the table) +- [ ] Tests covering the new or changed behaviour + +## Retired diagnostics + +None. No diagnostic has been removed from PureSharp. + +## Consistency verification + +The three representations of the diagnostic contract must agree: + +1. `DiagnosticDescriptor` definitions in `src/PureSharp.Core/*Analyzer.cs` +2. Resource strings in `src/PureSharp.Core/Resources/DiagnosticResources.resx` + (and per-language `.resx` files) +3. This catalog and the README table + +Each descriptor must reference the resource keys belonging to **its own ID** — +`_Title`, `_MessageFormat`, and `_Description`. A descriptor referencing +another diagnostic's resource key is a defect: it surfaces the wrong explanatory text to +consumers even though the ID, category, and severity look correct. diff --git a/src/PureSharp.Core/ImmutableNamingSuggestionAnalyzer.cs b/src/PureSharp.Core/ImmutableNamingSuggestionAnalyzer.cs index accbbda..da43993 100644 --- a/src/PureSharp.Core/ImmutableNamingSuggestionAnalyzer.cs +++ b/src/PureSharp.Core/ImmutableNamingSuggestionAnalyzer.cs @@ -22,7 +22,7 @@ public class ImmutableNamingSuggestionAnalyzer : DiagnosticAnalyzer category: "Naming", defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, - description: new LocalizableResourceString(nameof(DiagnosticResources.FIF0001_Description), DiagnosticResources.ResourceManager, typeof(DiagnosticResources))); + description: new LocalizableResourceString(nameof(DiagnosticResources.LVP0003_Description), DiagnosticResources.ResourceManager, typeof(DiagnosticResources))); /// public override ImmutableArray SupportedDiagnostics =>