Digitals.Analyzers
0.10.2
dotnet add package Digitals.Analyzers --version 0.10.2
NuGet\Install-Package Digitals.Analyzers -Version 0.10.2
<PackageReference Include="Digitals.Analyzers" Version="0.10.2"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
<PackageVersion Include="Digitals.Analyzers" Version="0.10.2" />
<PackageReference Include="Digitals.Analyzers"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
paket add Digitals.Analyzers --version 0.10.2
#r "nuget: Digitals.Analyzers, 0.10.2"
#:package Digitals.Analyzers@0.10.2
#addin nuget:?package=Digitals.Analyzers&version=0.10.2
#tool nuget:?package=Digitals.Analyzers&version=0.10.2
Digitals.Analyzers
One package that brings the Digitals C# analyzer set and the shared severity policy to a .NET repo. Reference it instead of wiring up four analyzer packages and copying an .editorconfig between repos.
This package covers production code. Test projects are covered by its companion, Digitals.Testing, which brings the test stack, the test severity policy and a coverage gate. Nothing test-specific lives here.
Install
<PackageReference Include="Digitals.Analyzers" PrivateAssets="all" />
Put it once in Directory.Build.props so it covers every project, and delete any existing references to the analyzer packages listed below — duplicates cause version conflicts.
The package is marked DevelopmentDependency, so PrivateAssets="all" is what NuGet applies by default anyway. It stops the analyzers flowing on to your consumers.
Set these alongside it, in the same Directory.Build.props:
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsAsErrors>nullable</WarningsAsErrors>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
The first two are enforced: the package fails the build with DIGB002 when Nullable is not enable and DIGB003 when TreatWarningsAsErrors is not true, because without them the whole severity policy is advisory. A project mid-migration can opt out explicitly, next to a comment saying why:
<DigitalsAllowNullableDisabled>true</DigitalsAllowNullableDisabled>
<DigitalsAllowWarningsNotAsErrors>true</DigitalsAllowWarningsNotAsErrors>
The NuGet vulnerability audit is checked too. Restore reports vulnerable packages as NU1901–NU1904, and TreatWarningsAsErrors turns those into a failed build, but only while the audit runs at full strength. DIGB004 is a warning, escalated to an error under TreatWarningsAsErrors, when NuGetAudit is not true, NuGetAuditMode is not all, NuGetAuditLevel is not low, or any NU1901–NU1904 is in NoWarn or WarningsNotAsErrors. The SDK defaults NuGetAuditMode to all only for net10.0 and later, so a project on an older target sets it in Directory.Build.props:
<NuGetAuditMode>all</NuGetAuditMode>
To accept one known advisory, suppress just that one with <NuGetAuditSuppress Include="https://github.com/advisories/GHSA-..." />. Weakening the audit as a whole takes <DigitalsAllowWeakNuGetAudit>true</DigitalsAllowWeakNuGetAudit>, next to a comment saying why.
WarningsAsErrors=nullable keeps nullable-flow warnings as errors even if warnings are later relaxed via WarningsNotAsErrors. Without EnforceCodeStyleInBuild the IDExxxx rules never fire in dotnet build — they stay IDE-only. GenerateDocumentationFile is what makes IDE0005 (unused usings) run in a build.
Migrating an existing repo, including how to stage the noise: docs/ADOPTION.md.
What you get
| Analyzer package | Rules |
|---|---|
| SonarAnalyzer.CSharp | Quality and bug rules (S####) |
| Roslynator.Analyzers | Refactoring and idiom rules (RCS####) |
| Roslynator.Formatting.Analyzers | Formatting rules (RCS0###) |
| Microsoft.VisualStudio.Threading.Analyzers | Async and threading rules (VSTHRD###) |
| Microsoft.CodeAnalysis.BannedApiAnalyzers | Enforces the banned-API list (RS0030) |
Plus configuration that is injected automatically — nothing to import:
- Severity policy for the packs above, plus the naming rules, applied at
global_level 100. - Banned APIs (
RS0030) — the ambient clock:DateTime.Now,DateTime.UtcNow,DateTime.Today,DateTimeOffset.Now,DateTimeOffset.UtcNow. InjectTimeProviderinstead:TimeProvider.Systemin your composition root,FakeTimeProviderin tests. - Banned APIs (
RS0030) — the millisecondintoverloads ofTask.Delay,Thread.Sleep,new CancellationTokenSource(int)andCancelAfter.Delay(5)doesn't say its unit, so pass aTimeSpan:Task.Delay(TimeSpan.FromSeconds(5)). - Banned packages (
DIGB001, a build error) — see the table below. - Custom rules (
DT#####) — see the table below. - Sonar thresholds — a
SonarLint.xmlthat raises theS1541limit from Sonar's default of 10 to 15.
Some rules that ship disabled or info-only are escalated to warning: SYSLIB1045 (use [GeneratedRegex]), CA2254 (no interpolation in log message templates), VSTHRD100 (no async void), CA1707 (no underscores in identifiers), S6513 ([ExcludeFromCodeCoverage] must carry a Justification), CA1851 (an IEnumerable enumerated more than once), CA1310 (locale-dependent string comparison), CA1069 (duplicated enum values), S3776 (cognitive complexity over 15), S1541 (cyclomatic complexity over 15), S134 (control flow nested more than 3 deep), S104 (file over 1000 lines), CA5404/CA5405 (token validation disabled or always skipped), CA5390/CA5403 (hard-coded key or certificate), CA2248 (Enum.HasFlag with the wrong enum type), RCS1157 (a [Flags] composite covering an undefined bit), IDE0072/IDE0010 (a switch over an enum must name every member even when it has a default/_ arm — otherwise the discard silently swallows members added later; the compiler's own CS8509 only fires when there is no default arm). Others are set to suggestion so they show in the IDE without failing a build: CA2100 (raw SQL), CA1002/CA1819 (read-only collection properties), CA1021 (no out parameters), S109 (magic numbers), CA1515 (public types in an app project could be internal), RCS1079 (throws NotImplementedException), RCS1208 (an if wrapping the rest of a method body, invert it into a guard clause), S138 (method over 80 lines), S107 (more than 7 parameters), S1067 (more than 3 &&/|| in one expression).
Everything ships as warning, never error. Your repo decides how hard that bites via TreatWarningsAsErrors — which is why the package insists on it. The exceptions are the DIGB00x build errors: DIGB001 (banned package), DIGB002 (Nullable not enable), DIGB003 (TreatWarningsAsErrors not true). DIGB004 (NuGet audit weakened) is a warning like the rest, and fails the build through TreatWarningsAsErrors.
Custom rules
| ID | Rule | Do this instead |
|---|---|---|
DT00001 |
Any use of System.DateTime, including bare declarations like DateTime Foo { get; set; } |
DateOnly for a calendar date, TimeOnly for a time of day, TimeSpan for a duration, DateTimeOffset in UTC for an instant. DateTime is an interop-boundary exception |
DT00002 |
DateTimeOffset.LocalDateTime / .DateTime — both quietly hand back a DateTime |
.UtcDateTime, the sanctioned boundary conversion |
DT00003 |
new HttpClient() |
IHttpClientFactory via AddHttpClient — a client per call exhausts sockets |
DT00004 |
dynamic |
Resolve the type at compile time: generics, DI, or a typed model |
DT00006 |
double/float whose name looks like money (price, cost, fee, …). suggestion, IDE-only |
decimal |
DT00007 |
A class deriving from System.TimeProvider |
FakeTimeProvider from Microsoft.Extensions.TimeProvider.Testing in tests, TimeProvider.System in production. Deriving from the real FakeTimeProvider to add helpers is fine |
DT00008 |
A method whose return type is a nullable collection — IReadOnlyCollection<string>? GetOrders(), Task<List<Order>?>, T[]?, IAsyncEnumerable<T>? |
Return an empty collection: [], Array.Empty<T>(), ImmutableArray<T>.Empty. null and empty are the same answer to a caller, so the ? only buys a guard at every call site. string? is untouched, and so are properties and fields |
DT00009 |
Any #region — named, unnamed, nested, or inside a method body |
Delete it. A region folds code out of sight, so a type grows past its job unnoticed and the standard member order stops being visible. If a file needs sections, it needs to be two files or two methods. Generated code is exempt automatically |
DT00010 |
A bool or bool? field, property, parameter, local or bool-returning method whose name contains the word Not, No, Non, Cannot or Dont — IsNotEligible, _noConsent, cannotRenew |
Name the affirmative: IsEligible, HasConsent, CanRenew. Otherwise every caller writes !IsNotEligible. Words are matched whole, so IsNotified and HasNonce are fine. Overrides and interface implementations are reported on the declaration they satisfy, not again on each implementer |
DT00011 |
A local that is read and then reassigned to a value that refers neither to it nor to anything computed from it — result = GetPatient(); Use(result); result = GetPrescription();. suggestion, IDE-only |
Declare a second local named for what it holds. Updates (total += x, s = s.Trim(), node = node.Next), loop reassignment, if (x is null) x = …, clearing to null, and assignments on separate branches are all fine |
DT00012 |
A file whose name matches none of the top-level types it declares — class Patient in Patients.cs |
Rename the file to Patient.cs. The name is read up to the first ., { or `, so Patient.Validation.cs (partial class), Result{T}.cs and Result`1.cs all pass. A record sharing its file with its validator passes if either name matches. Files with no type, files with top-level statements, and file-scoped types are skipped |
These carry their own IDs rather than reusing RS0030 on purpose: a #pragma disabling an interop DateTime at a boundary must not also switch off the ambient-clock ban in the same region.
Banned packages
A direct PackageReference to any of these fails the build with DIGB001. Transitive dependencies are not checked. Test frameworks, assertion and mocking libraries are banned by Digitals.Testing instead, under DIGT002.
| Instead of | Use |
|---|---|
Newtonsoft.Json, Microsoft.AspNetCore.Mvc.NewtonsoftJson |
System.Text.Json |
AutoMapper and its DI extension |
Explicit mapping code (v15+ needs a commercial licence) |
EntityFramework |
Microsoft.EntityFrameworkCore |
System.Data.SqlClient |
Microsoft.Data.SqlClient |
Microsoft.EntityFrameworkCore.Proxies |
Explicit loading — no lazy loading |
If you genuinely need one, allow it explicitly with a comment saying why:
<DigitalsAllowBannedPackages>Newtonsoft.Json</DigitalsAllowBannedPackages>
Turning something off
A whole rule, repo-wide. A repo-local .editorconfig always beats the packaged policy. This is the supported opt-out, not a workaround:
[*.cs]
dotnet_diagnostic.S1234.severity = none
A Sonar threshold. Sonar reads rule parameters only from an AdditionalFiles item named exactly SonarLint.xml, never from .editorconfig. Add your own and the packaged one steps aside:
<ItemGroup>
<AdditionalFiles Include="$(MSBuildThisFileDirectory)SonarLint.xml" />
</ItemGroup>
Your file replaces the packaged thresholds as a whole, so copy across any rule you still want at the org value. The packaged file is in the package under build/config/SonarLint.xml.
One line. Legitimate exceptions exist — bootstrap logging before DI is built, EF migration defaults:
#pragma warning disable RS0030 // Serilog bootstrap, DI not built yet
Log.Information("Starting at {Now}", DateTimeOffset.UtcNow);
#pragma warning restore RS0030
Suppressions are greppable and show up in review. If they multiply in one area, that is worth a conversation rather than more pragmas.
What your repo still needs
A global config can only carry severities, so editor behaviour stays in your own .editorconfig:
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
trim_trailing_whitespace = true
[*.{props,csproj}]
indent_style = tab
indent_size = 4
tab_width = 4
[*.{cs,vb,cshtml}]
indent_style = tab
indent_size = 4
tab_width = 4
max_line_length = 120
Any path-scoped exceptions live there too ([**/Pages/**.cshtml.cs], [**/Migrations/**.cs]) — a global config has no path sections.
EF Core migrations are not marked as generated code, so analyzers treat them as hand-written. The package exempts files under any Migrations/ folder from S104 (file over 1000 lines) on its own. If you moved migrations elsewhere with dotnet ef migrations add -o, exempt that folder yourself:
[**/Data/Schema/**.cs]
dotnet_diagnostic.S104.severity = none
Seeing suggestions outside the IDE
dotnet build prints warnings and errors only. Suggestion-level diagnostics (S138, S107, S1067, S109, ...) never reach the build output, at any verbosity. To get them in a file, which is how a coding agent or a script can read them, pass DigitalsSarifDir:
dotnet build -p:DigitalsSarifDir=artifacts/sarif
Each project writes <Project>.<tfm>.sarif there, in SARIF 2.1 (JSON). A suggestion is a result with "level": "note". A relative path resolves against the directory you run dotnet build from. Filter by rule ID: AnalysisMode=Recommended adds hundreds of notes of its own. A log that is missing is regenerated on the next build, even an up-to-date one. If you set ErrorLog yourself, yours wins.
Notes
C# only in practice. Sonar C# and Roslynator are C#-only; the threading and banned-API analyzers also cover VB. In an F# project the package does nothing.
One version to review. Bumping any bundled analyzer means a new version of this package, so an upgrade is a single version bump and a single changelog to read across every repo.
Versions come from git tags via MinVer and follow SemVer. Release notes: Releases.
License
MIT — see LICENSE. This covers the code in this repository: the config files, the banned-package check and the custom DT##### analyzer. The bundled third-party analyzers keep their own licences and reach you as ordinary NuGet dependencies.
Working on the package itself: docs/MAINTAINING.md.
Learn more about Target Frameworks and .NET Standard.
-
.NETStandard 2.0
- Microsoft.CodeAnalysis.BannedApiAnalyzers (>= 3.3.4)
- Microsoft.VisualStudio.Threading.Analyzers (>= 18.7.23)
- Roslynator.Analyzers (>= 5.0.0)
- Roslynator.Formatting.Analyzers (>= 5.0.0)
- SonarAnalyzer.CSharp (>= 10.33.0.1635)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.