From a437930880dbf1eb8716082e0511acff13b24223 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 26 Aug 2026 08:36:36 -0400 Subject: [PATCH 1/3] Release 1.1.0 A minor rather than a patch. Nothing in the public API breaks, but the Runtime package gains two members - [Intercept].Realm and a four-argument DecoratorHelper.Decorate overload - and three diagnostics arrive. Both of those are minor-version events under semver, and 1.0.0 said new diagnostics would come in a minor release. Version, changelog, and the analyzer release file moving DM0017-DM0019 from unshipped to shipped. Three documentation corrections that should not ship as they stand: * The diagnostics reference said `dotnet_diagnostic.DM0005.severity = none` had no effect. It works, including for the error-severity codes - verified three ways on SDK 10.0.302: no .editorconfig gives two errors, severity=none gives zero, removing it again gives two. What .editorconfig genuinely cannot do is raise a severity, because a generator's diagnostics arrive with theirs already fixed. Both halves are now stated, and the DM0018 descriptor comment that told readers to promote it that way is corrected. * DM0010 and DM0011 were described as never appearing in `dotnet build` at any verbosity. They appear at -v detailed. * DM0012 described the opposite of what the generator emits. A condition with nothing to test cannot be false, so the guard is `if (true)` and the service registers unconditionally; the page said it never registers. DM0017, DM0018 and DM0019 are now documented on the reference page, including that the two error-severity codes fire on shapes that previously built green - so no working program breaks, but a build carrying one goes red on upgrade. Verified: 1,830 tests, 0 warnings, scripts/verify-packages.sh green for net8.0 and net10.0, docs site builds with dead-link checking on. Separately, the four applications written against 1.0.0 during the evaluation were re-run against this build: 218 of 224 pass unchanged, and all six failures are the tests that were pinning the defects this release fixes. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01R9DWh8FWTib6hRYURbypWU --- CHANGELOG.md | 134 ++++++++++++++++++ Directory.Build.props | 6 +- .../AnalyzerReleases.Shipped.md | 10 ++ .../AnalyzerReleases.Unshipped.md | 8 -- .../DependencyModuleDiagnostics.cs | 9 +- website/reference/diagnostics.md | 102 +++++++++++-- 6 files changed, 248 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbab24f..20193dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,140 @@ All notable changes to this project are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.0] - 2026-08-26 + +Four registrations that silently were not what they said they were, and three new diagnostics. Every +fix here came out of building four applications against 1.0.0 — a CLI, a worker daemon, an Avalonia +desktop app and a Native AOT tool — and each one is the same shape: a clean build, no diagnostic, and +a program that quietly did the wrong thing. + +A minor rather than a patch. Nothing in the public API breaks, but two members are added and three +diagnostics arrive, and both of those are minor-version events. + +**Upgrade note: two of the new codes are errors, on code that compiled before.** `DM0017` and +`DM0019` report shapes that built green and registered nothing, so no working program breaks — but a +build carrying one goes red on upgrade. That is the fix arriving rather than a regression. Both can be +silenced with `NoWarn` or `.editorconfig` if you need to stage the work. + +**Also worth reading if you rely on interception:** an interceptor now applies only to the +implementation it was declared on. If a sibling implementation of the same interface was being +intercepted, that stops — it was never asked for. + +### Fixed + +- **A namespace-qualified or aliased service attribute registered with the wrong lifetime.** Every + legal spelling of `[SingletonService]` was discovered, but the lifetime was read back from how the + attribute was *written* — so only a spelling literally starting with `Singleton` or `Scoped` + produced that lifetime, and everything else fell through to `Transient`. + + | Written as | Before | Now | + |---|---|---| + | `[SingletonService]` | Singleton | Singleton | + | `[SingletonServiceAttribute]` | Singleton | Singleton | + | `[DependencyModules.Runtime.Attributes.SingletonService]` | **Transient** | Singleton | + | `[global::…SingletonServiceAttribute]` | **Transient** | Singleton | + | `using Svc = …SingletonServiceAttribute;` → `[Svc]` | **Transient** | Singleton | + | `using SingletonAlias = …;` → `[SingletonAlias]` | Singleton | Singleton | + + The last two rows are the same attribute type aliased twice, behaving differently on the spelling of + the alias. `As` and `Realm` were honoured throughout; only the lifetime was lost, which is what made + it hard to see — a stateful singleton silently becoming per-resolve is a correctness bug rather than + a performance one. + + The attribute is resolved through the semantic model to decide it matched at all, so the fix costs + nothing extra: the resolved type is now carried to the lifetime rather than discarded. + +- **An interceptor was applied to every implementation of the interface.** A wrapper is generated from + one class and forwards that class's members, but it was applied to every registration of the service + type — because interception reuses the decorator rewrite, and wrapping everything behind an interface + is right for a decorator and wrong for this. + + An implementation carrying no `[Intercept]` came back wrapped in another class's wrapper. With both + implementations marked, each registration was wrapped once per generated wrapper and every + interceptor ran twice per call: doubled metrics, audit rows and retries, with nothing thrown. + + `DecoratorHelper.Decorate` gains an overload naming the implementation, and the generator uses it. + Decoration is unchanged — a decorator still wraps every registration of its service. + +- **`OnlyRealm = true` did not filter interceptors.** Services and decorators were filtered correctly; + the interception pass ignored realms entirely, so a module built to be isolated emitted applicators + for every `[Intercept]` in the assembly. Where the leaked interceptor's dependencies did not resolve + in that container it threw while building the provider; where they did, it silently wrapped a service + the module had never heard of. `[Intercept]` now takes a `Realm`, matching `[Decorator]`. + +- **A module's property defaults were overwritten by a composition that did not name them.** The + generated attribute assigned every property unconditionally unless it was declared nullable, so + `public string Label { get; set; } = "default";` became `null` — surfacing as a + `NullReferenceException` inside the module's own `ConfigureServices`. Nullability is an annotation + rather than a runtime fact, so the guard is now emitted for every property. + + A value-typed parameter is unchanged and cannot be fixed this way: `int` is `0` until assigned and + `0` is a legitimate value, so nothing can tell "not supplied" from the default. `DM0018` covers the + case where that matters. + +- **Ten diagnostics reported at the project rather than at the declaration.** `DM0002`, `DM0003`, + `DM0007`, `DM0008`, `DM0009`, `DM0011`, `DM0012`, `DM0013`, `DM0014` and `DM0015` printed as bare + `CSC : warning DM00xx:` with no file, line or column, so the IDE had nowhere to take you and the type + name in the message was the only handle. They now report at the declaration. The remaining two + location-less sites are the `DM0001` generator-failure handlers, where an exception escaped and there + genuinely is no location. + +- **`DM0005` recommended a call you had already made.** Its advice was a fixed tail appended whatever + the cause, so a convention naming a *class* as the service type — which can never match, since + conventions match through declared interfaces — was told to call `IncludeBaseClasses()`. That is the + shape `dotnet new avalonia.mvvm` ships, and the shape every MVVM project starts from. The advice is + now chosen by cause. + +### Added + +- **`DM0017`** (Error) reports a module declared inside another type. The generator completes a module + at namespace level, so a nested one produced a second, detached type of the same name while the + nested declaration never implemented `IDependencyModule` — documented as always wrong in the README + and two guides, and reported by none of them. + +- **`DM0018`** (Info) reports a module with settable properties relying on the generated + `Equals`/`GetHashCode`, which compares by type alone — so two instances carrying different values + count as the same module and the first one reached wins. Declaring your own suppresses the generated + pair, which has always worked. + + Informational because composing a parameterised module once is both common and correct; this + repository's own integration tests carry eleven such modules doing nothing wrong. + +- **`DM0019`** (Error) reports an assembly-level module attribute outside the entry point file. Those + are composed into the generated `ApplicationModule`, which is built from one compilation unit, so + written anywhere else the attribute was read by nobody. It stays quiet when nothing generated an + `ApplicationModule` — a class library, or a test project, where the test integration reads assembly + attributes at run time and a file of their own is exactly right. + +- **`[Intercept].Realm`**, matching `[Decorator].Realm`. + +- **`DecoratorHelper.Decorate(services, decoratorIdentity, decoratorFactory, implementationType)`**, + a new overload. The three-argument one is untouched, so generated code from 1.0.0 still binds. + +### Documentation + +- **The diagnostics reference was wrong about `.editorconfig`.** It said + `dotnet_diagnostic.DM0005.severity = none` had no effect. It does work, including for the + error-severity codes; what `.editorconfig` cannot do is *raise* a severity, because a generator's + diagnostics arrive with theirs already fixed. Both halves are now stated. + +- **`DM0010` and `DM0011` were described as never appearing in `dotnet build` at any verbosity.** They + appear at `-v detailed`. + +- **`DM0012` described the opposite of what the generator emits.** A condition with nothing to test + cannot be false, so the guard is `if (true)` and the service registers *unconditionally* — the page + said it never registers. + +- `DM0017`, `DM0018` and `DM0019` are documented on the diagnostics reference. + +### Unversioned + +`DependencyModules.SourceGenerator.Impl` ships generator extension points as source and sits outside +the versioning promise, as [1.0.0](#100---2026-08-16) set out. It changed: `LocationModel` moved from +`DependencyModules.Conventions.Models` to `DependencyModules.SourceGenerator.Impl.Models` so the +service, interceptor, decorator and module models can all carry one, and `ModuleEntryPointModel`'s +constructor takes a `LocationModel` as its third argument. Anything built on `.Impl` will need both. + ## [1.0.0] - 2026-08-16 The first stable release. Identical in content to `1.0.0-rc9340` below — it is the same commit diff --git a/Directory.Build.props b/Directory.Build.props index a5c703a..acf860f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -6,14 +6,14 @@ local and CI builds fall back to the values below. --> - 1.0.0 + 1.1.0 - 1.0.0.0 - 1.0.0.0 + 1.1.0.0 + 1.1.0.0 - 1.1.0.0 + + 1.0.0.0 1.1.0.0 From 5921979caa791d5764586f80f7a9c5d9240e0abf Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 26 Aug 2026 09:03:19 -0400 Subject: [PATCH 3/3] Raise DM0018 to a warning, and only for actual parameters A module's identity is genuinely ambiguous once it carries parameters, and the generator picks type-only on the developer's behalf either way - so the choice is being made regardless, and only a warning makes it visible. Informational could never have been opted into: a generator's diagnostics arrive with their severity already fixed, so nothing can raise an Info into a build. Promoting it exposed a false positive in the condition, which counted every property. A read-only property is not a parameter - a module implementing an interface with `public string Value => "A";` has nothing anyone can configure, and the generated attribute never assigns it. The condition now mirrors exactly what ModuleAttributeWriter carries across: settable and non-static. That took the diagnostic from ten modules in this repository's own integration tests to three, and the seven that dropped out were all get-only properties implementing an interface. The three that remain are true positives and now declare their identity rather than being exempted, one of each shape: ParameterizedModule by its values, ArrayParameterModule by its type stated explicitly, DefaultedParameterModule by its values because two composers carry different ones. Zero warnings again. Seven tests added. Three of them fail against the old condition. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01R9DWh8FWTib6hRYURbypWU --- CHANGELOG.md | 18 ++- .../ParameterizedModule.cs | 7 ++ .../ArrayParameterModule.cs | 6 + .../DefaultedParameterModule.cs | 6 + .../AnalyzerReleases.Shipped.md | 2 +- .../DependencyModuleDiagnostics.cs | 19 ++- .../DependencyModuleWriter.cs | 11 +- .../ModuleEqualityDiagnosticTests.cs | 108 ++++++++++++++++++ website/reference/diagnostics.md | 49 +++++--- 9 files changed, 194 insertions(+), 32 deletions(-) create mode 100644 tests/DependencyModules.Tests/GeneratorTests/ModuleEqualityDiagnosticTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 92dcbb0..1527825 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -96,13 +96,21 @@ intercepted, that stops — it was never asked for. nested declaration never implemented `IDependencyModule` — documented as always wrong in the README and two guides, and reported by none of them. -- **`DM0018`** (Info) reports a module with settable properties relying on the generated - `Equals`/`GetHashCode`, which compares by type alone — so two instances carrying different values - count as the same module and the first one reached wins. Declaring your own suppresses the generated +- **`DM0018`** reports a module with parameters relying on the generated `Equals`/`GetHashCode`, + which compares by type alone — so two instances carrying different values count as the same module, + the first one reached wins, and the other is discarded with nothing said. Load a + `[CacheModule(SizeLimit = 10)]` feature and a `[CacheModule(SizeLimit = 999)]` feature together and + one `CacheSettings` arrives, not two. + + The generator has to choose an identity either way and picks type-only. This reports that it chose, + so the choice becomes the developer's — and both answers are legitimate, whether that is "identity + is the values" or "identity is the type, and I meant it". Declaring either suppresses the generated pair, which has always worked. - Informational because composing a parameterised module once is both common and correct; this - repository's own integration tests carry eleven such modules doing nothing wrong. + Only **settable, non-static** properties count. A read-only property is not a parameter: a module + implementing an interface with `public string Value => "A";` has nothing to configure and is not + reported. The three parameterised modules in this repository's own integration tests now declare + their identity rather than being exempted, one of each kind. - **`DM0019`** (Error) reports an assembly-level module attribute outside the entry point file. Those are composed into the generated `ApplicationModule`, which is built from one compilation unit, so diff --git a/integ-tests/SecondarySutProject/ParameterizedModules/ParameterizedModule.cs b/integ-tests/SecondarySutProject/ParameterizedModules/ParameterizedModule.cs index 8590ea4..3cd4932 100644 --- a/integ-tests/SecondarySutProject/ParameterizedModules/ParameterizedModule.cs +++ b/integ-tests/SecondarySutProject/ParameterizedModules/ParameterizedModule.cs @@ -20,4 +20,11 @@ public ParameterizedModule(string a, int b) { public void ConfigureServices(IServiceCollection services) { services.AddTransient(_ => new SomeRuntimeDependency(_a, _b, C!)); } + + // Declared rather than generated, which is what DM0018 asks for: this module carries + // configuration, so two instances are the same module only when they carry the same values. + public override bool Equals(object? obj) => + obj is ParameterizedModule other && other._a == _a && other._b == _b && other.C == C; + + public override int GetHashCode() => HashCode.Combine(_a, _b, C); } \ No newline at end of file diff --git a/integ-tests/SutProject.Tests/ParameterizedModuleTests/ArrayParameterModule.cs b/integ-tests/SutProject.Tests/ParameterizedModuleTests/ArrayParameterModule.cs index 7955ac2..cfe6677 100644 --- a/integ-tests/SutProject.Tests/ParameterizedModuleTests/ArrayParameterModule.cs +++ b/integ-tests/SutProject.Tests/ParameterizedModuleTests/ArrayParameterModule.cs @@ -7,6 +7,12 @@ public partial class ArrayParameterModule { public string[]? ArrayParameter { get; set; } = []; public Type? TypeValue { get; set; } + + // The other answer DM0018 accepts: this fixture is composed once, so type-only identity is + // what is wanted. Declaring it says so, rather than leaving the generator to assume it. + public override bool Equals(object? obj) => obj is ArrayParameterModule; + + public override int GetHashCode() => typeof(ArrayParameterModule).GetHashCode(); } diff --git a/integ-tests/SutProject.Tests/ParameterizedModuleTests/DefaultedParameterModule.cs b/integ-tests/SutProject.Tests/ParameterizedModuleTests/DefaultedParameterModule.cs index 88c9fe6..02913d9 100644 --- a/integ-tests/SutProject.Tests/ParameterizedModuleTests/DefaultedParameterModule.cs +++ b/integ-tests/SutProject.Tests/ParameterizedModuleTests/DefaultedParameterModule.cs @@ -33,6 +33,12 @@ public partial class DefaultedParameterModule : IServiceCollectionConfiguration public void ConfigureServices(IServiceCollection services) { services.AddSingleton(new DefaultedParameterValues(Label, Size)); } + + // Two composers below carry different values, so identity is the values rather than the type. + public override bool Equals(object? obj) => + obj is DefaultedParameterModule other && other.Label == Label && other.Size == Size; + + public override int GetHashCode() => HashCode.Combine(Label, Size); } /// Composes the module above without naming either parameter. diff --git a/src/DependencyModules.SourceGenerator.Impl/AnalyzerReleases.Shipped.md b/src/DependencyModules.SourceGenerator.Impl/AnalyzerReleases.Shipped.md index 586b95d..b3efbd0 100644 --- a/src/DependencyModules.SourceGenerator.Impl/AnalyzerReleases.Shipped.md +++ b/src/DependencyModules.SourceGenerator.Impl/AnalyzerReleases.Shipped.md @@ -31,5 +31,5 @@ DM0016 | DependencyModules | Warning | An assembly-level module attribute's name Rule ID | Category | Severity | Notes --------|----------|----------|------- DM0017 | DependencyModules | Error | A dependency module is declared inside another type. -DM0018 | DependencyModules | Info | A module with settable properties relies on generated equality. +DM0018 | DependencyModules | Warning | A module with settable properties relies on generated equality. DM0019 | DependencyModules | Error | An assembly-level module attribute is outside the entry point file. diff --git a/src/DependencyModules.SourceGenerator.Impl/DependencyModuleDiagnostics.cs b/src/DependencyModules.SourceGenerator.Impl/DependencyModuleDiagnostics.cs index 318bc66..d02631b 100644 --- a/src/DependencyModules.SourceGenerator.Impl/DependencyModuleDiagnostics.cs +++ b/src/DependencyModules.SourceGenerator.Impl/DependencyModuleDiagnostics.cs @@ -124,17 +124,16 @@ public static class DependencyModuleDiagnostics { "and the first one reached wins. Declare Equals and GetHashCode on '{0}' to say which " + "instances are the same.", category: Category, - // Info, not Warning. Dedupe-by-type is correct for the common case — a parameterised module - // composed once — and this repo's own integration tests carry eleven such modules that are - // not doing anything wrong. Only a module reached twice with different values is actually - // bitten, and that is not decidable here. + // A warning rather than informational. The identity of a module with parameters is genuinely + // ambiguous, and the generator picks type-only on the developer's behalf — so the choice is + // being made either way, and only one of the two makes it visible. Informational would not: + // a generator's diagnostics arrive with their severity already fixed and Roslyn's + // .editorconfig mapping applies to analyzer diagnostics, so an Info here could never be + // raised into a build by anyone who wanted it enforced. // - // Note that this cannot be promoted per project. A generator's diagnostics arrive with their - // severity already fixed, and Roslyn's .editorconfig severity mapping applies to analyzer - // diagnostics — so dotnet_diagnostic.DM0018.severity = warning has no effect, and - // WarningsAsErrors only promotes something that is already a warning. Silencing does work, - // through NoWarn or .editorconfig; raising does not. - defaultSeverity: DiagnosticSeverity.Info, + // Silencing still works per project, through NoWarn or .editorconfig, for a codebase whose + // parameterised modules are each composed once. + defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); /// diff --git a/src/DependencyModules.SourceGenerator.Impl/DependencyModuleWriter.cs b/src/DependencyModules.SourceGenerator.Impl/DependencyModuleWriter.cs index 4345f0c..423da9e 100644 --- a/src/DependencyModules.SourceGenerator.Impl/DependencyModuleWriter.cs +++ b/src/DependencyModules.SourceGenerator.Impl/DependencyModuleWriter.cs @@ -93,9 +93,14 @@ private void ProcessEntryPoint( return; } - // Only when the generator is supplying equality. A module that declares its own Equals has - // already answered the question this asks about. - if (entryPointModel.PropertyInfoModels.Count > 0 && + // Settable and non-static, matching exactly what ModuleAttributeWriter carries across from + // the generated attribute. A read-only property is not a parameter — a module implementing + // an interface with `public string Value => "A";` has nothing anyone can configure, and + // asking it to declare Equals would be noise about a decision it never faces. + // + // Only when the generator is supplying equality, too: a module that declares its own Equals + // has already answered the question this asks about. + if (entryPointModel.PropertyInfoModels.Any(p => !p.IsReadOnly && !p.IsStatic) && entryPointModel.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.ShouldImplementEquals)) { context.ReportDiagnostic( Diagnostic.Create( diff --git a/tests/DependencyModules.Tests/GeneratorTests/ModuleEqualityDiagnosticTests.cs b/tests/DependencyModules.Tests/GeneratorTests/ModuleEqualityDiagnosticTests.cs new file mode 100644 index 0000000..f318bff --- /dev/null +++ b/tests/DependencyModules.Tests/GeneratorTests/ModuleEqualityDiagnosticTests.cs @@ -0,0 +1,108 @@ +using System.Linq; +using DependencyModules.Tests.Infrastructure; +using Xunit; + +namespace DependencyModules.Tests.GeneratorTests; + +/// +/// DM0018. Modules de-duplicate by type, which is right for a module with no parameters and wrong +/// for one with them: two instances carrying different values are the same module by that rule, so +/// the first reached wins and the other is discarded with nothing said. +/// +/// The generator has to pick an identity either way. This reports that it picked, so the choice is +/// the developer's. +/// +public class ModuleEqualityDiagnosticTests { + + [Fact] + public void ASettableProperty_IsReported() { + var result = Run("public int SizeLimit { get; set; }"); + + var diagnostic = Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0018"); + + Assert.Contains("TestModule", diagnostic.GetMessage()); + } + + /// + /// A read-only property is not a parameter. A module implementing an interface with + /// public string Value => "A"; has nothing anyone can configure, and the generated + /// attribute never assigns it — so there is no identity question to answer and nothing to report. + /// + [Fact] + public void AnExpressionBodiedProperty_IsNotReported() { + var result = Run("""public string Value => "A";"""); + + Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0018"); + } + + [Fact] + public void AGetOnlyProperty_IsNotReported() { + var result = Run("public string Value { get; } = \"A\";"); + + Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0018"); + } + + [Fact] + public void AStaticProperty_IsNotReported() { + var result = Run("public static int Shared { get; set; }"); + + Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0018"); + } + + [Fact] + public void NoPropertiesAtAll_IsNotReported() { + var result = Run(""); + + Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0018"); + } + + /// + /// Declaring equality is the whole point of the diagnostic, so having done it must silence it. + /// The generator already stands aside when a module declares its own Equals. + /// + [Fact] + public void DeclaringEquals_SilencesIt() { + var result = Run( + """ + public int SizeLimit { get; set; } + + public override bool Equals(object? obj) => + obj is TestModule other && other.SizeLimit == SizeLimit; + + public override int GetHashCode() => SizeLimit; + """); + + Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0018"); + } + + /// A record gets its equality from the language, so it never faces the question. + [Fact] + public void ARecordModule_IsNotReported() { + var result = GeneratorTestHarness.Run( + """ + using DependencyModules.Runtime.Attributes; + + namespace TestNamespace; + + [DependencyModule] + public partial record TestModule { + public int SizeLimit { get; set; } + } + """); + + Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0018"); + } + + private static GeneratorResult Run(string body) => + GeneratorTestHarness.Run( + $$""" + using DependencyModules.Runtime.Attributes; + + namespace TestNamespace; + + [DependencyModule] + public partial class TestModule { + {{body}} + } + """); +} diff --git a/website/reference/diagnostics.md b/website/reference/diagnostics.md index 08f5403..ea7ee83 100644 --- a/website/reference/diagnostics.md +++ b/website/reference/diagnostics.md @@ -24,12 +24,12 @@ dotnet_diagnostic.DM0019.severity = none **Raising a severity is the part `.editorconfig` cannot do.** A generator's diagnostics reach the compilation with their severity already fixed, and Roslyn's severity *mapping* applies to analyzer -diagnostics — so `dotnet_diagnostic.DM0018.severity = warning` will not promote an informational code +diagnostics — so `dotnet_diagnostic.DM0010.severity = warning` will not promote an informational code into the build. `WarningsAsErrors` promotes a warning to an error; nothing promotes an `Info`. -`DM0010`, `DM0011` and `DM0018` are informational and exist to make a registration or a decision -visible at the class. They appear in the IDE, and in `dotnet build` only at `-v detailed` or higher — -not at the default verbosity. The rest are worth reading. +`DM0010` and `DM0011` are informational and exist to make a registration visible at the class. They +appear in the IDE, and in `dotnet build` only at `-v detailed` or higher — not at the default +verbosity. The rest are worth reading. | Code | Severity | Meaning | |---|---|---| @@ -50,7 +50,7 @@ not at the default verbosity. The rest are worth reading. | [DM0015](#dm0015) | Warning | An interceptor does not apply to every member | | [DM0016](#dm0016) | Warning | An assembly-level module attribute's namespace is not imported | | [DM0017](#dm0017) | Error | A module is declared inside another type | -| [DM0018](#dm0018) | Info | A module with properties relies on generated equality | +| [DM0018](#dm0018) | Warning | A module with parameters relies on generated equality | | [DM0019](#dm0019) | Error | An assembly-level module attribute is outside the entry point file | ## DM0001 {#dm0001} @@ -289,7 +289,7 @@ See [Modules](/guide/modules) and [Troubleshooting](/guide/troubleshooting). ## DM0018 {#dm0018} -**A module with settable properties relies on generated equality.** +**A module with parameters relies on generated equality.** Modules de-duplicate by type, which is what stops a module reached twice from registering everything twice. A module carrying parameters is the case that rule does not fit: two instances holding @@ -298,17 +298,40 @@ silently. ```csharp [DependencyModule] -public partial class CacheModule { - public int SizeLimit { get; set; } // DM0018 +public partial class CacheModule : IServiceCollectionConfiguration { + public int SizeLimit { get; set; } // DM0018 + public void ConfigureServices(IServiceCollection services) => + services.AddSingleton(new CacheSettings(SizeLimit)); } + +[DependencyModule] [CacheModule(SizeLimit = 10)] public partial class SmallCacheFeature; +[DependencyModule] [CacheModule(SizeLimit = 999)] public partial class BigCacheFeature; +``` + +Load both features and one `CacheSettings` arrives, not two — whichever was reached first, with no +error and no duplicate to notice. + +The generator has to choose an identity for you, and type-only is the choice it makes. Declaring your +own `Equals` and `GetHashCode` suppresses the generated pair and says which you meant. Both answers +are legitimate: + +```csharp +// identity is the values: both configurations survive +public override bool Equals(object? obj) => + obj is CacheModule other && other.SizeLimit == SizeLimit; +public override int GetHashCode() => SizeLimit; + +// identity is the type: one wins, and that is intended +public override bool Equals(object? obj) => obj is CacheModule; +public override int GetHashCode() => typeof(CacheModule).GetHashCode(); ``` -Declaring your own `Equals` and `GetHashCode` suppresses the generated pair and makes the intent -explicit — whether that is "these are the same module regardless of parameters" or "they are not". +Only **settable, non-static** properties count. A read-only property is not a parameter — a module +implementing an interface with `public string Value => "A";` has nothing to configure and is not +reported. -Informational rather than a warning, because composing a parameterised module once is both common and -correct, and only a module reached *twice with differing values* is actually bitten. It cannot be -promoted through `.editorconfig`; see the note at the top of this page. +Silence it per project with `NoWarn` or `.editorconfig` if every parameterised module in the codebase +is composed once. See [Modules](/guide/modules#parameters).