diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index d51c9f0..9edd0c0 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -17,7 +17,7 @@ "rollForward": false }, "purview.build": { - "version": "0.5.4", + "version": "0.7.0", "commands": [ "purview-build" ], diff --git a/.editorconfig b/.editorconfig index 3476e6e..b511789 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,3 +1,23 @@ +# Purview.BuildSdk 1.1.0 - managed file, do not edit +# +# This file is written by Purview.BuildSdk and is replaced whenever the SDK version changes. The version +# on the first line is how the SDK recognises a file it owns: a .editorconfig without that marker is +# yours, and is never touched. +# +# To add your own rules, create a second .editorconfig in a subdirectory - `src/.editorconfig` is the +# usual choice - without `root = true`. EditorConfig applies the nearest file last, so your settings win +# over these, and nothing you write is lost when this file is updated. +# +# To stop the SDK updating this file, set one of: +# false keep this file as it is +# Never opt out of all bootstrapping +# false +# +# Deleting the first line also works: without the marker the SDK treats the file as yours. You then own +# every rule in it, including the ones a future SDK version would have fixed. +# +# See https://github.com/purview-dev/build-sdk#editorconfig for the full description. + root = true # Base Configuration - General Settings, File Formatting, and Naming Conventions @@ -360,12 +380,8 @@ dotnet_style_require_accessibility_modifiers = omit_if_default:warning # Code-block preferences csharp_prefer_braces = when_multiline:suggestion csharp_prefer_simple_using_statement = false:silent -csharp_prefer_system_threading_lock = true dotnet_style_prefer_collection_expression = true:suggestion -# Enable foreach explicit cast preference -dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed:warning - # Prefer System.Threading.Lock csharp_prefer_system_threading_lock = true:warning @@ -497,7 +513,7 @@ dotnet_style_prefer_auto_properties = true:warning dotnet_style_prefer_compound_assignment = true:warning dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion dotnet_style_prefer_conditional_expression_over_return = true:suggestion -dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed:warning dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning dotnet_style_prefer_inferred_tuple_names = true:warning dotnet_style_prefer_simplified_boolean_expressions = false:silent @@ -2676,7 +2692,8 @@ dotnet_diagnostic.PDS0004.severity = none [**/Extensions/**.{cs,vb}] # Files under the Extensions/ namespace-reset convention must not force developers to add pragmas; -# suppress the namespace-conflict diagnostics that arise from declaring framework-style namespaces. +# suppress only the namespace-conflict diagnostics that arise from declaring framework-style +# namespaces. Accessibility/design rules (for example CA1034) are deliberately not suppressed here. dotnet_diagnostic.IDE0130.severity = none dotnet_diagnostic.CA1724.severity = none dotnet_diagnostic.CS0436.severity = none diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5f4c7eb..3a95f9a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,13 @@ on: push: branches: [main] +permissions: + contents: write + # The shared release workflow pushes the same packages to nuget.org and, when + # PURVIEW_PUBLISH_GITHUB_PACKAGES=true, to GitHub Packages. A reusable workflow cannot grant + # more than its caller, so the permission is declared here. + packages: write + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: false diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b7af299 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,140 @@ +# Changelog + +All notable changes to `Purview.SourceGeneratorFramework` and its testing packages. + +The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versions come from the +`version` field in `package.json`, which is the single source of truth: a merge to `main` publishes +that version to nuget.org. + +This file was reconstructed from the conventional-commit history at 1.1.1, so entries before that +release summarise what the commits recorded rather than what was written at the time. + +## [1.2.0] - 2026-10-21 + +### Fixed + +- `TypeLibraryModelLibrary` held an `INamedTypeSymbol` in its pipeline node value and combined it with + `CompilationProvider`. A symbol compares by reference, so the stage reported `Modified` on every new + compilation and rebuilt its target on every keystroke. The whole target is now built inside the + `ForAttributeWithMetadataName` transform, and the framework `PurviewTypeLibrary` shape is memoised per + compilation through a `ConditionalWeakTable` — preserving the once-per-compilation walk without a + pipeline node holding the compilation. The `GetFrameworkTypeLibraryTree` stage no longer exists. +- `AttributeDataModelLibrary`'s type-library spec provider returned the symbol and projected it in a + following `Select`; the read now happens inside the transform, so the node value is the + value-equatable `TypeLibrarySpecData`. +- `AvoidSymbolsInPipelineModelAnalyzer` (`PSGFR33`) matched banned types by exact display string, so + `INamedTypeSymbol` never matched `ISymbol` and `CSharpCompilation` never matched `Compilation`. It now + walks base types and implemented interfaces, which is what real pipeline code names. +- `AttributeDataModelGenerator` had three `_ => throw new InvalidOperationException(...)` arms in its emit + helpers — exactly the `PSGFR44` shape, and reachable by a consumer only as a `CS8785` that would cost them + every file the generator produces. All three were unreachable by validation (`ADM0008` is an error unless a + type-argument property is a `TypeIdentity`), so each now falls through to the equivalent defined arm with a + comment naming the validation that makes it unreachable. No behaviour changes for any defined input. +- Corrected the `Attirbutes` spelling in `GeneratorTypeLibrary` (internal). +- Removed a committed zero-byte `AttributeDataGenerationModel.cs`. + +### Added + +- **`ComponentGuard` / `ComponentFailure` — an escaping exception becomes a diagnostic.** Covers every + component kind the framework supports: source generators, analyzers, code fixes, refactorings and + diagnostic suppressors. `Run` reports the failure where there is a channel to report on (a generator's + `SourceProductionContext`, an analyzer's `ReportDiagnostic`); `TryRun` returns whether the body succeeded + where there is not (suppressors, code fixes — the correct behaviour there is to suppress nothing and offer + no fix). Cancellation is never caught: an `OperationCanceledException` raised while the supplied token is + cancelled propagates untouched, because Roslyn cancels analysis on every keystroke. + + The reported diagnostic defaults entirely from the component, because it belongs to the consumer rather + than to the framework: the id is derived from the component's namespace (`Acme.Widgets.Generators` → + `AWG0001`), the title from the component kind, the category from the root namespace, and the severity is + `Warning`. Every part is overridable through `ComponentFailureOptions`, including an opt-in stack trace. + See [`docs/wiki/Component-Failures.md`](docs/wiki/Component-Failures.md). + + The framework's own `RegisterSourceOutput` now runs the emit callback through the guard, so a generator + built on it is covered without changing a line. Its signature is unchanged — the overrides are declared on + the new `GenerationSettings.FailureOptions`, alongside everything else a generator says about itself. + Adding an optional parameter to `RegisterSourceOutput` instead would have removed the four-parameter + signature from metadata and broken every assembly already compiled against it; package validation caught + that before it shipped. +- **`PSGFR44` — source generators must not throw.** An exception escaping a generator is reported as + `CS8785` in every consuming build and IDE, and the generator produces no output at all for that + compilation, so the consumer also gets every "type or namespace not found" error that follows — with a + stack trace through the generator's internals rather than a diagnostic on their own code. The rule flags a + `throw` anywhere inside a type implementing `IIncrementalGenerator` (the `Initialize` body, the pipeline + lambdas, and the emit helpers hanging off it) and in any lambda handed to a pipeline method. Report a + diagnostic instead: `GeneratorResult` with a `ReportableDiagnostic`, or + `SourceProductionContext.ReportDiagnostic`. Cancellation is unaffected, because + `CancellationToken.ThrowIfCancellationRequested()` is a method call rather than a `throw`. +- `PSGFR33` now also inspects `IncrementalValue(s)Provider` type arguments, so a transform returning a + symbol or a tuple carrying one is reported. Previously the rule only walked the members of named model + types, which is a shape the framework's own generators did not use — so the rule could not see them. + Findings are collected during the compilation and reported at compilation end, because the descriptor + carries `WellKnownDiagnosticTags.CompilationEnd` and Roslyn drops such diagnostics when they are + reported from an operation action. +- `GeneratorCompilationRetentionTests`, which pins that a generator's pipeline nodes do not keep earlier + compilations reachable. + +### Changed + +- **Breaking for test authors.** `SourceGeneratorTestRunner.RunIncremental` now builds a fresh + `Compilation` for every run, matching what an IDE produces on every keystroke. It previously reused one + instance across runs with identical sources, which let a stage holding a symbol report `Cached` on + reference equality alone and hid exactly the regression these tests exist to catch. Set + `SourceGeneratorTestOptions.ReuseCompilationForIdenticalSources` to restore the old behaviour where a + scenario genuinely needs it. Assert the generator's own tracked stages with `StepIsCached` rather than + `AllStepsCachedOrUnchanged`, which also covers Roslyn's internal steps. + +### Documentation + +- `docs/wiki/Step-Cache-Tests.md` documents the fresh-compilation default and the opt-out. + +## [1.1.1] + +### Fixed + +- Incorrect identification of a drive result. + +### Changed + +- Migrated authored agent skills to the repository-root `skills/` folder and the skills CLI. + +## [1.1.0] + +### Added + +- `PSGFR42` for undisposed test-harness results. + +### Performance + +- Various caching and expensive-operation fixes across the framework. +- `MetadataReference` instances shared across test compilations. + +### Fixed + +- The logging sink is registered for incremental runs, and entries stay ordered. +- A scoped `CodeQuery` no longer repeats matches once per tree. +- The `TypeIdentity` symbol cache is keyed on the symbol. + +## [1.0.0] + +### Added + +- ILRepack-based merge so a component ships a self-contained analyzer and two generators cannot collide + on assembly identity in `VBCSCompiler`. +- Expanded type library, `EnumValue`/`EnumValues` support, operator method names. + +### Fixed + +- A component's own internals grants survive the merge. +- Internaliser incorrectly matching tested types. +- Source-generator resolution on pack, and framework types leaking from a component. +- XML cref fixers. + +### Changed + +- Removed the separate `Shared` assembly; its sources are link-compiled into the framework. + +[Unreleased]: https://github.com/purview-dev/sourcegenerator-framework/compare/v1.2.0...HEAD +[1.2.0]: https://github.com/purview-dev/sourcegenerator-framework/compare/v1.1.1...v1.2.0 +[1.1.1]: https://github.com/purview-dev/sourcegenerator-framework/compare/v1.1.0...v1.1.1 +[1.1.0]: https://github.com/purview-dev/sourcegenerator-framework/compare/v1.0.0...v1.1.0 +[1.0.0]: https://github.com/purview-dev/sourcegenerator-framework/releases/tag/v1.0.0 diff --git a/Directory.Packages.props b/Directory.Packages.props index 331a47f..05cce36 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,9 +3,7 @@ true [5.9.0,) [5.9.0,) - - [1.75.0,) + [1.76.0,) @@ -25,7 +23,6 @@ - - + diff --git a/docs/wiki/Analyzers.md b/docs/wiki/Analyzers.md index b436181..77c3570 100644 --- a/docs/wiki/Analyzers.md +++ b/docs/wiki/Analyzers.md @@ -74,6 +74,7 @@ Opt out with `PurviewSourceGeneratorFrameworkAnalyzerValidation=false` (`PRSGD00 | `PSGFR40` | In Roslyn components (`IsRoslynComponent=true`), reference SGF types as inline code (`Type`) instead of a `cref`: copied documentation must not depend on cref resolution. | | `PSGFR41` | In Roslyn components whose framework implementation is merged, a public member (or generic constraint) whose signature references an SGF type: the merge internalizes every SGF type, so the signature is left referring to an internal type. Make the member or its declaring type non-public. | | `PSGFR42` | A disposable Testing-harness result (`DriverRunResult`) is produced and never disposed, leaking the collectible `AssemblyLoadContext` and `MetadataLoadContext` of a compiled run. Reported as a `warning` when the run syntactically compiles to an assembly (a `Compile` options member or `CompileToAssembly = true`), otherwise as `info`. Awaiting an assertion, or calling a helper that is handed the result — an extension on it, or a method taking it as a parameter — is not a production: both hand the same instance back, so only a task-like await (the shape the harness runners return) or a genuine producer is reported. | +| `PSGFR44` | A `throw` in code that runs during source generation. An exception escaping a generator is reported as `CS8785` in every consuming build and IDE, and the generator produces no output at all for that compilation — so the consumer also sees every "type or namespace not found" error that follows from the missing files, with a stack trace through the generator's internals rather than a diagnostic on their own code. Report a diagnostic instead: carry it on `GeneratorResult` as a `ReportableDiagnostic`, or call `SourceProductionContext.ReportDiagnostic`. Two scopes are checked — anything inside a type implementing `IIncrementalGenerator` (the `Initialize` body, the pipeline lambdas, and the emit helpers hanging off it), and any lambda handed to a pipeline method wherever it is written. Cancellation is unaffected: `CancellationToken.ThrowIfCancellationRequested()` is a method call, not a `throw`, and Roslyn expects that exception to propagate. | ## Type-library and attribute-model diagnostics diff --git a/docs/wiki/Component-Failures.md b/docs/wiki/Component-Failures.md new file mode 100644 index 0000000..6a342bc --- /dev/null +++ b/docs/wiki/Component-Failures.md @@ -0,0 +1,150 @@ +# Component failures + +A Roslyn component must never let an exception escape. This page covers what it costs when one does, the +guard the framework provides, and how to configure the diagnostic it reports. + +`PSGFR44` enforces the rule at authoring time; `ComponentGuard` is how you satisfy it at runtime. + +## What an escaping exception costs + +The consequence depends on the component, and none of them are good: + +| Component | What the consumer sees | +| -- | -- | +| Source generator | `CS8785`, **and no generated files at all** for that compilation — so every type the generator was meant to produce is also reported missing. | +| Analyzer | `AD0001`, with a stack trace through code they do not own. | +| Code fix / refactoring | The light-bulb fails; the action is not offered or not applied. | +| Diagnostic suppressor | It silently stops suppressing, and the diagnostics it was hiding reappear with no explanation. | + +In every case the failure is attributed to the consumer's build rather than to the component, and the +information they are given is not something they can act on. + +## The guard + +`ComponentGuard` runs a body and converts an unhandled exception into a diagnostic. Not every component can +report a diagnostic, so it offers two shapes rather than pretending to a uniform one. + +### Source generators — reported for you + +Nothing to do. The framework's `RegisterSourceOutput` already wraps the emit callback: + +```csharp +context.RegisterSourceOutput( + targets, + generationContext, + static (spc, target, ctx) => + { + // An exception here is reported as a diagnostic, not CS8785. + var writer = ctx.CreateCodeWriter(); + writer.Line($"partial class {target.Name} {{ }}"); + spc.AddSource($"{target.Name}.g.cs", writer.ToString()); + }); +``` + +For a generator that registers its own output, call the guard directly: + +```csharp +context.RegisterSourceOutput( + targets, + (spc, target) => ComponentGuard.Run(spc, () => Emit(spc, target))); +``` + +### Analyzers — report through the analysis context + +An analyzer must declare every diagnostic it can report in `SupportedDiagnostics`, so declare the failure +descriptor alongside your rules. `ComponentFailure.For()` is cached, so the instance you declare is the +instance the guard reports: + +```csharp +public override ImmutableArray SupportedDiagnostics => + [Rule, ComponentFailure.For()]; + +static void Analyze(SyntaxNodeAnalysisContext context) => + ComponentGuard.Run( + context.ReportDiagnostic, + () => AnalyzeCore(context), + cancellationToken: context.CancellationToken); +``` + +Reporting an id that is not in `SupportedDiagnostics` makes Roslyn throw — which is the failure this guard +exists to prevent, so do not skip the declaration. + +### Suppressors, code fixes and refactorings — no channel to report on + +None of these can report a diagnostic. The correct behaviour is to do nothing: suppress nothing, offer no +fix. `TryRun` says whether the body succeeded: + +```csharp +public override void ReportSuppressions(SuppressionAnalysisContext context) +{ + if (!ComponentGuard.TryRun(() => SuppressCore(context), out var failure)) + { + // Nothing was suppressed. Log it if you have a logger; there is nowhere to report it. + return; + } +} +``` + +## Cancellation is never caught + +Roslyn cancels analysis on every keystroke. An `OperationCanceledException` raised while the supplied token +is cancelled propagates untouched — swallowing it would turn a normal cancellation into a reported defect +and hide from Roslyn that the work was abandoned. + +An `OperationCanceledException` thrown when nothing was cancelled is an ordinary defect and *is* reported. + +## The default diagnostic + +Every part of the diagnostic defaults from the component itself, because the diagnostic belongs to the +consumer rather than to the framework — it appears in their build, and they decide whether to suppress it. + +| Part | Default | +| -- | -- | +| Id | The initial of each namespace segment, upper cased, capped at four, plus `0001`. `Acme.Widgets.Generators` → `AWG0001`. A single-segment namespace uses its first three letters; a type with no namespace falls back to `GEN`. | +| Title | `Source generator failed`, `Analyzer failed`, `Code fix failed`, … — resolved from the base type or interface the component implements. | +| Message | The component name, the exception type, and the exception message. | +| Category | The component's root namespace, so failures group under the consumer's own product. | +| Severity | `Warning`. A component that failed has produced nothing; failing the build on top of that rarely helps. | + +The derived id is stable for a given namespace, which is what matters — a consumer suppressing it in an +`.editorconfig` must not find it renamed by an unrelated change. It is **not** guaranteed unique across two +assemblies sharing a namespace shape; set the id explicitly when that matters. + +## Overriding the details + +Pass `ComponentFailureOptions` to override any part: + +```csharp +static readonly ComponentFailureOptions Failure = new() +{ + Id = "ACME9999", + Title = "Widget generator fell over", + Severity = DiagnosticSeverity.Error, + HelpLinkUri = "https://acme.example/diagnostics/acme9999", +}; + +public override ImmutableArray SupportedDiagnostics => + [Rule, ComponentFailure.For(Failure)]; +``` + +For a generator using the framework's `RegisterSourceOutput`, declare them on `GenerationSettings` +alongside everything else the generator says about itself: + +```csharp +var generationContext = IncrementalPipeline.DefaultGenerationContextValueProvider( + context, + new GenerationSettings("MyGenerator", "1.0.0") { FailureOptions = Failure }); +``` + +Hold the options in a `static readonly` field and pass the same instance everywhere. `ComponentFailure.For` +caches on `(component, options)`, so a fresh options instance per call produces a fresh descriptor — and an +analyzer that declares one descriptor then reports another makes Roslyn throw. + +`IncludeStackTrace` adds the stack trace to the message. It is off by default: a stack trace through a +component's internals is noise to the consumer and makes the diagnostic text unstable between builds. Turn +it on when you are asking someone to report a bug. + +### Why these are code settings, not build properties + +An analyzer declares `SupportedDiagnostics` before any analyzer options exist, so a build property could not +reach it in time. Code-level configuration is the only form that works for all four component kinds. diff --git a/docs/wiki/Step-Cache-Tests.md b/docs/wiki/Step-Cache-Tests.md index 611e2de..ba20987 100644 --- a/docs/wiki/Step-Cache-Tests.md +++ b/docs/wiki/Step-Cache-Tests.md @@ -85,8 +85,28 @@ The framework's pipeline helpers assign a tracking name to every stage. Cache te | `RegisterSourceOutput` extension | `RegisterSourceOutput_{OutputType}` | Bundled generators add their own names, for example `GetAttributeDataTargets`, -`GetTypeLibrarySpecClassNames`, `GetTypeLibraryTargets`, and `GetFrameworkTypeLibraryTree` (the cached -framework `PurviewTypeLibrary` shape). `WithTrackingName` can rename any stage. +`GetTypeLibrarySpecClassNames` and `GetTypeLibraryTargets`. `WithTrackingName` can rename any stage. + +## A fresh compilation per run + +`RunIncremental` builds a **new `Compilation` for every run**, because that is what an IDE does on every +keystroke. A stage only reports `Cached`/`Unchanged` across those runs if its node value genuinely has +value equality — which is the whole property these tests exist to prove. + +Two consequences worth knowing: + +- Roslyn's own compilation and syntax-tree steps necessarily report `Modified`. Assert the generator's + own tracked stages with `StepIsCached` rather than reaching for `AllStepsCachedOrUnchanged`, which + covers Roslyn internals too. +- A stage whose node value holds an `ISymbol` or a `Compilation` cannot be cached, because those compare + by reference. That is a defect, not a test problem — see `PSGFR33`. + +`SourceGeneratorTestOptions.ReuseCompilationForIdenticalSources` reuses one `Compilation` instance for +runs with identical sources. It defaults to `false`. Turning it on lets a symbol-holding stage report +`Cached` on reference equality alone, hiding exactly the regression above, so enable it only when the +scenario is specifically about compilation-instance reuse — and say why at the call site. +`GeneratorLoggingTests.RunIncrementalAsync_ScopesEntriesToTheRunThatWroteThem` is the one such case in +this repository: it is about per-run log draining and needs a fully cached rerun to have anything to prove. ## Assertion API diff --git a/global.json b/global.json index 32f1a20..8b388e3 100644 --- a/global.json +++ b/global.json @@ -1,9 +1,11 @@ { "sdk": { - "allowPrerelease": false + "version": "11.0.100-rc.2.26504.105", + "rollForward": "latestMajor", + "allowPrerelease": true }, "msbuild-sdks": { - "Purview.BuildSdk": "1.0.6" + "Purview.BuildSdk": "1.1.0" }, "test": { "runner": "Microsoft.Testing.Platform" diff --git a/package.json b/package.json index d43debd..296b9f4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sourcegenerator-framework", - "version": "1.1.1", + "version": "1.2.0", "license": "MIT", "author": { "name": "Kieron Lanning", diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 1ae1b18..dbd0908 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,6 +1,6 @@ - net8.0;net9.0;net10.0; + net8.0;net9.0;net10.0;net11.0; netstandard2.1;$(TestingTargetFrameworks) net10.0 Purview.SourceGeneratorFramework diff --git a/src/src/SourceGeneratorFramework.Analyzers/AnalyzerReleases.Unshipped.md b/src/src/SourceGeneratorFramework.Analyzers/AnalyzerReleases.Unshipped.md index aa2d6e7..c97ae24 100644 --- a/src/src/SourceGeneratorFramework.Analyzers/AnalyzerReleases.Unshipped.md +++ b/src/src/SourceGeneratorFramework.Analyzers/AnalyzerReleases.Unshipped.md @@ -18,6 +18,7 @@ PSGFR39 | Purview.SourceGeneratorFramework | Error | Roslyn component must produ PSGFR40 | Purview.SourceGeneratorFramework | Warning | SGF cref should be inline code PSGFR41 | Purview.SourceGeneratorFramework | Warning | Component public surface exposes framework types PSGFR42 | Purview.SourceGeneratorFramework | Info | Dispose test harness results with a using declaration +PSGFR44 | Purview.SourceGeneratorFramework | Warning | Source generators must not throw TLB0014 | TypeLibrary | Warning | Type library partial extension is declared in a different namespace TLB0015 | TypeLibrary | Info | Type library partial extension must be declared 'public static partial' TLB0016 | TypeLibrary | Error | Enum value member type must be TypeIdentity or EnumValueDefinition diff --git a/src/src/SourceGeneratorFramework.Analyzers/AvoidSymbolsInPipelineModelAnalyzer.cs b/src/src/SourceGeneratorFramework.Analyzers/AvoidSymbolsInPipelineModelAnalyzer.cs index b135f10..7c1f0d5 100644 --- a/src/src/SourceGeneratorFramework.Analyzers/AvoidSymbolsInPipelineModelAnalyzer.cs +++ b/src/src/SourceGeneratorFramework.Analyzers/AvoidSymbolsInPipelineModelAnalyzer.cs @@ -1,6 +1,8 @@ +using System.Collections.Concurrent; using System.Collections.Immutable; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; namespace Purview.SourceGeneratorFramework.Analyzers; @@ -18,7 +20,7 @@ public sealed class AvoidSymbolsInPipelineModelAnalyzer : DiagnosticAnalyzer public static readonly DiagnosticDescriptor Rule = new( DiagnosticId, "Pipeline model retains a Roslyn object", - "Pipeline model member '{0}' uses '{1}', which is never equatable between runs and can root old compilations. Extract the information you need into strings or value types so incremental caching works.", + "Pipeline value '{0}' uses '{1}', which is never equatable between runs and can root old compilations. Extract the information you need into strings or value types so incremental caching works.", "Purview.SourceGeneratorFramework", DiagnosticSeverity.Warning, isEnabledByDefault: true, @@ -42,6 +44,12 @@ public sealed class AvoidSymbolsInPipelineModelAnalyzer : DiagnosticAnalyzer "Microsoft.CodeAnalysis.GeneratorAttributeSyntaxContext", ]; + static readonly string[] ProviderMetadataNames = + [ + "Microsoft.CodeAnalysis.IncrementalValuesProvider`1", + "Microsoft.CodeAnalysis.IncrementalValueProvider`1", + ]; + public override ImmutableArray SupportedDiagnostics => [Rule]; public override void Initialize(AnalysisContext context) @@ -52,6 +60,43 @@ public override void Initialize(AnalysisContext context) context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); + // A pipeline stage is flagged where the Roslyn object enters it, which a named model type never + // sees: ForAttributeWithMetadataName returning the symbol, or a Select projecting one. + // AnalyzeModelType only walks a model's own members, so neither shape is reachable from it. + // + // The findings are collected during the compilation and reported at the end rather than from the + // operation action itself: Rule carries WellKnownDiagnosticTags.CompilationEnd, and Roslyn drops a + // diagnostic with that tag when it is reported from anywhere but a compilation-end action. + context.RegisterCompilationStartAction(static startContext => + { + // Resolved once and compared by symbol identity. Matching on a display string is guesswork: + // the format of an open generic's name is not something to rely on. + var providerTypesBuilder = ImmutableArray.CreateBuilder(ProviderMetadataNames.Length); + foreach (var metadataName in ProviderMetadataNames) + { + if (startContext.Compilation.GetTypeByMetadataName(metadataName) is { } providerType) + providerTypesBuilder.Add(providerType); + } + + if (providerTypesBuilder.Count == 0) + return; + + var providerTypes = providerTypesBuilder.ToImmutable(); + + ConcurrentBag providerDiagnostics = []; + + startContext.RegisterOperationAction( + operationContext => AnalyzeProviderValueType(operationContext, providerTypes, providerDiagnostics), + OperationKind.Invocation + ); + + startContext.RegisterCompilationEndAction(endContext => + { + foreach (var diagnostic in providerDiagnostics) + endContext.ReportDiagnostic(diagnostic); + }); + }); + context.RegisterCompilationAction(static context => { var compilation = context.Compilation; @@ -74,6 +119,80 @@ public override void Initialize(AnalysisContext context) }); } + /// + /// Flags an invocation that produces an IncrementalValue(s)Provider<T> whose T retains a + /// Roslyn object, which is the value the driver caches between runs. + /// + /// + /// Only the call that *introduces* the retained type is reported: if the receiver already carried it, + /// the earlier link in the chain is the real site and reporting both would say the same thing twice. + /// + static void AnalyzeProviderValueType( + OperationAnalysisContext context, + ImmutableArray providerTypes, + ConcurrentBag diagnostics + ) + { + var invocation = (IInvocationOperation)context.Operation; + + var producedValueType = GetProviderValueType(invocation.Type, providerTypes); + if (producedValueType is null) + return; + + var retainedType = FindRetainedRoslynType(producedValueType); + if (retainedType is null) + return; + + // An extension method's receiver is its first argument; an instance method's is Instance. + var receiver = + invocation.Instance + ?? ( + invocation.TargetMethod.IsExtensionMethod && invocation.Arguments.Length > 0 + ? invocation.Arguments[0].Value + : null + ); + + // Only an earlier *invocation* that already carried the retained type is reported instead of this + // one; otherwise the chain would be reported twice for the same mistake. A receiver that is not an + // invocation — context.CompilationProvider, say — has no earlier call to report, so this call is + // the site. + if ( + receiver is IInvocationOperation + && GetProviderValueType(receiver.Type, providerTypes) is { } receivedValueType + && FindRetainedRoslynType(receivedValueType) is not null + ) + { + return; + } + + diagnostics.Add( + Diagnostic.Create( + Rule, + invocation.Syntax.GetLocation(), + invocation.TargetMethod.Name, + retainedType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat) + ) + ); + } + + /// + /// Returns the T of an IncrementalValueProvider<T> or + /// IncrementalValuesProvider<T>, or for any other type. + /// + static ITypeSymbol? GetProviderValueType(ITypeSymbol? typeSymbol, ImmutableArray providerTypes) + { + if (typeSymbol is not INamedTypeSymbol { IsGenericType: true, TypeArguments.Length: 1 } named) + return null; + + foreach (var providerType in providerTypes) + { + if (SymbolEqualityComparer.Default.Equals(named.OriginalDefinition, providerType)) + return named.TypeArguments[0]; + } + + return null; + } + static void AnalyzeModelType(CompilationAnalysisContext context, INamedTypeSymbol modelType) { foreach (var member in modelType.GetMembers()) @@ -136,7 +255,34 @@ static void AnalyzeModelType(CompilationAnalysisContext context, INamedTypeSymbo return null; } + /// + /// Determines whether is, derives from, or implements one of the + /// non-equatable Roslyn types. + /// + /// + /// The inheritance walk is load-bearing. Real pipeline code names the derived types — a transform + /// returns INamedTypeSymbol, not ISymbol, and a field holds a CSharpCompilation, + /// not a Compilation. Matching only the exact names in + /// would miss every realistic case. + /// static bool IsNonEquatableRoslynType(INamedTypeSymbol typeSymbol) + { + for (ITypeSymbol? current = typeSymbol; current is not null; current = current.BaseType) + { + if (MatchesNonEquatableName(current)) + return true; + } + + foreach (var implemented in typeSymbol.AllInterfaces) + { + if (MatchesNonEquatableName(implemented)) + return true; + } + + return false; + } + + static bool MatchesNonEquatableName(ITypeSymbol typeSymbol) { var displayName = typeSymbol.ToDisplayString(); foreach (var nonEquatableType in NonEquatableRoslynTypes) diff --git a/src/src/SourceGeneratorFramework.Analyzers/DoNotThrowFromGeneratorAnalyzer.cs b/src/src/SourceGeneratorFramework.Analyzers/DoNotThrowFromGeneratorAnalyzer.cs new file mode 100644 index 0000000..bdc62ef --- /dev/null +++ b/src/src/SourceGeneratorFramework.Analyzers/DoNotThrowFromGeneratorAnalyzer.cs @@ -0,0 +1,294 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Purview.SourceGeneratorFramework.Analyzers; + +/// +/// Flags a throw in code that runs during source generation. +/// +/// +/// +/// An exception escaping a source generator is not a private failure. Roslyn catches it and reports +/// CS8785 — "Generator 'X' failed to generate source" — in every build and in every IDE that loads the +/// project, and the generator produces no output for that compilation, so the consumer sees a wall +/// of unrelated "type or namespace not found" errors alongside it. The consumer cannot act on any of it: the +/// stack trace names the generator's internals, not the code that triggered it. +/// +/// +/// A generator always has a reporting channel that does not have those properties. Carry the failure on a +/// GeneratorResult<T> as a ReportableDiagnostic and let the source-output stage report it, +/// or call SourceProductionContext.ReportDiagnostic directly. Either way the consumer gets a +/// diagnostic pointing at their own code, with an id they can suppress or configure, and the rest of the +/// generator's output still arrives. +/// +/// +/// Two scopes are checked, because generation code is not all written in one place: anything lexically inside +/// a type implementing (the Initialize body, the pipeline lambdas +/// and the emit helpers that hang off the generator), and any lambda handed to a pipeline method, wherever it +/// is written. +/// +/// +/// Cancellation is unaffected: it is signalled by CancellationToken.ThrowIfCancellationRequested(), a +/// method call rather than a throw, and Roslyn expects that exception to propagate. +/// +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class DoNotThrowFromGeneratorAnalyzer : DiagnosticAnalyzer +{ + public const string DiagnosticId = "PSGFR44"; + + public static readonly DiagnosticDescriptor Rule = new( + DiagnosticId, + "Source generators must not throw", + "{0} throws {1}; an exception escaping a generator is reported as CS8785 in every consuming build and IDE, and the generator produces no output at all for that compilation. Report a diagnostic instead - carry it on GeneratorResult as a ReportableDiagnostic, call SourceProductionContext.ReportDiagnostic, or wrap the work in ComponentGuard.Run so the failure is reported for you.", + "Purview.SourceGeneratorFramework", + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Code that runs during source generation must report failures as diagnostics rather than throwing. An escaping exception surfaces as CS8785, loses every generated file for that compilation, and gives the consumer a stack trace through the generator's internals instead of a diagnostic on their own code." + ); + + /// + /// The pipeline methods whose lambda arguments run during generation. Mirrors + /// , plus the registration methods that take a callback. + /// + static readonly HashSet PipelineMethodNames = + [ + "Select", + "SelectMany", + "Where", + "Combine", + "Collect", + "ForAttributeWithMetadataName", + "CreateSyntaxProvider", + "CombineWith", + "CollectWith", + "CombineWithContext", + "RegisterSourceOutput", + "RegisterImplementationSourceOutput", + "RegisterPostInitializationOutput", + ]; + + public override ImmutableArray SupportedDiagnostics => [Rule]; + + public override void Initialize(AnalysisContext context) + { + if (context is null) + throw new ArgumentNullException(nameof(context)); + + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static startContext => + { + var generatorInterface = startContext.Compilation.GetTypeByMetadataName( + "Microsoft.CodeAnalysis.IIncrementalGenerator" + ); + + // Nothing in this compilation can be a generator, so there is nothing to check. + if (generatorInterface is null) + return; + + var cancellation = startContext.Compilation.GetTypeByMetadataName("System.OperationCanceledException"); + + startContext.RegisterSyntaxNodeAction( + nodeContext => AnalyzeThrow(nodeContext, generatorInterface, cancellation), + SyntaxKind.ThrowStatement, + SyntaxKind.ThrowExpression + ); + }); + } + + static void AnalyzeThrow( + SyntaxNodeAnalysisContext context, + INamedTypeSymbol generatorInterface, + INamedTypeSymbol? cancellation + ) + { + if (IsCancellation(context, cancellation)) + return; + + if (!TryDescribeScope(context, generatorInterface, out var scope)) + return; + + context.ReportDiagnostic(Diagnostic.Create(Rule, context.Node.GetLocation(), scope, DescribeThrown(context))); + } + + /// + /// Determines whether the throw is propagating cancellation, which must never be reported. + /// + /// + /// + /// Roslyn cancels analysis on every keystroke and expects an + /// to reach it. Two shapes count: throwing one directly, and the far more common + /// catch (OperationCanceledException) { throw; } that sits in front of a broad + /// catch (Exception) precisely so cancellation is not swallowed by it. + /// + /// + /// A bare throw; is only exempt because of the clause it sits in, so the enclosing + /// catch has to be resolved rather than the throw read on its own — the rule would otherwise + /// push an author to delete the one piece of cancellation handling that is correct. + /// + /// + static bool IsCancellation(SyntaxNodeAnalysisContext context, INamedTypeSymbol? cancellation) + { + if (cancellation is null) + return false; + + var thrown = context.Node switch + { + ThrowStatementSyntax statement => statement.Expression, + ThrowExpressionSyntax expression => expression.Expression, + _ => null, + }; + + if (thrown is not null) + { + return IsCancellationType( + context.SemanticModel.GetTypeInfo(thrown, context.CancellationToken).Type, + cancellation + ); + } + + // A rethrow. Its type is whatever the enclosing catch clause filters on. + foreach (var ancestor in context.Node.Ancestors()) + { + // Stop at the first lambda or method body: a rethrow cannot cross one. + if (ancestor is AnonymousFunctionExpressionSyntax or MemberDeclarationSyntax) + return false; + + if (ancestor is not CatchClauseSyntax { Declaration.Type: { } caughtType }) + continue; + + return IsCancellationType( + context.SemanticModel.GetTypeInfo(caughtType, context.CancellationToken).Type, + cancellation + ); + } + + return false; + } + + /// + /// Whether is or derives from it, + /// which covers TaskCanceledException. + /// + static bool IsCancellationType(ITypeSymbol? type, INamedTypeSymbol cancellation) + { + for (var current = type; current is not null; current = current.BaseType) + { + if (SymbolEqualityComparer.Default.Equals(current, cancellation)) + return true; + } + + return false; + } + + /// + /// Describes the generation scope the throw sits in, or returns when it + /// does not run during generation. + /// + static bool TryDescribeScope( + SyntaxNodeAnalysisContext context, + INamedTypeSymbol generatorInterface, + out string scope + ) + { + foreach (var current in context.Node.AncestorsAndSelf()) + { + if ( + current is AnonymousFunctionExpressionSyntax lambda + && TryGetPipelineMethodName(context, lambda, out var pipelineMethod) + ) + { + scope = $"The lambda passed to '{pipelineMethod}'"; + return true; + } + + if (current is not TypeDeclarationSyntax declaration) + continue; + + if ( + context.SemanticModel.GetDeclaredSymbol(declaration, context.CancellationToken) is INamedTypeSymbol type + && type.AllInterfaces.Any(implemented => + SymbolEqualityComparer.Default.Equals(implemented, generatorInterface) + ) + ) + { + scope = $"Generator '{type.Name}'"; + return true; + } + } + + scope = string.Empty; + return false; + } + + /// + /// Names the thrown exception for the message, falling back to a description of a rethrow. + /// + static string DescribeThrown(SyntaxNodeAnalysisContext context) + { + var thrown = context.Node switch + { + ThrowStatementSyntax statement => statement.Expression, + ThrowExpressionSyntax expression => expression.Expression, + _ => null, + }; + + // A bare 'throw;' rethrows whatever was caught, which escapes the generator just the same. + if (thrown is null) + return "the caught exception"; + + // The type of the expression is what was thrown, which is what the consumer sees in the stack trace. + return context.SemanticModel.GetTypeInfo(thrown, context.CancellationToken).Type is { } type + ? $"'{type.Name}'" + : "an exception"; + } + + static bool TryGetPipelineMethodName( + SyntaxNodeAnalysisContext context, + AnonymousFunctionExpressionSyntax lambda, + out string methodName + ) + { + methodName = string.Empty; + + if (lambda.Parent is not ArgumentSyntax) + return false; + + if (lambda.Parent?.Parent?.Parent is not InvocationExpressionSyntax invocation) + return false; + + if ( + context.SemanticModel.GetSymbolInfo(invocation, context.CancellationToken).Symbol + is not IMethodSymbol method + ) + return false; + + if (!PipelineMethodNames.Contains(method.Name)) + return false; + + // Pipeline methods come from Roslyn or from the framework's own helpers; this keeps LINQ's + // Select/Where and anything else sharing a name out. + var containingType = method.ContainingType; + if (containingType is null) + return false; + + var containingNamespace = containingType.ContainingNamespace?.ToDisplayString(); + if ( + containingNamespace != "Microsoft.CodeAnalysis" + && containingType.Name != "IncrementalPipeline" + && containingType.Name != "IncrementalValueProviderExtensions" + && containingType.Name != "IncrementalGeneratorInitializationContextExtensions" + ) + { + return false; + } + + methodName = method.Name; + return true; + } +} diff --git a/src/src/SourceGeneratorFramework.Analyzers/UndisposedTestResultAnalyzer.cs b/src/src/SourceGeneratorFramework.Analyzers/UndisposedTestResultAnalyzer.cs index 83dfea0..be74be9 100644 --- a/src/src/SourceGeneratorFramework.Analyzers/UndisposedTestResultAnalyzer.cs +++ b/src/src/SourceGeneratorFramework.Analyzers/UndisposedTestResultAnalyzer.cs @@ -180,6 +180,7 @@ static bool ProducesResult(ExpressionSyntax expression, SyntaxNodeAnalysisContex if (awaited && !IsTaskLike(context.SemanticModel.GetTypeInfo(current, context.CancellationToken).Type)) return false; + // The expression produces a result if it is not an invocation, or if it is an invocation that does not consume a result. return current is not InvocationExpressionSyntax invocation || !ConsumesResult(invocation, context); } diff --git a/src/src/SourceGeneratorFramework.ExampleGenerator/Extensions/System/StringExtensions.cs b/src/src/SourceGeneratorFramework.ExampleGenerator/Extensions/System/StringExtensions.cs index 47e68ea..81ae6af 100644 --- a/src/src/SourceGeneratorFramework.ExampleGenerator/Extensions/System/StringExtensions.cs +++ b/src/src/SourceGeneratorFramework.ExampleGenerator/Extensions/System/StringExtensions.cs @@ -3,7 +3,7 @@ namespace System; -[System.Diagnostics.CodeAnalysis.SuppressMessage( +[Diagnostics.CodeAnalysis.SuppressMessage( "Design", "CA1034:Nested types should not be visible", Justification = "The extension(...) block is lowered to an unnamed nested type; the language requires it to be nested in a static class, and PSGFR34 mandates extension blocks over classic 'this'-parameter methods." diff --git a/src/src/SourceGeneratorFramework.ExampleGenerator/Models.cs b/src/src/SourceGeneratorFramework.ExampleGenerator/Models.cs index 7bdb8fa..cb2abff 100644 --- a/src/src/SourceGeneratorFramework.ExampleGenerator/Models.cs +++ b/src/src/SourceGeneratorFramework.ExampleGenerator/Models.cs @@ -43,7 +43,7 @@ public sealed class GenerateServiceAttribute(ServiceLifetime lifetime = ServiceL /// name argument or as a named argument ([GenerateService(Name = "…")]); the named /// argument wins when both are supplied because it is assigned after the constructor runs. /// - public string? Name { get; init; } = name; + public string? Name { get; internal init; } = name; } /// diff --git a/src/src/SourceGeneratorFramework.Generators/AttributeDataModelGenerator.cs b/src/src/SourceGeneratorFramework.Generators/AttributeDataModelGenerator.cs index 765f7c4..6d39246 100644 --- a/src/src/SourceGeneratorFramework.Generators/AttributeDataModelGenerator.cs +++ b/src/src/SourceGeneratorFramework.Generators/AttributeDataModelGenerator.cs @@ -731,7 +731,12 @@ static string GetEnumSingleSourceExtractionExpression(PropertySource source, str $"attributeData.GetEnumConstructorArgument({source.ConstructorIndex}, {defaultValueExpression})", AttributePropertySource.ConstructorName => $"attributeData.GetEnumConstructorArgument(\"{source.MappedName}\", {defaultValueExpression})", - _ => throw new InvalidOperationException($"Unsupported enum property source: {source.Source}"), + // The only remaining source is TypeArgument, which cannot reach an enum property: ADM0008 is an + // error when a type-argument property is anything other than TypeIdentity, so the model never + // gets here. Falling back to the default expression keeps that unreachable case as valid, + // type-correct generated code - a throw would escape as CS8785 and cost the consumer every file + // this generator produces (PSGFR44). + _ => defaultValueExpression, }; } @@ -800,9 +805,11 @@ string defaultValueExpression $"var {variableName} = attributeData.GetConstructorArgument<{typeName}>(\"{source.MappedName}\", {defaultValueExpression});", AttributePropertySource.TypeArgument when source.MappedName is not null => $"var {variableName} = attributeData.GetGenericTypeArgument<{typeName}>(\"{source.MappedName}\", {defaultValueExpression});", - AttributePropertySource.TypeArgument => + // TypeArgument by index is the last defined source, so it doubles as the default arm the + // compiler requires. A throw here would escape as CS8785 and cost the consumer every file this + // generator produces (PSGFR44); every defined source is already handled above. + _ => $"var {variableName} = attributeData.GetGenericTypeArgument<{typeName}>({source.ConstructorIndex}, {defaultValueExpression});", - _ => throw new InvalidOperationException($"Unsupported property source: {source.Source}"), }; } @@ -820,9 +827,10 @@ static string GetTryGetMethodCall(PropertySource source, string variableName, st $"attributeData.TryGetConstructorArgument<{typeName}>(\"{source.MappedName}\", {outVariable})", AttributePropertySource.TypeArgument when source.MappedName is not null => $"attributeData.TryGetGenericTypeArgument<{typeName}>(\"{source.MappedName}\", {outVariable})", - AttributePropertySource.TypeArgument => - $"attributeData.TryGetGenericTypeArgument<{typeName}>({source.ConstructorIndex}, {outVariable})", - _ => throw new InvalidOperationException($"Unsupported property source: {source.Source}"), + // TypeArgument by index is the last defined source, so it doubles as the default arm the + // compiler requires. A throw here would escape as CS8785 and cost the consumer every file this + // generator produces (PSGFR44); every defined source is already handled above. + _ => $"attributeData.TryGetGenericTypeArgument<{typeName}>({source.ConstructorIndex}, {outVariable})", }; } diff --git a/src/src/SourceGeneratorFramework.Generators/Helpers/AttributeDataModelLibrary.cs b/src/src/SourceGeneratorFramework.Generators/Helpers/AttributeDataModelLibrary.cs index 6fbe845..531f491 100644 --- a/src/src/SourceGeneratorFramework.Generators/Helpers/AttributeDataModelLibrary.cs +++ b/src/src/SourceGeneratorFramework.Generators/Helpers/AttributeDataModelLibrary.cs @@ -21,21 +21,35 @@ IncrementalGeneratorInitializationContext context // expression (see BuildTarget). var typeLibrarySpecData = context .SyntaxProvider.ForAttributeWithMetadataName( - GeneratorTypeLibrary.Attirbutes.GenerateTypeLibraryAttribute.MetadataFullName, - transform: static (ctx, _) => (INamedTypeSymbol)ctx.TargetSymbol, + GeneratorTypeLibrary.Attributes.GenerateTypeLibraryAttribute.MetadataFullName, + // Read the spec inside the transform so the node value is the value-equatable + // TypeLibrarySpecData rather than an INamedTypeSymbol. A symbol held in a node value is + // retained by the driver between runs, rooting the previous compilation. + transform: static (ctx, _) => ReadTypeLibrarySpecData((INamedTypeSymbol)ctx.TargetSymbol), predicate: static (ctx, _) => ctx is ClassDeclarationSyntax ) - .Select(static (specSymbol, _) => ReadTypeLibrarySpecData(specSymbol)) .Collect() .Select(static (specs, _) => new EquatableArray(specs)) .WithTrackingName("GetTypeLibrarySpecClassNames"); + // KNOWN PSGFR33 VIOLATION, not yet fixed. This node value is an INamedTypeSymbol, which compares by + // reference, so the stage reports Modified on every new compilation and BuildTarget re-runs on every + // keystroke for every [Generate] target. + // + // The measured cost is that re-execution and nothing more. It does not leak: because the values + // compare unequal, each run's value replaces the previous one rather than pinning it, which + // GeneratorCompilationRetentionTests pins. Generated output stays correct because GeneratorResult + // is value-equatable and stops the change propagating past this stage. + // + // It is not fixable the way TypeLibraryModelLibrary was — by doing the whole build inside the + // transform — because BuildTarget also needs typeLibrarySpecData, which is Collect()ed across the + // whole compilation and so is not reachable from a single target's transform. Removing it means + // giving the transform an equatable intermediate that captures every fact BuildTarget reads from the + // symbol, and deferring only the spec-dependent target resolution (which needs the compilation to + // resolve a name derived from the specs) to the combined stage. var attributeCandidates = IncrementalPipeline.ForAttributeWithMetadataName( context, - GeneratorTypeLibrary.Attirbutes.GenerateAttribute, - // ForAttributeWithMetadataName already resolves TargetSymbol; calling - // SemanticModel.GetDeclaredSymbol again would re-run the same symbol resolution on every - // pipeline rerun, so the pre-resolved symbol is carried through as the intermediate value. + GeneratorTypeLibrary.Attributes.GenerateAttribute, static (ctx, _) => (INamedTypeSymbol)ctx.TargetSymbol, predicate: static (ctx, _) => ctx is StructDeclarationSyntax or RecordDeclarationSyntax ); @@ -63,7 +77,7 @@ CancellationToken cancellationToken // AttributeDataModelSymbolPropertyAnalyzer; the generator carries them on the result so it can gate // generation on ShouldProcess without emitting the diagnostics itself. List diagnostics = []; - var generateAttribute = GetAttribute(structSymbol, GeneratorTypeLibrary.Attirbutes.GenerateAttribute); + var generateAttribute = GetAttribute(structSymbol, GeneratorTypeLibrary.Attributes.GenerateAttribute); if (generateAttribute is null) return GeneratorResult.Empty; @@ -394,7 +408,7 @@ static ParameterAttributeInfo ReadParameterAttributes( object? defaultValue; bool hasDefaultValue; - var excludeAttribute = GetAttribute(parameter, GeneratorTypeLibrary.Attirbutes.ExcludeAttribute); + var excludeAttribute = GetAttribute(parameter, GeneratorTypeLibrary.Attributes.ExcludeAttribute); if (excludeAttribute is not null) isExcluded = true; @@ -418,7 +432,7 @@ static ParameterAttributeInfo ReadParameterAttributes( var hasExclusive = isExcluded || isNestedModel || isTypeArgument; - var ctorAttribute = GetAttribute(parameter, GeneratorTypeLibrary.Attirbutes.ArgumentAttribute); + var ctorAttribute = GetAttribute(parameter, GeneratorTypeLibrary.Attributes.ArgumentAttribute); var isEnum = false; // Constructor sources are added from this index so the named-argument source below can be placed @@ -450,7 +464,7 @@ static ParameterAttributeInfo ReadParameterAttributes( } } - var namedAttribute = GetAttribute(parameter, GeneratorTypeLibrary.Attirbutes.PropertyAttribute); + var namedAttribute = GetAttribute(parameter, GeneratorTypeLibrary.Attributes.PropertyAttribute); if (namedAttribute is not null && !hasExclusive) { var namedName = GetNamedArgument(namedAttribute, "Name", (string?)null); @@ -523,7 +537,7 @@ bool IsEnum bool HasDefaultValue ) ReadNestedModelAttributeInfo(IParameterSymbol parameter, bool isExcluded) { - var nestedModelAttribute = GetAttribute(parameter, GeneratorTypeLibrary.Attirbutes.NestedModelAttribute); + var nestedModelAttribute = GetAttribute(parameter, GeneratorTypeLibrary.Attributes.NestedModelAttribute); if (nestedModelAttribute is null) return (false, [], null, false); @@ -546,7 +560,7 @@ bool HasDefaultValue { var typeArgumentAttribute = GetAttribute( parameter, - GeneratorTypeLibrary.Attirbutes.GenericTypeArgumentAttribute + GeneratorTypeLibrary.Attributes.GenericTypeArgumentAttribute ); if (typeArgumentAttribute is null) return (false, [], null, false); @@ -976,7 +990,7 @@ static bool IsSupportedType(ITypeSymbol typeSymbol) static bool IsGeneratedAttributeModel(ITypeSymbol typeSymbol) => typeSymbol is not INamedTypeSymbol namedType || namedType.TypeKind != TypeKind.Struct ? false - : GetAttribute(namedType, GeneratorTypeLibrary.Attirbutes.GenerateAttribute) is not null; + : GetAttribute(namedType, GeneratorTypeLibrary.Attributes.GenerateAttribute) is not null; static AttributeData? GetAttribute(ISymbol symbol, TypeIdentity attributeType) { @@ -1008,7 +1022,7 @@ static TypeIdentity ParseTypeIdentity(string fullyQualifiedName) /// static TypeLibrarySpecData ReadTypeLibrarySpecData(INamedTypeSymbol specSymbol) { - var generateAttribute = GetAttribute(specSymbol, GeneratorTypeLibrary.Attirbutes.GenerateTypeLibraryAttribute); + var generateAttribute = GetAttribute(specSymbol, GeneratorTypeLibrary.Attributes.GenerateTypeLibraryAttribute); var className = generateAttribute is null ? "TypeLibrary" : GetNamedArgument(generateAttribute, "ClassName", (string?)null) ?? "TypeLibrary"; @@ -1016,7 +1030,7 @@ static TypeLibrarySpecData ReadTypeLibrarySpecData(INamedTypeSymbol specSymbol) var members = ImmutableArray.CreateBuilder(); foreach (var field in specSymbol.GetMembers().OfType()) { - var typeRef = GetAttribute(field, GeneratorTypeLibrary.Attirbutes.TypeRefAttribute); + var typeRef = GetAttribute(field, GeneratorTypeLibrary.Attributes.TypeRefAttribute); if (typeRef is null) continue; diff --git a/src/src/SourceGeneratorFramework.Generators/Helpers/GeneratorTypeLibrary.cs b/src/src/SourceGeneratorFramework.Generators/Helpers/GeneratorTypeLibrary.cs index 3b4bd7f..4211e8d 100644 --- a/src/src/SourceGeneratorFramework.Generators/Helpers/GeneratorTypeLibrary.cs +++ b/src/src/SourceGeneratorFramework.Generators/Helpers/GeneratorTypeLibrary.cs @@ -10,7 +10,7 @@ static class GeneratorTypeLibrary public static readonly TypeIdentity EnumValueDefinition = TypeIdentity.Create(); - public static class Attirbutes + public static class Attributes { public static readonly TypeIdentity GenerateAttribute = new(nameof(GenerateAttribute), GeneratorsNamespace); diff --git a/src/src/SourceGeneratorFramework.Generators/Helpers/SourceEmitter.AttributeData.cs b/src/src/SourceGeneratorFramework.Generators/Helpers/SourceEmitter.AttributeData.cs index 25e2680..c992cee 100644 --- a/src/src/SourceGeneratorFramework.Generators/Helpers/SourceEmitter.AttributeData.cs +++ b/src/src/SourceGeneratorFramework.Generators/Helpers/SourceEmitter.AttributeData.cs @@ -7,19 +7,19 @@ partial class SourceEmitter { static SourceText GenerateAttribute() { - var writer = CreateWriter(GeneratorTypeLibrary.Attirbutes.GenerateAttribute); + var writer = CreateWriter(GeneratorTypeLibrary.Attributes.GenerateAttribute); return writer .XmlSummary("Generates parsing members for an attribute-data model.") .AttributeClass( - new(GeneratorTypeLibrary.Attirbutes.GenerateAttribute), + new(GeneratorTypeLibrary.Attributes.GenerateAttribute), AttributeTargets.Struct, bodyWriter => { bodyWriter .XmlSummary("Initializes the attribute for the target attribute type.") .Constructor( - new(GeneratorTypeLibrary.Attirbutes.GenerateAttribute, TypeDeclarationAccessibility.Public) + new(GeneratorTypeLibrary.Attributes.GenerateAttribute, TypeDeclarationAccessibility.Public) { Parameters = [new("targetAttribute", PurviewTypeLibrary.System.Type)], }, @@ -32,7 +32,7 @@ static SourceText GenerateAttribute() bodyWriter .XmlSummary("Initializes the attribute for the target attribute name.") .Constructor( - new(GeneratorTypeLibrary.Attirbutes.GenerateAttribute, TypeDeclarationAccessibility.Public) + new(GeneratorTypeLibrary.Attributes.GenerateAttribute, TypeDeclarationAccessibility.Public) { Parameters = [new("targetAttributeName", PurviewTypeLibrary.System.String)], }, @@ -89,21 +89,21 @@ static SourceText GenerateAttribute() static SourceText PropertyAttribute() { - var writer = CreateWriter(GeneratorTypeLibrary.Attirbutes.PropertyAttribute); + var writer = CreateWriter(GeneratorTypeLibrary.Attributes.PropertyAttribute); return writer .XmlSummary("Marks a record parameter as a named attribute argument.") .AttributeClass( - new(GeneratorTypeLibrary.Attirbutes.PropertyAttribute), + new(GeneratorTypeLibrary.Attributes.PropertyAttribute), AttributeTargets.Parameter, bodyWriter => { bodyWriter .XmlSummary( - $"Initializes a new instance of the class." + $"Initializes a new instance of the class." ) .Constructor( - new(GeneratorTypeLibrary.Attirbutes.PropertyAttribute, TypeDeclarationAccessibility.Public) + new(GeneratorTypeLibrary.Attributes.PropertyAttribute, TypeDeclarationAccessibility.Public) { Parameters = [ @@ -161,18 +161,18 @@ static SourceText PropertyAttribute() static SourceText ArgumentAttribute() { - var writer = CreateWriter(GeneratorTypeLibrary.Attirbutes.ArgumentAttribute); + var writer = CreateWriter(GeneratorTypeLibrary.Attributes.ArgumentAttribute); return writer .XmlSummary("Marks a record parameter as a constructor argument.") .AttributeClass( - new(GeneratorTypeLibrary.Attirbutes.ArgumentAttribute), + new(GeneratorTypeLibrary.Attributes.ArgumentAttribute), AttributeTargets.Parameter, bodyWriter => { bodyWriter .XmlSummary( - $"Initializes a new instance of the class." + $"Initializes a new instance of the class." ) .XmlParam( "name", @@ -180,7 +180,7 @@ static SourceText ArgumentAttribute() ) .XmlParam("defaultValue", "The default value of the constructor parameter.") .Constructor( - new(GeneratorTypeLibrary.Attirbutes.ArgumentAttribute, TypeDeclarationAccessibility.Public) + new(GeneratorTypeLibrary.Attributes.ArgumentAttribute, TypeDeclarationAccessibility.Public) { Parameters = [ @@ -200,12 +200,12 @@ static SourceText ArgumentAttribute() bodyWriter .XmlSummary( - $"Initializes a new instance of the class." + $"Initializes a new instance of the class." ) .XmlParam("index", "The index of the constructor parameter.") .XmlParam("defaultValue", "The default value of the constructor parameter.") .Constructor( - new(GeneratorTypeLibrary.Attirbutes.ArgumentAttribute, TypeDeclarationAccessibility.Public) + new(GeneratorTypeLibrary.Attributes.ArgumentAttribute, TypeDeclarationAccessibility.Public) { Parameters = [ @@ -279,11 +279,11 @@ static SourceText ArgumentAttribute() static SourceText NestedModelAttribute() { - var writer = CreateWriter(GeneratorTypeLibrary.Attirbutes.NestedModelAttribute); + var writer = CreateWriter(GeneratorTypeLibrary.Attributes.NestedModelAttribute); return writer .XmlSummary("Marks a record parameter as a nested generated attribute-data model.") .AttributeClass( - new(GeneratorTypeLibrary.Attirbutes.NestedModelAttribute), + new(GeneratorTypeLibrary.Attributes.NestedModelAttribute), AttributeTargets.Parameter, bodyWriter => bodyWriter.Comment("Empty") ); @@ -291,11 +291,11 @@ static SourceText NestedModelAttribute() static SourceText ExcludeAttribute() { - var writer = CreateWriter(GeneratorTypeLibrary.Attirbutes.ExcludeAttribute); + var writer = CreateWriter(GeneratorTypeLibrary.Attributes.ExcludeAttribute); return writer .XmlSummary("Excludes a record parameter from the generated attribute-data model.") .AttributeClass( - new(GeneratorTypeLibrary.Attirbutes.ExcludeAttribute), + new(GeneratorTypeLibrary.Attributes.ExcludeAttribute), AttributeTargets.Parameter, bodyWriter => bodyWriter.Comment("Empty") ); @@ -303,12 +303,12 @@ static SourceText ExcludeAttribute() static SourceText GenericTypeArgumentAttribute() { - var writer = CreateWriter(GeneratorTypeLibrary.Attirbutes.GenericTypeArgumentAttribute); + var writer = CreateWriter(GeneratorTypeLibrary.Attributes.GenericTypeArgumentAttribute); return writer .XmlSummary("Marks a record parameter as a generic type argument of the attribute class.") .AttributeClass( - new(GeneratorTypeLibrary.Attirbutes.GenericTypeArgumentAttribute), + new(GeneratorTypeLibrary.Attributes.GenericTypeArgumentAttribute), AttributeTargets.Parameter, bodyWriter => { @@ -316,7 +316,7 @@ static SourceText GenericTypeArgumentAttribute() .XmlSummary("Initializes a new instance marking the first type argument.") .Constructor( new( - GeneratorTypeLibrary.Attirbutes.GenericTypeArgumentAttribute, + GeneratorTypeLibrary.Attributes.GenericTypeArgumentAttribute, TypeDeclarationAccessibility.Public ), constructorWriter => constructorWriter.Comment("Empty") @@ -326,7 +326,7 @@ static SourceText GenericTypeArgumentAttribute() .XmlSummary("Initializes a new instance marking the type argument at the specified index.") .Constructor( new( - GeneratorTypeLibrary.Attirbutes.GenericTypeArgumentAttribute, + GeneratorTypeLibrary.Attributes.GenericTypeArgumentAttribute, TypeDeclarationAccessibility.Public ) { @@ -341,7 +341,7 @@ static SourceText GenericTypeArgumentAttribute() ) .Constructor( new( - GeneratorTypeLibrary.Attirbutes.GenericTypeArgumentAttribute, + GeneratorTypeLibrary.Attributes.GenericTypeArgumentAttribute, TypeDeclarationAccessibility.Public ) { diff --git a/src/src/SourceGeneratorFramework.Generators/Helpers/SourceEmitter.TypeLibrary.cs b/src/src/SourceGeneratorFramework.Generators/Helpers/SourceEmitter.TypeLibrary.cs index 13d4ee1..cc37516 100644 --- a/src/src/SourceGeneratorFramework.Generators/Helpers/SourceEmitter.TypeLibrary.cs +++ b/src/src/SourceGeneratorFramework.Generators/Helpers/SourceEmitter.TypeLibrary.cs @@ -7,12 +7,12 @@ partial class SourceEmitter { static SourceText GenerateTypeLibraryAttribute() { - var writer = CreateTypeLibraryWriter(GeneratorTypeLibrary.Attirbutes.GenerateTypeLibraryAttribute); + var writer = CreateTypeLibraryWriter(GeneratorTypeLibrary.Attributes.GenerateTypeLibraryAttribute); return writer .XmlSummary("Generates a type-library class from the annotated static partial class.") .AttributeClass( - new(GeneratorTypeLibrary.Attirbutes.GenerateTypeLibraryAttribute), + new(GeneratorTypeLibrary.Attributes.GenerateTypeLibraryAttribute), AttributeTargets.Class, bodyWriter => { @@ -47,12 +47,12 @@ static SourceText GenerateTypeLibraryAttribute() static SourceText TypeRefAttribute() { - var writer = CreateTypeLibraryWriter(GeneratorTypeLibrary.Attirbutes.TypeRefAttribute); + var writer = CreateTypeLibraryWriter(GeneratorTypeLibrary.Attributes.TypeRefAttribute); return writer .XmlSummary("Declares a type identity member for a generated type library.") .AttributeClass( - new(GeneratorTypeLibrary.Attirbutes.TypeRefAttribute), + new(GeneratorTypeLibrary.Attributes.TypeRefAttribute), AttributeTargets.Field, bodyWriter => { @@ -69,7 +69,7 @@ static SourceText TypeRefAttribute() "Whether the member generates a full name constant; useful for compile-time usage, such as in Attribute Data Model generation." ) .Constructor( - new(GeneratorTypeLibrary.Attirbutes.TypeRefAttribute, TypeDeclarationAccessibility.Public) + new(GeneratorTypeLibrary.Attributes.TypeRefAttribute, TypeDeclarationAccessibility.Public) { Parameters = [ @@ -110,7 +110,7 @@ static SourceText TypeRefAttribute() "Whether the member generates a full name constant; useful for compile-time usage, such as in Attribute Data Model generation." ) .Constructor( - new(GeneratorTypeLibrary.Attirbutes.TypeRefAttribute, TypeDeclarationAccessibility.Public) + new(GeneratorTypeLibrary.Attributes.TypeRefAttribute, TypeDeclarationAccessibility.Public) { Parameters = [ @@ -207,14 +207,14 @@ static SourceText TypeRefAttribute() static SourceText EnumValueAttribute() { - var writer = CreateTypeLibraryWriter(GeneratorTypeLibrary.Attirbutes.EnumValueAttribute); + var writer = CreateTypeLibraryWriter(GeneratorTypeLibrary.Attributes.EnumValueAttribute); return writer .XmlSummary( "Declares an enum value member for a generated type library. May be declared multiple times on a single field." ) .AttributeClass( - new(GeneratorTypeLibrary.Attirbutes.EnumValueAttribute), + new(GeneratorTypeLibrary.Attributes.EnumValueAttribute), AttributeTargets.Field, bodyWriter => { @@ -234,7 +234,7 @@ static SourceText EnumValueAttribute() "Optional alternate names used when matching, such as for registry-style enums." ) .Constructor( - new(GeneratorTypeLibrary.Attirbutes.EnumValueAttribute, TypeDeclarationAccessibility.Public) + new(GeneratorTypeLibrary.Attributes.EnumValueAttribute, TypeDeclarationAccessibility.Public) { Parameters = [ @@ -269,7 +269,7 @@ static SourceText EnumValueAttribute() "Optional alternate names used when matching, such as for registry-style enums." ) .Constructor( - new(GeneratorTypeLibrary.Attirbutes.EnumValueAttribute, TypeDeclarationAccessibility.Public) + new(GeneratorTypeLibrary.Attributes.EnumValueAttribute, TypeDeclarationAccessibility.Public) { Parameters = [ @@ -329,12 +329,12 @@ static SourceText EnumValueAttribute() static SourceText EnumValuesAttribute() { - var writer = CreateTypeLibraryWriter(GeneratorTypeLibrary.Attirbutes.EnumValuesAttribute); + var writer = CreateTypeLibraryWriter(GeneratorTypeLibrary.Attributes.EnumValuesAttribute); return writer .XmlSummary("Declares all enum values of a compile-time-available enum type for a generated type library.") .AttributeClass( - new(GeneratorTypeLibrary.Attirbutes.EnumValuesAttribute), + new(GeneratorTypeLibrary.Attributes.EnumValuesAttribute), AttributeTargets.Field, bodyWriter => { @@ -348,7 +348,7 @@ static SourceText EnumValuesAttribute() ) .Constructor( new( - GeneratorTypeLibrary.Attirbutes.EnumValuesAttribute, + GeneratorTypeLibrary.Attributes.EnumValuesAttribute, TypeDeclarationAccessibility.Public ) { diff --git a/src/src/SourceGeneratorFramework.Generators/Helpers/TypeLibraryModelLibrary.cs b/src/src/SourceGeneratorFramework.Generators/Helpers/TypeLibraryModelLibrary.cs index c5b52ec..9e6b3f9 100644 --- a/src/src/SourceGeneratorFramework.Generators/Helpers/TypeLibraryModelLibrary.cs +++ b/src/src/SourceGeneratorFramework.Generators/Helpers/TypeLibraryModelLibrary.cs @@ -1,4 +1,5 @@ using System.Globalization; +using System.Runtime.CompilerServices; using System.Xml.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -8,27 +9,48 @@ namespace Purview.SourceGeneratorFramework.Generators.Helpers; static class TypeLibraryModelLibrary { + /// + /// The framework model, memoised per . + /// + /// + /// The shape is fixed for a given compilation, so it is walked once rather than for every + /// [GenerateTypeLibrary] spec. A keeps that + /// property without a pipeline node holding the compilation: the entry dies with the compilation it + /// is keyed on, so nothing is rooted between runs. + /// + static readonly ConditionalWeakTable FrameworkModels = new(); + public static IncrementalValuesProvider> GetTypeLibraryTargetPipeline( IncrementalGeneratorInitializationContext context - ) - { - // The framework PurviewTypeLibrary shape is fixed for a given compilation, so it is walked once per - // compilation and cached as a value-equatable model instead of being re-walked for every - // [GenerateTypeLibrary] spec in the compilation. The framework's public type names travel with it - // because copied documentation renders framework-type cref references as inline code. - var frameworkTree = context - .CompilationProvider.Select(static (compilation, _) => BuildFrameworkModel(compilation)) - .WithTrackingName("GetFrameworkTypeLibraryTree"); - - return IncrementalPipeline + ) => + // The whole target is built inside the transform so the node value is the value-equatable + // GeneratorResult and never an ISymbol. A symbol in a node value is retained by the driver + // between runs, which roots the previous compilation and forces reference-equality comparison — + // the two things PSGFR33 exists to prevent. The transform already re-runs for every compilation, + // so doing the work here costs nothing extra; it only stops the symbol from being kept. + IncrementalPipeline .ForAttributeWithMetadataName( context, - GeneratorTypeLibrary.Attirbutes.GenerateTypeLibraryAttribute, - static (ctx, _) => (INamedTypeSymbol)ctx.TargetSymbol, + GeneratorTypeLibrary.Attributes.GenerateTypeLibraryAttribute, + static (ctx, ct) => + BuildTarget( + (INamedTypeSymbol)ctx.TargetSymbol, + GetFrameworkModel(ctx.SemanticModel.Compilation), + ct + ), predicate: static (ctx, _) => ctx is ClassDeclarationSyntax ) - .CombineWith(frameworkTree, static (specSymbol, framework, ct) => BuildTarget(specSymbol, framework, ct)) .WithTrackingName("GetTypeLibraryTargets"); + + static FrameworkTypeLibraryModel GetFrameworkModel(Compilation compilation) + { + if (FrameworkModels.TryGetValue(compilation, out var model)) + return model; + + model = BuildFrameworkModel(compilation); + FrameworkModels.Add(compilation, model); + + return model; } /// @@ -49,7 +71,7 @@ CancellationToken cancellationToken // Validation diagnostics are reported by TypeLibraryValidationAnalyzer; the generator carries them // on the result so it can gate generation on ShouldProcess without emitting the diagnostics itself. List diagnostics = []; - var generateAttribute = GetAttribute(specSymbol, GeneratorTypeLibrary.Attirbutes.GenerateTypeLibraryAttribute); + var generateAttribute = GetAttribute(specSymbol, GeneratorTypeLibrary.Attributes.GenerateTypeLibraryAttribute); if (generateAttribute is null) return GeneratorResult.Empty; @@ -62,7 +84,7 @@ CancellationToken cancellationToken // The generated type library inherits the full framework PurviewTypeLibrary shape, so every // framework member is present before the user's [TypeRef] members are merged in. The shape was - // already walked (once per compilation) by the GetFrameworkTypeLibraryTree stage. + // already walked (once per compilation) and memoised by GetFrameworkModel. AddFrameworkTree(framework.Tree, root, nodeLookup); foreach (var field in specSymbol.GetMembers().OfType()) @@ -124,7 +146,7 @@ static void ProcessTypeRefField( CancellationToken cancellationToken ) { - var typeRef = GetAttribute(field, GeneratorTypeLibrary.Attirbutes.TypeRefAttribute); + var typeRef = GetAttribute(field, GeneratorTypeLibrary.Attributes.TypeRefAttribute); if (typeRef is null) return; @@ -403,8 +425,8 @@ static void RewriteFrameworkCrefs(List nodes, EquatableArr /// /// Walks the framework PurviewTypeLibrary nested namespace classes and their members once, - /// producing a value-equatable model that is cached by the GetFrameworkTypeLibraryTree pipeline - /// stage. Each member is emitted as an alias reference to the framework value so arity and generic + /// producing a value-equatable model that is memoised per compilation by . + /// Each member is emitted as an alias reference to the framework value so arity and generic /// construction are preserved exactly. /// static EquatableArray BuildFrameworkTree(Compilation compilation) @@ -520,7 +542,7 @@ CancellationToken cancellationToken { cancellationToken.ThrowIfCancellationRequested(); - var enumValues = GetAttributes(field, GeneratorTypeLibrary.Attirbutes.EnumValueAttribute); + var enumValues = GetAttributes(field, GeneratorTypeLibrary.Attributes.EnumValueAttribute); if (enumValues.Count == 0) continue; @@ -582,7 +604,7 @@ CancellationToken cancellationToken // When the field also declares a [TypeRef], the attribute's first argument is the enum member // name and the enum type is inferred from the sibling [TypeRef] on the same field. - var typeRef = GetAttribute(field, GeneratorTypeLibrary.Attirbutes.TypeRefAttribute); + var typeRef = GetAttribute(field, GeneratorTypeLibrary.Attributes.TypeRefAttribute); var (declaredName, declaredNamespace, value, underlyingType, aliases) = ReadEnumValue(enumValue); if (declaredName is null) @@ -721,7 +743,7 @@ CancellationToken cancellationToken { cancellationToken.ThrowIfCancellationRequested(); - var enumValues = GetAttribute(field, GeneratorTypeLibrary.Attirbutes.EnumValuesAttribute); + var enumValues = GetAttribute(field, GeneratorTypeLibrary.Attributes.EnumValuesAttribute); if (enumValues is null) continue; diff --git a/src/src/SourceGeneratorFramework.Generators/Model/AttributeDataGenerationModel.cs b/src/src/SourceGeneratorFramework.Generators/Model/AttributeDataGenerationModel.cs deleted file mode 100644 index e69de29..0000000 diff --git a/src/src/SourceGeneratorFramework.Testing.TUnit/SourceGeneratorFramework.Testing.TUnit.csproj b/src/src/SourceGeneratorFramework.Testing.TUnit/SourceGeneratorFramework.Testing.TUnit.csproj index c90a875..bb7436c 100644 --- a/src/src/SourceGeneratorFramework.Testing.TUnit/SourceGeneratorFramework.Testing.TUnit.csproj +++ b/src/src/SourceGeneratorFramework.Testing.TUnit/SourceGeneratorFramework.Testing.TUnit.csproj @@ -3,6 +3,10 @@ netstandard2.0;$(TestingTargetFrameworks) true $(RootNamespace) + + true + 1.1.1 diff --git a/src/src/SourceGeneratorFramework.Testing/CompatibilitySuppressions.xml b/src/src/SourceGeneratorFramework.Testing/CompatibilitySuppressions.xml new file mode 100644 index 0000000..9f814a0 --- /dev/null +++ b/src/src/SourceGeneratorFramework.Testing/CompatibilitySuppressions.xml @@ -0,0 +1,23 @@ + + + + + + CP0001 + T:System.Diagnostics.CodeAnalysis.NotNullWhenAttribute + lib/netstandard2.0/Purview.SourceGeneratorFramework.Testing.dll + lib/netstandard2.0/Purview.SourceGeneratorFramework.Testing.dll + true + + diff --git a/src/src/SourceGeneratorFramework.Testing/Extensions/System/Diagnostics/CodeAnalysis/NotNullWhenAttribute.cs b/src/src/SourceGeneratorFramework.Testing/Extensions/System/Diagnostics/CodeAnalysis/NotNullWhenAttribute.cs index afd63c5..a89b0e8 100644 --- a/src/src/SourceGeneratorFramework.Testing/Extensions/System/Diagnostics/CodeAnalysis/NotNullWhenAttribute.cs +++ b/src/src/SourceGeneratorFramework.Testing/Extensions/System/Diagnostics/CodeAnalysis/NotNullWhenAttribute.cs @@ -2,8 +2,11 @@ using System.ComponentModel; -// netstandard2.0 declares NotNullWhenAttribute as internal, which makes it unusable as a -// public/override attribute; a public definition is required when targeting netstandard2.0. +// netstandard2.0 has no NotNullWhenAttribute, so it is polyfilled here. The C# compiler recognises the +// nullability attributes by name regardless of accessibility, so this is internal: a public polyfill +// would export a type from lib/netstandard2.0 that lib/net8.0 does not have, which is an API-surface +// difference between the package's own frameworks (CP0001) and makes the type appear to be part of this +// package's contract when it is an implementation detail. #pragma warning disable IDE0130 // Namespace does not match folder structure namespace System.Diagnostics.CodeAnalysis; @@ -11,7 +14,7 @@ namespace System.Diagnostics.CodeAnalysis; [EditorBrowsable(EditorBrowsableState.Never)] [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] -public sealed class NotNullWhenAttribute(bool returnValue) : Attribute +sealed class NotNullWhenAttribute(bool returnValue) : Attribute { public bool ReturnValue { get; } = returnValue; } diff --git a/src/src/SourceGeneratorFramework.Testing/SourceGeneratorFramework.Testing.csproj b/src/src/SourceGeneratorFramework.Testing/SourceGeneratorFramework.Testing.csproj index 9f4d14a..c9c1cc4 100644 --- a/src/src/SourceGeneratorFramework.Testing/SourceGeneratorFramework.Testing.csproj +++ b/src/src/SourceGeneratorFramework.Testing/SourceGeneratorFramework.Testing.csproj @@ -4,6 +4,10 @@ netstandard2.0;$(TestingTargetFrameworks) true $(RootNamespace) + + true + 1.1.1 diff --git a/src/src/SourceGeneratorFramework.Testing/SourceGeneratorHelpers.cs b/src/src/SourceGeneratorFramework.Testing/SourceGeneratorHelpers.cs index 7870c58..f744850 100644 --- a/src/src/SourceGeneratorFramework.Testing/SourceGeneratorHelpers.cs +++ b/src/src/SourceGeneratorFramework.Testing/SourceGeneratorHelpers.cs @@ -182,7 +182,9 @@ static ImmutableArray CreateMetadataReferences(string[] paths static ImmutableHashSet CollectDisplayPaths(ImmutableArray references) { +#pragma warning disable IDE0001 // Simplify Names var builder = ImmutableHashSet.CreateBuilder(StringComparer.OrdinalIgnoreCase); +#pragma warning restore IDE0001 // Simplify Names foreach (var reference in references) { if (reference.Display is { Length: > 0 } display) diff --git a/src/src/SourceGeneratorFramework.Testing/SourceGeneratorTestOptions.cs b/src/src/SourceGeneratorFramework.Testing/SourceGeneratorTestOptions.cs index 7c8ea1c..f031911 100644 --- a/src/src/SourceGeneratorFramework.Testing/SourceGeneratorTestOptions.cs +++ b/src/src/SourceGeneratorFramework.Testing/SourceGeneratorTestOptions.cs @@ -155,6 +155,25 @@ public SourceGeneratorTestOptions() /// public bool ThrowOnLogError { get; init; } = true; + /// + /// Gets a value indicating whether an incremental run reuses the same instance + /// across runs whose source sets are identical. + /// + /// + /// + /// The default is , which rebuilds the compilation for every run. That is what an + /// IDE does: every keystroke produces a new , and a pipeline stage only reports + /// Cached across those runs if its node value genuinely has value equality. + /// + /// + /// Setting this to reuses one instance for identical sources, which lets any stage + /// that holds an or a report Cached on reference + /// equality alone. That hides exactly the defect these tests exist to catch, so turn it on only when the + /// scenario is specifically about compilation-instance reuse, and say why at the call site. + /// + /// + public bool ReuseCompilationForIdenticalSources { get; init; } + /// /// Gets the analyzer-config property name used to disable the source generator. /// diff --git a/src/src/SourceGeneratorFramework.Testing/SourceGeneratorTestRunner.cs b/src/src/SourceGeneratorFramework.Testing/SourceGeneratorTestRunner.cs index bbc4b06..542f337 100644 --- a/src/src/SourceGeneratorFramework.Testing/SourceGeneratorTestRunner.cs +++ b/src/src/SourceGeneratorFramework.Testing/SourceGeneratorTestRunner.cs @@ -163,17 +163,29 @@ public IncrementalCacheResult RunIncremental( var driver = CreateDriver(generator, options, loggingSessionId); var references = SourceGeneratorHelpers.ResolveReferences(options, typeof(TGenerator).Assembly); var runs = ImmutableArray.CreateBuilder(materializedInputs.Count); - Dictionary compilationCache = new(StringComparer.Ordinal); + var compilationCache = options.ReuseCompilationForIdenticalSources + ? new Dictionary(StringComparer.Ordinal) + : null; foreach (var input in materializedInputs) { - // Reuse the compilation for identical source sets so Roslyn can report the unchanged pipeline - // stages as cached rather than modified on the second run. - var key = string.Join("\u0001", input.Sources.Select(source => PrepareSource(source, options))); - if (!compilationCache.TryGetValue(key, out var compilation)) - { + // A fresh compilation per run is the default because that is what an IDE produces on every + // keystroke. Reusing one instance for identical sources lets a stage holding a symbol or a + // Compilation report Cached on reference equality alone, which would mask the very regression + // these runs exist to detect. Opt in only where the scenario is about instance reuse itself. + Compilation compilation; + if (compilationCache is null) compilation = BuildCompilation(input.Sources, options, references, cancellationToken); - compilationCache[key] = compilation; + else + { + var key = string.Join("\u0001", input.Sources.Select(source => PrepareSource(source, options))); + if (!compilationCache.TryGetValue(key, out var cached)) + { + cached = BuildCompilation(input.Sources, options, references, cancellationToken); + compilationCache[key] = cached; + } + + compilation = cached; } if (input.AnalyzerConfig is not null) diff --git a/src/src/SourceGeneratorFramework/SourceGeneratorFramework.csproj b/src/src/SourceGeneratorFramework/SourceGeneratorFramework.csproj index 277c10a..e4b57eb 100644 --- a/src/src/SourceGeneratorFramework/SourceGeneratorFramework.csproj +++ b/src/src/SourceGeneratorFramework/SourceGeneratorFramework.csproj @@ -15,6 +15,12 @@ false + + true + 1.1.1 false $(TargetsForTfmSpecificContentInPackage);BuildAndPackBundledAnalyzerAssemblies diff --git a/src/src/SourceGeneratorShared/ComponentFailure.cs b/src/src/SourceGeneratorShared/ComponentFailure.cs new file mode 100644 index 0000000..8e0e3f8 --- /dev/null +++ b/src/src/SourceGeneratorShared/ComponentFailure.cs @@ -0,0 +1,196 @@ +using System.Collections.Concurrent; +using System.Globalization; +using System.Text; +using Microsoft.CodeAnalysis; + +namespace Purview.SourceGeneratorFramework; + +/// +/// Builds the a Roslyn component reports when it fails unexpectedly, +/// defaulting every part of it from the component's own identity. +/// +/// +/// +/// The defaults are derived from the consumer's codebase rather than from the framework, because the +/// diagnostic belongs to them: it appears in their build, they decide whether to suppress it, and an id in +/// the framework's own namespace would suggest the framework is at fault. A generator in +/// Acme.Widgets.Generators reports AWG0001, in category Acme. +/// +/// +/// An analyzer must declare this descriptor in SupportedDiagnostics, or Roslyn throws when it is +/// reported. is cached, so the instance an analyzer +/// declares is the same one the guard reports. +/// +/// +public static class ComponentFailure +{ + const string FallbackIdPrefix = "GEN"; + const string FallbackCategory = "SourceGeneration"; + + static readonly ConcurrentDictionary< + (Type Component, ComponentFailureOptions? Options), + DiagnosticDescriptor + > Descriptors = new(); + + /// + /// Gets the failure descriptor for , creating it on first use. + /// + public static DiagnosticDescriptor For(ComponentFailureOptions? options = null) => + For(typeof(TComponent), options); + + /// + /// Gets the failure descriptor for , creating it on first use. + /// + public static DiagnosticDescriptor For(Type componentType, ComponentFailureOptions? options = null) + { + if (componentType is null) + throw new ArgumentNullException(nameof(componentType)); + + // The descriptor is cached by component type and options, so the same instance is returned for the same + // component and options. This is important for analyzers, which must declare the same + // DiagnosticDescriptor instance in SupportedDiagnostics that they report at runtime. + return Descriptors.GetOrAdd((componentType, options), static key => Create(key.Component, key.Options)); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase")] + static DiagnosticDescriptor Create(Type componentType, ComponentFailureOptions? options) + { + var kind = DescribeKind(componentType); + + return new DiagnosticDescriptor( + options?.Id ?? (DeriveIdPrefix(componentType) + "0001"), + options?.Title ?? $"{kind} failed", + options?.MessageFormat + ?? $"{kind} '{{0}}' failed with '{{1}}': {{2}}. This is a defect in the {kind.ToLowerInvariant()}, not in the code being compiled.", + options?.Category ?? DeriveCategory(componentType), + options?.Severity ?? DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: options?.Description + ?? $"The {kind.ToLowerInvariant()} threw an unhandled exception. It has been reported as a diagnostic so the rest of the compilation still succeeds, rather than being surfaced as a compiler-level failure.", + helpLinkUri: options?.HelpLinkUri + ); + } + + /// + /// Names the kind of Roslyn component, for the diagnostic's title and message. + /// + static string DescribeKind(Type componentType) + { + foreach (var (typeName, label) in KnownComponentBaseTypes) + { + if (InheritsOrImplements(componentType, typeName)) + return label; + } + + return "Roslyn component"; + } + + static readonly (string TypeName, string Label)[] KnownComponentBaseTypes = + [ + ("Microsoft.CodeAnalysis.IIncrementalGenerator", "Source generator"), + ("Microsoft.CodeAnalysis.ISourceGenerator", "Source generator"), + ("Microsoft.CodeAnalysis.Diagnostics.DiagnosticSuppressor", "Diagnostic suppressor"), + ("Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer", "Analyzer"), + ("Microsoft.CodeAnalysis.CodeFixes.CodeFixProvider", "Code fix"), + ("Microsoft.CodeAnalysis.CodeRefactorings.CodeRefactoringProvider", "Code refactoring"), + ]; + + static bool InheritsOrImplements(Type componentType, string typeName) + { + for (var current = componentType; current is not null; current = current.BaseType) + { + if (string.Equals(current.FullName, typeName, StringComparison.Ordinal)) + return true; + } + + foreach (var implemented in componentType.GetInterfaces()) + { + if (string.Equals(implemented.FullName, typeName, StringComparison.Ordinal)) + return true; + } + + return false; + } + + /// + /// Derives a diagnostic id prefix from the component's namespace: the initial of each segment, upper + /// cased, capped at four characters. + /// + /// + /// + /// Acme.Widgets.Generators becomes AWG. A single-segment namespace uses its first three + /// letters instead, because a one-letter prefix reads like a compiler id. A type with no namespace at + /// all falls back to GEN. + /// + /// + /// The result is stable for a given namespace, which is what matters: a consumer suppressing the id in + /// an .editorconfig must not find it renamed by an unrelated change. It is not guaranteed unique + /// across two assemblies that share a namespace shape — set + /// when that matters. + /// + /// + public static string DeriveIdPrefix(Type componentType) + { + if (componentType is null) + throw new ArgumentNullException(nameof(componentType)); + + var @namespace = componentType.Namespace; + if (string.IsNullOrWhiteSpace(@namespace)) + return FallbackIdPrefix; + + var segments = @namespace.Split(['.'], StringSplitOptions.RemoveEmptyEntries); + if (segments.Length == 0) + return FallbackIdPrefix; + + if (segments.Length == 1) + { + var single = Letters(segments[0]); + + return single.Length >= 2 ? Upper(single.Substring(0, Math.Min(3, single.Length))) : FallbackIdPrefix; + } + + StringBuilder prefix = new(4); + foreach (var segment in segments) + { + var letters = Letters(segment); + if (letters.Length == 0) + continue; + + prefix.Append(char.ToUpperInvariant(letters[0])); + + if (prefix.Length == 4) + break; + } + + return prefix.Length >= 2 ? prefix.ToString() : FallbackIdPrefix; + } + + /// + /// Uses the component's root namespace as the diagnostic category, so failures group under the + /// consumer's own product. + /// + static string DeriveCategory(Type componentType) + { + var @namespace = componentType.Namespace; + if (string.IsNullOrWhiteSpace(@namespace)) + return FallbackCategory; + + var separator = @namespace.IndexOf('.'); + + return separator > 0 ? @namespace.Substring(0, separator) : @namespace; + } + + static string Letters(string value) + { + StringBuilder letters = new(value.Length); + foreach (var character in value) + { + if (char.IsLetter(character)) + letters.Append(character); + } + + return letters.ToString(); + } + + static string Upper(string value) => value.ToUpper(CultureInfo.InvariantCulture); +} diff --git a/src/src/SourceGeneratorShared/ComponentFailureOptions.cs b/src/src/SourceGeneratorShared/ComponentFailureOptions.cs new file mode 100644 index 0000000..28c41b3 --- /dev/null +++ b/src/src/SourceGeneratorShared/ComponentFailureOptions.cs @@ -0,0 +1,68 @@ +using Microsoft.CodeAnalysis; + +namespace Purview.SourceGeneratorFramework; + +/// +/// Overrides for the diagnostic a Roslyn component reports when it fails unexpectedly. Every member is +/// optional; anything left unset keeps the default derived from the component itself. +/// +/// +/// +/// These are code-level settings rather than build properties, and deliberately so. An analyzer has to +/// declare every diagnostic it can report in SupportedDiagnostics, which Roslyn reads before any +/// analyzer options exist — a build property could not reach it in time, and reporting an undeclared id +/// makes Roslyn throw. Declaring the descriptor in code is the only form that works for all four component +/// kinds. +/// +/// +public sealed record ComponentFailureOptions +{ + /// + /// Gets the diagnostic id. Defaults to a prefix derived from the component's own namespace, plus + /// 0001 — see . + /// + public string? Id { get; init; } + + /// + /// Gets the diagnostic title. Defaults to "<component kind> failed". + /// + public string? Title { get; init; } + + /// + /// Gets the message format. Receives the component name ({0}), the exception type name + /// ({1}) and the exception message ({2}). + /// + public string? MessageFormat { get; init; } + + /// + /// Gets the diagnostic category. Defaults to the component's root namespace, so a consumer's failures + /// group under their own product rather than under the framework. + /// + public string? Category { get; init; } + + /// + /// Gets the severity. Defaults to : a component that failed has + /// produced nothing, and failing the consumer's build on top of that rarely helps them. + /// + public DiagnosticSeverity? Severity { get; init; } + + /// + /// Gets the long description shown in IDE diagnostic details. + /// + public string? Description { get; init; } + + /// + /// Gets the help link shown alongside the diagnostic. + /// + public string? HelpLinkUri { get; init; } + + /// + /// Gets a value indicating whether the exception's stack trace is included in the reported message. + /// Defaults to . + /// + /// + /// A stack trace through a component's internals is noise to the consumer and makes the diagnostic + /// unstable between builds, so it is off by default. Turn it on when asking a consumer to report a bug. + /// + public bool IncludeStackTrace { get; init; } +} diff --git a/src/src/SourceGeneratorShared/ComponentGuard.cs b/src/src/SourceGeneratorShared/ComponentGuard.cs new file mode 100644 index 0000000..c8c89d8 --- /dev/null +++ b/src/src/SourceGeneratorShared/ComponentGuard.cs @@ -0,0 +1,204 @@ +using Microsoft.CodeAnalysis; + +namespace Purview.SourceGeneratorFramework; + +/// +/// Runs the body of a Roslyn component so an unexpected exception becomes a diagnostic rather than a +/// compiler-level failure. +/// +/// +/// +/// What an escaping exception costs depends on the component, and none of the outcomes are good. A source +/// generator produces CS8785 and emits nothing at all for that compilation, so the consumer also gets +/// every "type or namespace not found" error that follows. An analyzer produces AD0001. A code fix or +/// refactoring takes down the light-bulb. A suppressor silently stops suppressing, and the diagnostics it +/// was hiding reappear with no explanation. In every case the consumer is shown a stack trace through code +/// they do not own. +/// +/// +/// Not every component can report a diagnostic, so the guard offers two shapes rather than pretending to a +/// uniform one: +/// +/// +/// +/// and +/// report the failure, for +/// generators and analyzers. +/// +/// +/// returns whether the body +/// succeeded, for suppressors, code fixes and refactorings — none of which have a channel to report on. The +/// correct behaviour there is to do nothing: suppress nothing, offer no fix. +/// +/// +/// +/// Cancellation is never caught. Roslyn cancels analysis constantly as the user types, and swallowing an +/// would turn a normal cancellation into a reported defect. +/// +/// +public static class ComponentGuard +{ + /// + /// Runs , reporting any failure to . + /// + public static void Run( + SourceProductionContext context, + Action body, + ComponentFailureOptions? options = null + ) => Run(context, typeof(TComponent), body, options); + + /// + public static void Run( + SourceProductionContext context, + Type componentType, + Action body, + ComponentFailureOptions? options = null + ) + { + if (TryRun(componentType, body, out var failure, options, context.CancellationToken)) + return; + + if (failure is not null) + context.ReportDiagnostic(failure); + } + + /// + /// Runs , reporting any failure through . + /// + /// + /// For an analyzer, pass the ReportDiagnostic of whichever analysis context is in hand. The + /// descriptor must also appear in the analyzer's SupportedDiagnostics, or Roslyn throws when it + /// is reported — use to declare + /// the same instance. + /// + public static void Run( + Action report, + Action body, + ComponentFailureOptions? options = null, + CancellationToken cancellationToken = default + ) => Run(report, typeof(TComponent), body, options, cancellationToken); + + /// + public static void Run( + Action report, + Type componentType, + Action body, + ComponentFailureOptions? options = null, + CancellationToken cancellationToken = default + ) + { + if (report is null) + throw new ArgumentNullException(nameof(report)); + + if (TryRun(componentType, body, out var failure, options, cancellationToken)) + return; + + if (failure is not null) + report(failure); + } + + /// + /// Runs and reports whether it succeeded, for a component with no channel to + /// report a diagnostic on. + /// + /// + /// when the body completed; when it threw, with + /// describing it. + /// + /// + /// A suppressor, code fix or refactoring cannot report a diagnostic. The failure is handed back so the + /// caller can log it if it has a logger, but the behaviour that matters is simply to stop: a suppressor + /// suppresses nothing, a fix is not offered. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1021:Avoid out parameters")] + public static bool TryRun( + Action body, + out Diagnostic? failure, + ComponentFailureOptions? options = null, + CancellationToken cancellationToken = default + ) => TryRun(typeof(TComponent), body, out failure, options, cancellationToken); + + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1021:Avoid out parameters")] + public static bool TryRun( + Type componentType, + Action body, + out Diagnostic? failure, + ComponentFailureOptions? options = null, + CancellationToken cancellationToken = default + ) + { + if (componentType is null) + throw new ArgumentNullException(nameof(componentType)); + if (body is null) + throw new ArgumentNullException(nameof(body)); + + try + { + body(); + failure = null; + + return true; + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // Roslyn cancels analysis on every keystroke. This is not a defect and must propagate. + throw; + } +#pragma warning disable CA1031 // Catching a general exception is the entire point of a guard. + catch (Exception exception) +#pragma warning restore CA1031 + { + failure = Describe(componentType, exception, options); + + return false; + } + } + + /// + /// Resolves the component type behind a callback, skipping the compiler-generated closure types a lambda + /// is compiled into. + /// + /// + /// A lambda's declaring type is a closure (<>c, <>c__DisplayClass0_0) nested in + /// the type that wrote it. Walking out to the first type with a usable name gives the generator, which is + /// what names the diagnostic and supplies the namespace its id is derived from. + /// + public static Type ResolveComponentType(Delegate callback, Type fallback) + { + if (callback is null) + throw new ArgumentNullException(nameof(callback)); + if (fallback is null) + throw new ArgumentNullException(nameof(fallback)); + + var declaring = callback.Method.DeclaringType; + while (declaring is not null && declaring.Name.IndexOf('<') >= 0) + declaring = declaring.DeclaringType; + + return declaring ?? fallback; + } + + /// + /// Builds the diagnostic for a caught exception. + /// + public static Diagnostic Describe(Type componentType, Exception exception, ComponentFailureOptions? options = null) + { + if (exception is null) + throw new ArgumentNullException(nameof(exception)); + + var message = + options?.IncludeStackTrace == true + ? exception.Message + Environment.NewLine + exception.StackTrace + : exception.Message; + + return Diagnostic.Create( + ComponentFailure.For(componentType, options), + Location.None, + componentType.Name, + exception.GetType().Name, + // A message spanning lines breaks the single-line shape every diagnostic consumer expects. + message?.Replace("\r", " ").Replace("\n", " ") + ?? "no message" + ); + } +} diff --git a/src/src/SourceGeneratorShared/Extensions/Microsoft/CodeAnalysis/IncrementalGeneratorInitializationContextExtensions.cs b/src/src/SourceGeneratorShared/Extensions/Microsoft/CodeAnalysis/IncrementalGeneratorInitializationContextExtensions.cs index 761dae5..430ec08 100644 --- a/src/src/SourceGeneratorShared/Extensions/Microsoft/CodeAnalysis/IncrementalGeneratorInitializationContextExtensions.cs +++ b/src/src/SourceGeneratorShared/Extensions/Microsoft/CodeAnalysis/IncrementalGeneratorInitializationContextExtensions.cs @@ -74,6 +74,12 @@ public IncrementalGeneratorInitializationContext RegisterEmbeddedAttribute( /// successful results. This keeps generator methods thin /// and enforces the rule that diagnostics are reported in the source output stage. /// + /// + /// The callback is run through , so an unhandled exception becomes a + /// diagnostic on the consumer's build rather than a CS8785 that discards every file this + /// generator produces. Set to override that + /// diagnostic's id, severity or help link. + /// public IncrementalGeneratorInitializationContext RegisterSourceOutput( IncrementalValuesProvider> outputs, IncrementalValueProvider> contextProvider, @@ -100,7 +106,16 @@ public IncrementalGeneratorInitializationContext RegisterSourceOutput generate(spc, item.Output.Value, item.Context), + item.Context.Settings?.FailureOptions + ); } ); diff --git a/src/src/SourceGeneratorShared/GenerationSettings.cs b/src/src/SourceGeneratorShared/GenerationSettings.cs index f49429b..b431506 100644 --- a/src/src/SourceGeneratorShared/GenerationSettings.cs +++ b/src/src/SourceGeneratorShared/GenerationSettings.cs @@ -73,6 +73,19 @@ public GenerationSettings( /// public bool IsLoggingEnabled { get; init; } + /// + /// Gets the overrides for the diagnostic reported when a source-output callback throws, or + /// to take the defaults derived from the generator itself. + /// + /// + /// The framework's RegisterSourceOutput runs the callback through , + /// so an unhandled exception is reported as a diagnostic rather than escaping as CS8785 and + /// discarding every file the generator produced. These settings are where a generator names its own id, + /// severity or help link for that diagnostic — declared once, alongside everything else the generator + /// declares about itself. + /// + public ComponentFailureOptions? FailureOptions { get; init; } + /// /// Gets how generated code is indented. The default is . /// diff --git a/src/tests/Fixtures/Purview.ComponentClosureFixture/Fixture.Generator/.editorconfig b/src/tests/Fixtures/Purview.ComponentClosureFixture/Fixture.Generator/.editorconfig new file mode 100644 index 0000000..b511789 --- /dev/null +++ b/src/tests/Fixtures/Purview.ComponentClosureFixture/Fixture.Generator/.editorconfig @@ -0,0 +1,2714 @@ +# Purview.BuildSdk 1.1.0 - managed file, do not edit +# +# This file is written by Purview.BuildSdk and is replaced whenever the SDK version changes. The version +# on the first line is how the SDK recognises a file it owns: a .editorconfig without that marker is +# yours, and is never touched. +# +# To add your own rules, create a second .editorconfig in a subdirectory - `src/.editorconfig` is the +# usual choice - without `root = true`. EditorConfig applies the nearest file last, so your settings win +# over these, and nothing you write is lost when this file is updated. +# +# To stop the SDK updating this file, set one of: +# false keep this file as it is +# Never opt out of all bootstrapping +# false +# +# Deleting the first line also works: without the marker the SDK treats the file as yours. You then own +# every rule in it, including the ones a future SDK version would have fixed. +# +# See https://github.com/purview-dev/build-sdk#editorconfig for the full description. + +root = true + +# Base Configuration - General Settings, File Formatting, and Naming Conventions + +# All Files +[*] +charset = utf-8 +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_primary_constructors = true:suggestion +csharp_style_prefer_top_level_statements = true:silent +end_of_line = lf +indent_size = 4 +indent_style = tab +insert_final_newline = true +tab_width = 4 +trim_trailing_whitespace = true +max_line_length = 120 + +# Type members +dotnet_hide_advanced_members = false +dotnet_member_insertion_location = with_other_members_of_the_same_kind +dotnet_property_generation_behavior = prefer_throwing_properties + +# Symbol search +dotnet_search_reference_assemblies = true + +# Nullability settings +dotnet_build_property.Nullable = enable + +# Per-rule severity is authoritative in this file. The SDK sets AnalysisMode/AnalysisLevel as +# MSBuild properties, and the .NET SDK ignores bulk 'dotnet_analyzer_diagnostic.*' severity +# configuration whenever those properties are present. Do not re-add a bulk entry here: it +# silently does nothing and hides which severities are actually enforced. + +# Visual Studio XML Project Files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +# XML Configuration Files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct,xml,stylecop}] +indent_size = 2 + +# JSON Files +[*.{json,json5,webmanifest}] +indent_size = 2 + +# YAML Files +[*.{yml,yaml}] +indent_size = 2 + +# Markdown Files +[*.{md,mdx}] +trim_trailing_whitespace = false + +# Bash Files +[*.sh] +end_of_line = lf + +# Batch Files +[*.{cmd,bat}] +end_of_line = crlf + +# Web Files +[*.{htm,html,js,jsm,ts,tsx,cjs,cts,ctsx,mjs,mts,mtsx,css,sass,scss,less,pcss,svg,vue}] +indent_size = 2 +insert_final_newline = true + +# Makefiles +[Makefile] +indent_style = tab + +# .NET Style Rules - Naming Conventions +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/language-rules#net-style-rules + +[*.{cs,csx,cake,vb,vbx}] + +# Non-private static fields are PascalCase +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = warning +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields +dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case +# NOTE: the private-field rules that used to sit here referenced a style ('camel_case_underscore') +# and a symbol group ('private_fields') that were never defined, so they never applied. The real +# private-field rules are declared in the "StyleCop Field Naming Rules" section below. +dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected +dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field +dotnet_naming_symbols.non_private_static_fields.required_modifiers = static + +# Constants are PascalCase (field constants; local constants are locals and stay camelCase) +dotnet_naming_rule.constants_should_be_pascal_case.severity = warning +dotnet_naming_rule.constants_should_be_pascal_case.style = non_private_static_field_style +dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants +dotnet_naming_style.constant_style.capitalization = pascal_case +dotnet_naming_symbols.constants.applicable_kinds = field +dotnet_naming_symbols.constants.required_modifiers = const + +# Locals and parameters are camelCase +dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters +dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style +dotnet_naming_rule.locals_should_be_camel_case.severity = warning + +# camel_case_style - Define the camelCase style +dotnet_naming_style.camel_case_style.capitalization = camel_case +dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local + +# first_upper_style - The first character must start with an upper-case character +dotnet_naming_style.first_upper_style.capitalization = first_word_upper + +# prefix_interface_with_i_style - Interfaces must be PascalCase and first character must be 'I' +dotnet_naming_style.prefix_interface_with_i_style.required_prefix = I +dotnet_naming_style.prefix_interface_with_i_style.capitalization = pascal_case + +# prefix_type_parameters_with_t_style - Generic Type Parameters must be PascalCase and start with 'T' +dotnet_naming_style.prefix_type_parameters_with_t_style.capitalization = pascal_case +dotnet_naming_style.prefix_type_parameters_with_t_style.required_prefix = T + +# disallowed_style - Anything that has this style applied is marked as disallowed +dotnet_naming_style.disallowed_style.capitalization = pascal_case +dotnet_naming_style.disallowed_style.required_prefix = ____RULE_VIOLATION____ +dotnet_naming_style.disallowed_style.required_suffix = ____RULE_VIOLATION____ + +# internal_error_style - This style should never occur +dotnet_naming_style.internal_error_style.capitalization = pascal_case +dotnet_naming_style.internal_error_style.required_prefix = ____INTERNAL_ERROR____ +dotnet_naming_style.internal_error_style.required_suffix = ____INTERNAL_ERROR____ + +# All public/protected/protected_internal constant fields must be PascalCase +dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.severity = warning +dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.style = non_private_static_field_style +dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.symbols = public_protected_constant_fields_group +dotnet_naming_symbols.public_protected_constant_fields_group.applicable_accessibilities = public, protected, protected_internal +dotnet_naming_symbols.public_protected_constant_fields_group.applicable_kinds = field +dotnet_naming_symbols.public_protected_constant_fields_group.required_modifiers = const + +# All public/protected/protected_internal static readonly fields must be PascalCase +dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.severity = warning +dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.style = non_private_static_field_style +dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.symbols = public_protected_static_readonly_fields_group +dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_accessibilities = public, protected, protected_internal +dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_kinds = field +dotnet_naming_symbols.public_protected_static_readonly_fields_group.required_modifiers = static, readonly + +# No other public/protected/protected_internal fields are allowed +dotnet_naming_rule.other_public_protected_fields_disallowed_rule.severity = error +dotnet_naming_rule.other_public_protected_fields_disallowed_rule.style = disallowed_style +dotnet_naming_rule.other_public_protected_fields_disallowed_rule.symbols = other_public_protected_fields_group +dotnet_naming_symbols.other_public_protected_fields_group.applicable_accessibilities = public, protected, protected_internal +dotnet_naming_symbols.other_public_protected_fields_group.applicable_kinds = field + +# StyleCop Field Naming Rules + +# All non-private constant fields must be PascalCase. Private constants are owned by +# 'private_static_fields_group' below - a field must be covered by exactly one rule, otherwise the +# same violation is reported once per matching rule. +dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.severity = warning +dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.style = non_private_static_field_style +dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.symbols = stylecop_constant_fields_group +dotnet_naming_symbols.stylecop_constant_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected +dotnet_naming_symbols.stylecop_constant_fields_group.applicable_kinds = field +dotnet_naming_symbols.stylecop_constant_fields_group.required_modifiers = const + +# All non-private static readonly fields must be PascalCase. Private static readonly fields are +# owned by 'private_static_fields_group' below (see the note on the constant rule above). +dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.severity = warning +dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.style = non_private_static_field_style +dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.symbols = stylecop_static_readonly_fields_group +dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected +dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_kinds = field +dotnet_naming_symbols.stylecop_static_readonly_fields_group.required_modifiers = static, readonly + +# No non-private instance fields are allowed +dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.severity = error +dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.style = disallowed_style +dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.symbols = stylecop_fields_must_be_private_group +dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected +dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_kinds = field + +# Private static fields are PascalCase: they are type-level state and are never qualified with 'this.' +dotnet_naming_rule.private_static_fields_must_be_pascal_case_rule.severity = warning +dotnet_naming_rule.private_static_fields_must_be_pascal_case_rule.style = non_private_static_field_style +dotnet_naming_rule.private_static_fields_must_be_pascal_case_rule.symbols = private_static_fields_group +dotnet_naming_symbols.private_static_fields_group.applicable_accessibilities = private +dotnet_naming_symbols.private_static_fields_group.applicable_kinds = field +dotnet_naming_symbols.private_static_fields_group.required_modifiers = static + +# Private instance fields must be camelCase with a leading underscore: '_name', never 'name'. The +# prefix keeps field access unambiguous (so the noisy 'this.' qualifier is never needed) and keeps +# fields distinguishable from locals and parameters. +dotnet_naming_rule.private_instance_fields_must_be_camel_case_with_underscore_prefix.severity = warning +dotnet_naming_rule.private_instance_fields_must_be_camel_case_with_underscore_prefix.style = camel_case_underscore_style +dotnet_naming_rule.private_instance_fields_must_be_camel_case_with_underscore_prefix.symbols = private_instance_fields_group +dotnet_naming_symbols.private_instance_fields_group.applicable_accessibilities = private +dotnet_naming_symbols.private_instance_fields_group.applicable_kinds = field + +# Local variables must be camelCase +dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.severity = silent +dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.style = camel_case_style +dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.symbols = stylecop_local_fields_group +dotnet_naming_symbols.stylecop_local_fields_group.applicable_accessibilities = local +dotnet_naming_symbols.stylecop_local_fields_group.applicable_kinds = local + +# Sanity check - uncovered field case +dotnet_naming_rule.sanity_check_uncovered_field_case_rule.severity = error +dotnet_naming_rule.sanity_check_uncovered_field_case_rule.style = internal_error_style +dotnet_naming_rule.sanity_check_uncovered_field_case_rule.symbols = sanity_check_uncovered_field_case_group +dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_accessibilities = * +dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_kinds = field + +# All elements (namespaces, classes, enums, etc.) must be PascalCase +dotnet_naming_rule.element_rule.severity = warning +dotnet_naming_rule.element_rule.style = non_private_static_field_style +dotnet_naming_rule.element_rule.symbols = element_group +dotnet_naming_symbols.element_group.applicable_kinds = namespace, class, enum, struct, delegate, event, method, property + +# Interfaces use PascalCase and are prefixed with uppercase 'I' +dotnet_naming_rule.interface_rule.severity = warning +dotnet_naming_rule.interface_rule.style = prefix_interface_with_i_style +dotnet_naming_rule.interface_rule.symbols = interface_group +dotnet_naming_symbols.interface_group.applicable_kinds = interface + +# Generics Type Parameters use PascalCase and are prefixed with uppercase 'T' +dotnet_naming_rule.type_parameter_rule.severity = warning +dotnet_naming_rule.type_parameter_rule.style = prefix_type_parameters_with_t_style +dotnet_naming_rule.type_parameter_rule.symbols = type_parameter_group +dotnet_naming_symbols.type_parameter_group.applicable_kinds = type_parameter + +# Function parameters use camelCase +dotnet_naming_rule.parameters_rule.severity = warning +dotnet_naming_rule.parameters_rule.style = camel_case_style +dotnet_naming_rule.parameters_rule.symbols = parameters_group +dotnet_naming_symbols.parameters_group.applicable_kinds = parameter + +# Type Parameters +dotnet_naming_rule.type_parameter_naming.severity = warning +dotnet_naming_rule.type_parameter_naming.style = type_parameter_style +dotnet_naming_rule.type_parameter_naming.symbols = type_parameter_symbol +dotnet_naming_style.type_parameter_style.capitalization = pascal_case +dotnet_naming_style.type_parameter_style.required_prefix = T +dotnet_naming_symbols.type_parameter_symbol.applicable_accessibilities = * +dotnet_naming_symbols.type_parameter_symbol.applicable_kinds = type_parameter + +# Naming style shared by the private-field rules: camelCase with a required '_' prefix. +dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case +dotnet_naming_style.camel_case_underscore_style.required_prefix = _ + +# Local functions are PascalCase +dotnet_naming_rule.local_functions_should_be_pascal_case.severity = warning +dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style +dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions +dotnet_naming_style.local_function_style.capitalization = pascal_case +dotnet_naming_symbols.local_functions.applicable_kinds = local_function + +# "this." and "Me." qualifiers +dotnet_style_qualification_for_event = false:silent +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_property = false:silent + +# Undocumented +dotnet_style_operator_placement_when_wrapping = end_of_line + +# Naming styles +# NOTE: a 'types_should_be_pascal_case' rule (symbol group 'types') and an +# 'interface_should_be_begins_with_i' rule (symbol group 'interface') used to sit here, but neither +# symbol group was ever defined, so neither rule applied. Types are covered by 'element_rule' and +# interfaces by 'interface_rule' above. + +# By default, name items with PascalCase +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = warning +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = non_private_static_field_style +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members + +# pascal_case_style - Define the PascalCase style +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# Symbol specifications +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.required_modifiers = * + +# Naming styles +dotnet_naming_style.begins_with_i.capitalization = pascal_case +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = + +# Simplify interpolation +dotnet_diagnostic.IDE0071.severity = warning + +# Accessibility modifiers +dotnet_diagnostic.IDE0040.severity = warning + +# Allow multiple blank lines +dotnet_diagnostic.IDE2000.severity = silent + +# Code Quality Settings +dotnet_code_quality.enable_nullable_reference_types = true +dotnet_code_quality.enableNETAnalyzers = true +dotnet_code_quality.interpolated_string_composite_format = true +dotnet_code_quality.prefer_auto_properties = false +dotnet_code_quality.prefer_const = true +dotnet_code_quality.prefer_inferred_anonymous_type_member_names = true +dotnet_code_quality.prefer_inferred_tuple_names = true +dotnet_code_quality.prefer_readonly = true +# The real modifier policy is 'dotnet_style_require_accessibility_modifiers' (declared under +# "Modifier preferences" below). The 'require_accessibility_modifiers' and +# 'require_explicit_visibility' entries that used to live here are not .NET analyzer options: +# declaring them only pretended to enforce a policy that was never applied, so they were removed. +dotnet_code_quality.require_explicit_type_arguments = true +dotnet_code_quality.require_variable_declaration_for_explicit_type = false +dotnet_code_quality_unused_parameters = all:warning +dotnet_enable_roslyn_analyzers = true +dotnet_remove_unnecessary_suppression_exclusions = none + +# RS0016: Add public types and members to the declared API +dotnet_public_api_analyzer.require_api_files = true + +# Do not use generic CodeAction.Create to create CodeAction +dotnet_diagnostic.RS0005.severity = none + +# C# Specific Settings + +[*.{cs,csx,cake}] + +# Newline options +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# C# Unnecessary code rules +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent + +# Set the C# language version +csharp_language_version = latest + +# Prefer var for inferred expressions such as method calls. +# PDS0003 supplies the narrower explicit-type rule for object creation. +csharp_style_var_elsewhere = true:warning +csharp_style_var_for_built_in_types = true:warning +csharp_style_var_when_type_is_apparent = true:warning + +# Modifier preferences +csharp_prefer_static_anonymous_function = true +csharp_prefer_static_local_function = true:warning +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:warning +csharp_style_prefer_readonly_struct = true:suggestion +csharp_style_prefer_readonly_struct_member = true:suggestion +dotnet_style_readonly_field = true:warning +dotnet_style_require_accessibility_modifiers = omit_if_default:warning + +# Code-block preferences +csharp_prefer_braces = when_multiline:suggestion +csharp_prefer_simple_using_statement = false:silent +dotnet_style_prefer_collection_expression = true:suggestion + +# Prefer System.Threading.Lock +csharp_prefer_system_threading_lock = true:warning + +# Prefer parameter null checking +csharp_style_prefer_parameter_null_checking = true:suggestion + +# Prefer extended property pattern +csharp_style_prefer_extended_property_pattern = true:suggestion + +# Prefer UTF-8 string literals +csharp_style_prefer_utf8_string_literals = true:suggestion + +# Prefer tuple swap +csharp_style_prefer_tuple_swap = true:suggestion + +# Prefer local over anonymous function +csharp_style_prefer_local_over_anonymous_function = true:suggestion + +# Prefer unbound generic type in nameof +csharp_style_prefer_unbound_generic_type_in_nameof = true:warning + +# Expression-level preferences +csharp_prefer_simple_default_expression = true:warning +csharp_style_deconstructed_variable_declaration = true:warning +csharp_style_implicit_object_creation_when_type_is_apparent = true:warning +csharp_style_inlined_variable_declaration = true:warning +csharp_style_pattern_local_over_anonymous_function = true:warning +csharp_style_prefer_index_operator = true:warning +csharp_style_prefer_range_operator = true:warning +csharp_style_prefer_null_check_over_type_check = true:warning +csharp_style_throw_expression = true:warning + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:warning + +# New line preferences +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent +csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent + +# Expression-Bodied members +csharp_style_expression_bodied_accessors = true:warning +csharp_style_expression_bodied_constructors = true:warning +csharp_style_expression_bodied_indexers = true:warning +csharp_style_expression_bodied_lambdas = true:warning +csharp_style_expression_bodied_local_functions = true:warning +csharp_style_expression_bodied_methods = true:suggestion +csharp_style_expression_bodied_operators = true:warning +csharp_style_expression_bodied_properties = true:warning + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:warning +csharp_style_prefer_not_pattern = true:warning +csharp_style_prefer_pattern_matching = true:warning +csharp_style_prefer_switch_expression = true:warning +csharp_style_prefer_explicit_this = true:warning + +# "Null" checking preferences +csharp_style_conditional_delegate_call = true:warning + +# Spacing options +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrap options +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = false + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Namespace options +csharp_style_namespace_declarations = file_scoped:warning + +# Visual Basic Specific Settings + +[*.{vb}] + +visual_basic_preferred_modifier_order = Partial, Default, Private, Protected, Public, Friend, NotOverridable, Overridable, MustOverride, Overloads, Overrides, MustInherit, NotInheritable, Static, Shared, Shadows, ReadOnly, WriteOnly, Dim, Const, WithEvents, Widening, Narrowing, Custom, Async:warning +visual_basic_style_prefer_simplified_object_creation = all : suggestion +visual_basic_style_prefer_isnot_expression = true : suggestion + +# .NET Style Preferences + +[*.{cs,csx,cake,vb,vbx}] + +# Language keywords instead of framework type names for type references +dotnet_style_predefined_type_for_locals_parameters_members = true:warning +dotnet_style_predefined_type_for_member_access = true:warning + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:warning +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning + +dotnet_style_collection_initializer = true:warning +dotnet_style_explicit_tuple_names = true:warning +dotnet_style_object_initializer = true:warning +dotnet_style_prefer_auto_properties = true:warning +dotnet_style_prefer_compound_assignment = true:warning +dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_return = true:suggestion +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed:warning +dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning +dotnet_style_prefer_inferred_tuple_names = true:warning +dotnet_style_prefer_simplified_boolean_expressions = false:silent +dotnet_style_prefer_simplified_interpolation = true:warning + +# Expression-level preferences +dotnet_prefer_system_hash_code = true + +# Null-checking preferences +dotnet_style_coalesce_expression = true:warning +dotnet_style_null_propagation = true:warning +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning + +[*.g.cs] +# Generated source-generator output: any diagnostics are worthless, so treat as generated code. +generated_code = true +# Missing XML comment for publicly visible type or member 'Type_or_Member' +dotnet_diagnostic.CS1591.severity = none + +# Design Rules (CA1000-CA1070) +# Design rules support adherence to the .NET Framework design guidelines + + +[*.{cs,vb}] + +# Do not declare static members on generic types +dotnet_diagnostic.CA1000.severity = suggestion + +# Types that own disposable fields should be disposable +dotnet_diagnostic.CA1001.severity = error + +# Do not expose generic lists +dotnet_diagnostic.CA1002.severity = error + +# Use generic event handler instances +dotnet_diagnostic.CA1003.severity = warning + +# Avoid excessive parameters on generic types +dotnet_diagnostic.CA1005.severity = warning + +# Enums should have zero value +dotnet_diagnostic.CA1008.severity = error + +# Collections should implement generic interface +dotnet_diagnostic.CA1010.severity = warning + +# Abstract types should not have public constructors +dotnet_diagnostic.CA1012.severity = error + +# Mark assemblies with CLSCompliantAttribute +dotnet_diagnostic.CA1014.severity = silent + +# Mark assemblies with AssemblyVersionAttribute +dotnet_diagnostic.CA1016.severity = error + +# Mark assemblies with ComVisibleAttribute +dotnet_diagnostic.CA1017.severity = silent + +# Mark attributes with AttributeUsageAttribute +dotnet_diagnostic.CA1018.severity = suggestion + +# Define accessors for attribute arguments +dotnet_diagnostic.CA1019.severity = suggestion + +# Avoid out parameters +dotnet_diagnostic.CA1021.severity = suggestion + +# Use properties where appropriate +dotnet_diagnostic.CA1024.severity = suggestion + +# Mark enums with FlagsAttribute +dotnet_diagnostic.CA1027.severity = error + +# Enum storage should be Int32 +dotnet_diagnostic.CA1028.severity = error + +# Use events where appropriate +dotnet_diagnostic.CA1030.severity = suggestion + +# Do not catch general exception types +dotnet_diagnostic.CA1031.severity = error + +# Implement standard exception constructors +# Raised to warning: the public API surface of exceptions is part of the accessibility policy and +# must not be invisible to command-line builds. +dotnet_diagnostic.CA1032.severity = warning + +# Interface methods should be callable by child types +# Raised to warning: explicit interface implementations that hide the member from derived types are +# an accessibility problem, not a style preference. +dotnet_diagnostic.CA1033.severity = warning + +# Nested types should not be visible +dotnet_diagnostic.CA1034.severity = error + +# Override methods on comparable types +dotnet_diagnostic.CA1036.severity = suggestion + +# Avoid empty interfaces +# Raised to warning: empty interfaces are unenforceable contracts and were previously invisible to +# command-line builds. +dotnet_diagnostic.CA1040.severity = warning + +# Provide ObsoleteAttribute message +dotnet_diagnostic.CA1041.severity = warning + +# Use integral or string argument for indexers +dotnet_diagnostic.CA1043.severity = suggestion + +# Properties should not be write only +dotnet_diagnostic.CA1044.severity = error + +# Do not pass types by reference +dotnet_diagnostic.CA1045.severity = warning + +# Do not overload operator equals on reference types +dotnet_diagnostic.CA1046.severity = warning + +# Do not declare protected members in sealed types +dotnet_diagnostic.CA1047.severity = error + +# Declare types in namespaces +dotnet_diagnostic.CA1050.severity = error + +# Do not declare visible instance fields +dotnet_diagnostic.CA1051.severity = error + +# Static holder types should be Static or NotInheritable +dotnet_diagnostic.CA1052.severity = warning + +# Static holder types should not have default constructors +dotnet_diagnostic.CA1053.severity = warning + +# URI parameters should not be strings +dotnet_diagnostic.CA1054.severity = warning + +# URI return values should not be strings +dotnet_diagnostic.CA1055.severity = warning + +# URI properties should not be strings +dotnet_diagnostic.CA1056.severity = warning + +# Types should not extend certain base types +dotnet_diagnostic.CA1058.severity = warning + +# Move P/Invokes to NativeMethods class +dotnet_diagnostic.CA1060.severity = warning + +# Do not hide base class methods +dotnet_diagnostic.CA1061.severity = warning + +# Validate arguments of public methods +dotnet_code_quality.CA1062.api_surface = public, protected +dotnet_diagnostic.CA1062.severity = error + +# Implement IDisposable correctly +dotnet_diagnostic.CA1063.severity = error + +# Exceptions should be public +dotnet_diagnostic.CA1064.severity = warning + +# Do not raise exceptions in unexpected locations +dotnet_diagnostic.CA1065.severity = warning + +# Implement IEquatable when overriding Equals +dotnet_diagnostic.CA1066.severity = warning + +# Override Equals when implementing IEquatable +dotnet_diagnostic.CA1067.severity = warning + +# CancellationToken parameters must come last +dotnet_diagnostic.CA1068.severity = warning + +# Enums should not have duplicate values +dotnet_diagnostic.CA1069.severity = error + +# Do not declare event fields as virtual +dotnet_diagnostic.CA1070.severity = warning + +# Documentation Rules (CA1200) +# Documentation rules support writing well-documented libraries + + +[*.{cs,vb}] + +# Avoid using cref tags with a prefix +dotnet_diagnostic.CA1200.severity = suggestion + +# Globalization Rules (CA1303-CA1311 and CA2101) +# Globalization rules support world-ready libraries and applications + + +[*.{cs,vb}] + +# Do not pass literals as localized parameters +dotnet_code_quality.CA1303.use_naming_heuristic = true +dotnet_diagnostic.CA1303.severity = none + +# Specify CultureInfo +dotnet_diagnostic.CA1304.severity = error + +# Specify IFormatProvider +dotnet_diagnostic.CA1305.severity = error + +# Specify StringComparison for clarity +dotnet_diagnostic.CA1307.severity = error + +# Normalize strings to uppercase +dotnet_diagnostic.CA1308.severity = warning + +# Use ordinal StringComparison +dotnet_diagnostic.CA1309.severity = warning + +# Specify StringComparison for correctness +dotnet_diagnostic.CA1310.severity = warning + +# Specify a culture or use an invariant version +dotnet_diagnostic.CA1311.severity = error + +# Specify marshaling for P/Invoke string arguments +dotnet_diagnostic.CA2101.severity = warning + +# Interoperability Rules (CA1400-CA1422) +# Portability rules support portability across different platforms +# Interoperability rules support interaction with COM clients + + +[*.{cs,vb}] + +# P/Invokes should not be visible +dotnet_diagnostic.CA1401.severity = error + +# Validate platform compatibility +dotnet_diagnostic.CA1416.severity = suggestion + +# Do not use OutAttribute on string parameters for P/Invokes +dotnet_diagnostic.CA1417.severity = suggestion + +# Validate platform compatibility +dotnet_diagnostic.CA1418.severity = suggestion + +# Provide a parameterless constructor that is as visible as the containing type for concrete types derived from 'System.Runtime.InteropServices.SafeHandle' +dotnet_diagnostic.CA1419.severity = suggestion + +# Property, type, or attribute requires runtime marshalling +dotnet_diagnostic.CA1420.severity = suggestion + +# Method uses runtime marshalling when DisableRuntimeMarshallingAttribute is applied +dotnet_diagnostic.CA1421.severity = warning + +# Validate platform compatibility - obsoleted APIs +dotnet_diagnostic.CA1422.severity = warning + +# Maintainability Rules (CA1501-CA1516) +# Maintainability rules support library and application maintenance + + +[*.{cs,vb}] + +# Avoid excessive inheritance +dotnet_diagnostic.CA1501.severity = warning + +# Avoid excessive complexity +dotnet_diagnostic.CA1502.severity = error + +# Avoid unmaintainable code +dotnet_diagnostic.CA1505.severity = error + +# Avoid excessive class coupling +dotnet_diagnostic.CA1506.severity = error + +# Use nameof in place of string +dotnet_diagnostic.CA1507.severity = warning + +# Avoid dead conditional code +dotnet_diagnostic.CA1508.severity = error + +# Invalid entry in code metrics configuration file +dotnet_diagnostic.CA1509.severity = error + +# Use ArgumentNullException throw helper +dotnet_diagnostic.CA1510.severity = warning + +# Use ArgumentException throw helper +dotnet_diagnostic.CA1511.severity = warning + +# Use ArgumentOutOfRangeException throw helper +dotnet_diagnostic.CA1512.severity = warning + +# Use ObjectDisposedException throw helper +dotnet_diagnostic.CA1513.severity = warning + +# Avoid redundant length argument +dotnet_diagnostic.CA1514.severity = warning + +# Consider making public types internal +# Reported as a real build warning (not a suggestion) so accessibility hygiene cannot be ignored: +# a public type in an application or test assembly should be made internal rather than suppressed. +dotnet_diagnostic.CA1515.severity = warning + +# Naming Rules (CA1700-CA1727 and IDE0130) +# Naming rules support adherence to the naming conventions of the .NET design guidelines + + +[*.{cs,vb}] + +# Namespace does not match folder structure +dotnet_diagnostic.IDE0130.severity = warning + +# Do not name enum values 'Reserved' +dotnet_code_quality.CA1700.api_surface = public, protected +dotnet_diagnostic.CA1700.severity = error + +# Identifiers should not contain underscores +dotnet_code_quality.CA1707.api_surface = public, protected +dotnet_diagnostic.CA1707.severity = error + +# Identifiers should differ by more than case +dotnet_diagnostic.CA1708.severity = error + +# Identifiers should have correct suffix +dotnet_diagnostic.CA1710.severity = error + +# Identifiers should not have incorrect suffix +dotnet_diagnostic.CA1711.severity = error + +# Do not prefix enum values with type name +dotnet_diagnostic.CA1712.severity = error + +# Events should not have Before or After prefix +dotnet_diagnostic.CA1713.severity = error + +# Flags enums should have plural names +dotnet_diagnostic.CA1714.severity = error + +# Identifiers should have correct prefix +dotnet_diagnostic.CA1715.severity = error + +# Identifiers should not match keywords +dotnet_diagnostic.CA1716.severity = error + +# Only FlagsAttribute enums should have plural names +dotnet_diagnostic.CA1717.severity = error + +# Identifiers should not contain type names +dotnet_diagnostic.CA1720.severity = warning + +# Property names should not match get methods +dotnet_diagnostic.CA1721.severity = error + +# Type names should not match namespaces +dotnet_diagnostic.CA1724.severity = warning + +# Parameter names should match base declaration +dotnet_diagnostic.CA1725.severity = warning + +# Use PascalCase for named placeholders +dotnet_diagnostic.CA1727.severity = error + +# Performance Rules (CA1802-CA1877) +# Performance rules support high-performance libraries and applications + + + +[*.{cs,vb}] + +# Use Literals Where Appropriate +dotnet_diagnostic.CA1802.severity = suggestion + +# Do not initialize unnecessarily +dotnet_diagnostic.CA1805.severity = error + +# Do not ignore method results +dotnet_diagnostic.CA1806.severity = error + +# Initialize reference type static fields inline +dotnet_diagnostic.CA1810.severity = suggestion + +# Avoid uninstantiated internal classes +dotnet_diagnostic.CA1812.severity = suggestion + +# Avoid unsealed attributes +dotnet_diagnostic.CA1813.severity = suggestion + +# Prefer jagged arrays over multidimensional +dotnet_diagnostic.CA1814.severity = warning + +# Override equals and operator equals on value types +dotnet_diagnostic.CA1815.severity = warning + +# Properties should not return arrays +dotnet_diagnostic.CA1819.severity = warning +dotnet_code_quality.CA1819.api_surface = public, protected + +# Test for empty strings using string length +dotnet_diagnostic.CA1820.severity = error + +# Remove empty finalizers +dotnet_diagnostic.CA1821.severity = error + +# Mark members as static +dotnet_diagnostic.CA1822.severity = suggestion + +# Avoid unused private fields +dotnet_diagnostic.CA1823.severity = error + +# Mark assemblies with NeutralResourcesLanguageAttribute +dotnet_diagnostic.CA1824.severity = suggestion + +# Avoid zero-length array allocations +dotnet_diagnostic.CA1825.severity = suggestion + +# Use property instead of Linq Enumerable method +dotnet_diagnostic.CA1826.severity = error + +# Do not use Count/LongCount when Any can be used +dotnet_diagnostic.CA1827.severity = silent + +# Do not use CountAsync/LongCountAsync when AnyAsync can be used +dotnet_diagnostic.CA1828.severity = silent + +# Use Length/Count property instead of Enumerable.Count() method +dotnet_diagnostic.CA1829.severity = error + +# Prefer strongly-typed Append and Insert method overloads on StringBuilder +dotnet_diagnostic.CA1830.severity = warning + +# Use AsSpan instead of Range-based indexers for string when appropriate +dotnet_diagnostic.CA1831.severity = error + +# Use AsSpan or AsMemory instead of Range-based indexers for getting ReadOnlySpan or ReadOnlyMemory portion of an array * +dotnet_diagnostic.CA1832.severity = error + +# Use AsSpan or AsMemory instead of Range-based indexers for getting Span or Memory portion of an array +dotnet_diagnostic.CA1833.severity = error + +# Use StringBuilder.Append(char) for single character strings +dotnet_diagnostic.CA1834.severity = suggestion + +# Prefer the memory-based overloads of ReadAsync/WriteAsync methods in stream-based classes +dotnet_diagnostic.CA1835.severity = warning + +# Prefer IsEmpty over Count when available +dotnet_diagnostic.CA1836.severity = error + +# Use Environment.ProcessId instead of Process.GetCurrentProcess().Id +dotnet_diagnostic.CA1837.severity = suggestion + +# Avoid StringBuilder parameters for P/Invokes +dotnet_diagnostic.CA1838.severity = warning + +# Use Environment.ProcessPath instead of Process.GetCurrentProcess().MainModule.FileName +dotnet_diagnostic.CA1839.severity = error + +# Use Environment.CurrentManagedThreadId instead of Thread.CurrentThread.ManagedThreadId +dotnet_diagnostic.CA1840.severity = silent + +# Prefer Dictionary Contains methods +dotnet_diagnostic.CA1841.severity = silent + +# Do not use 'WhenAll' with a single task + * +dotnet_diagnostic.CA1842.severity = error + +# Do not use 'WaitAll' with a single task + * +dotnet_diagnostic.CA1843.severity = error + +# Provide memory-based overrides of async methods when subclassing 'Stream' * +dotnet_diagnostic.CA1844.severity = warning + +# Use span-based 'string.Concat' +dotnet_diagnostic.CA1845.severity = error + +# Prefer AsSpan over Substring +dotnet_diagnostic.CA1846.severity = error + +# CA1852 (seal internal types) is intentionally not suppressed for internal types that are exposed +# to other assemblies through InternalsVisibleTo: those types must still be sealed. The blanket +# suppression that used to sit here relied on a malformed key, so it never even applied. + +# Unsafe DataSet or DataTable in serializable type can be vulnerable to remote code execution attacks +dotnet_diagnostic.CA2352.severity = error + +# Unsafe DataSet or DataTable in serializable type +dotnet_diagnostic.CA2353.severity = error + +# Unsafe DataSet or DataTable in deserialized object graph can be vulnerable to remote code execution attack +dotnet_diagnostic.CA2354.severity = error + +# Unsafe DataSet or DataTable in deserialized object graph +dotnet_diagnostic.CA2355.severity = error + +# Unsafe DataSet or DataTable type in web deserialized object graph +dotnet_diagnostic.CA2356.severity = error + +# Ensure autogenerated class containing DataSet.ReadXml() is not used with untrusted data +dotnet_diagnostic.CA2361.severity = error + +# Unsafe DataSet or DataTable in autogenerated serializable type can be vulnerable to remote code execution attacks +dotnet_diagnostic.CA2362.severity = error + +# Review code for SQL injection vulnerabilities +dotnet_diagnostic.CA3001.severity = warning + +# Review code for XSS vulnerabilities +dotnet_diagnostic.CA3002.severity = warning + +# Review code for file path injection vulnerabilities +dotnet_diagnostic.CA3003.severity = warning + +# Review code for information disclosure vulnerabilities +dotnet_diagnostic.CA3004.severity = warning + +# Review code for LDAP injection vulnerabilities +dotnet_diagnostic.CA3005.severity = error + +# Review code for process command injection vulnerabilities +dotnet_diagnostic.CA3006.severity = error + +# Review code for open redirect vulnerabilities +dotnet_diagnostic.CA3007.severity = warning + +# Review code for XPath injection vulnerabilities +dotnet_diagnostic.CA3008.severity = error + +# Review code for XML injection vulnerabilities +dotnet_diagnostic.CA3009.severity = error + +# Review code for XAML injection vulnerabilities +dotnet_diagnostic.CA3010.severity = error + +# Review code for DLL injection vulnerabilities +dotnet_diagnostic.CA3011.severity = warning + +# Review code for regex injection vulnerabilities +dotnet_diagnostic.CA3012.severity = warning + +# Do Not Add Schema By URL +dotnet_diagnostic.CA3061.severity = warning + +# Insecure DTD processing in XML +dotnet_diagnostic.CA3075.severity = warning + +# Insecure XSLT script processing +dotnet_diagnostic.CA3076.severity = warning + +# Insecure Processing in API Design, XmlDocument and XmlTextReader +dotnet_diagnostic.CA3077.severity = warning + +# Mark Verb Handlers With Validate Antiforgery Token +dotnet_diagnostic.CA3147.severity = warning + +# Do Not Use Weak Cryptographic Algorithms +dotnet_diagnostic.CA5350.severity = warning + +# Do Not Use Broken Cryptographic Algorithms +dotnet_diagnostic.CA5351.severity = error + +# Do Not Use Unsafe Cipher Modes +dotnet_diagnostic.CA5358.severity = warning + +# Do Not Disable Certificate Validation +dotnet_diagnostic.CA5359.severity = warning + +# Do Not Call Dangerous Methods In Deserialization +dotnet_diagnostic.CA5360.severity = warning + +# Do Not Disable SChannel Use of Strong Crypto +dotnet_diagnostic.CA5361.severity = warning + +# Do Not Refer Self In Serializable Class +dotnet_diagnostic.CA5362.severity = error + +# Do Not Disable Request Validation +dotnet_diagnostic.CA5363.severity = warning + +# Do Not Use Deprecated Security Protocols +dotnet_diagnostic.CA5364.severity = error + +# Do Not Disable HTTP Header Checking +dotnet_diagnostic.CA5365.severity = warning + +# Use XmlReader For DataSet Read Xml +dotnet_diagnostic.CA5366.severity = warning + +# Do Not Serialize Types With Pointer Fields +dotnet_diagnostic.CA5367.severity = warning + +# Set ViewStateUserKey For Classes Derived From Page +dotnet_diagnostic.CA5368.severity = warning + +# Use XmlReader For Deserialize +dotnet_diagnostic.CA5369.severity = warning + +# Use XmlReader For Validating Reader +dotnet_diagnostic.CA5370.severity = warning + +# Use XmlReader For Schema Read +dotnet_diagnostic.CA5371.severity = warning + +# Use XmlReader For XPathDocument +dotnet_diagnostic.CA5372.severity = warning + +# Do not use obsolete key derivation function +dotnet_diagnostic.CA5373.severity = warning + +# Do Not Use XslTransform +dotnet_diagnostic.CA5374.severity = warning + +# Do Not Use Account Shared Access Signature +dotnet_diagnostic.CA5375.severity = warning + +# Use SharedAccessProtocol HttpsOnly +dotnet_diagnostic.CA5376.severity = warning + +# Use Container Level Access Policy +dotnet_diagnostic.CA5377.severity = warning + +# Do not disable ServicePointManagerSecurityProtocols +dotnet_diagnostic.CA5378.severity = warning + +# Do Not Use Weak Key Derivation Function Algorithm +dotnet_diagnostic.CA5379.severity = warning + +# Do Not Add Certificates To Root Store +dotnet_diagnostic.CA5380.severity = warning + +# Ensure Certificates Are Not Added To Root Store +dotnet_diagnostic.CA5381.severity = warning + +# Use Secure Cookies In ASP.Net Core +dotnet_diagnostic.CA5382.severity = warning + +# Ensure Use Secure Cookies In ASP.Net Core +dotnet_diagnostic.CA5383.severity = warning + +# Do Not Use Digital Signature Algorithm (DSA) +dotnet_diagnostic.CA5384.severity = warning + +# Use Rivest–Shamir–Adleman (RSA) Algorithm With Sufficient Key Size +dotnet_diagnostic.CA5385.severity = warning + +# Avoid hardcoding SecurityProtocolType value +dotnet_diagnostic.CA5386.severity = warning + +# Do Not Use Weak Key Derivation Function With Insufficient Iteration Count +dotnet_diagnostic.CA5387.severity = warning + +# Ensure Sufficient Iteration Count When Using Weak Key Derivation Function +dotnet_diagnostic.CA5388.severity = warning + +# Do Not Add Archive Item's Path To The Target File System Path +dotnet_diagnostic.CA5389.severity = warning + +# Do Not Hard Code Encryption Key +dotnet_diagnostic.CA5390.severity = error + +# Use antiforgery tokens in ASP.NET Core MVC controllers +dotnet_diagnostic.CA5391.severity = warning + +# Use DefaultDllImportSearchPaths attribute for P/Invokes +dotnet_diagnostic.CA5392.severity = warning + +# Do not use unsafe DllImportSearchPath value +dotnet_diagnostic.CA5393.severity = warning + +# Do not use insecure randomness +dotnet_diagnostic.CA5394.severity = warning + +# Miss HttpVerb attribute for action methods +dotnet_diagnostic.CA5395.severity = warning + +# Set HttpOnly to true for HttpCookie +dotnet_diagnostic.CA5396.severity = warning + +# Do not use deprecated SslProtocols values +dotnet_diagnostic.CA5397.severity = warning + +# Avoid hardcoded SslProtocols values +dotnet_diagnostic.CA5398.severity = warning + +# Definitely disable HttpClient certificate revocation list check +dotnet_diagnostic.CA5399.severity = warning + +# Ensure HttpClient certificate revocation list check is not disabled +dotnet_diagnostic.CA5400.severity = warning + +# Do not use CreateEncryptor with non-default IV +dotnet_diagnostic.CA5401.severity = suggestion + +# Use CreateEncryptor with the default IV +dotnet_diagnostic.CA5402.severity = warning + +# Do not hard-code certificate +dotnet_diagnostic.CA5403.severity = error + +# Do not disable token validation checks +dotnet_diagnostic.CA5404.severity = error + +# Do not always skip token validation in delegates +dotnet_diagnostic.CA5405.severity = error + +dotnet_diagnostic.SEC001.severity = error + +# SingleFile Rules (IL3000-IL3005) +# Single-file rules support single-file applications + + +[*.{cs,vb}] + +# Avoid using accessing Assembly file path when publishing as a single-file +dotnet_diagnostic.IL3000.severity = error + +# Avoid accessing Assembly file path when publishing as a single file +dotnet_diagnostic.IL3001.severity = error + +# Avoid calling members annotated with 'RequiresAssemblyFilesAttribute' when publishing as a single file +dotnet_diagnostic.IL3002.severity = error + +# RequiresAssemblyFilesAttribute annotations must match across all interface implementations or overrides +dotnet_diagnostic.IL3003.severity = error + +# RequiresAssemblyFilesAttribute cannot be placed directly on application entry point +dotnet_diagnostic.IL3005.severity = error + +# Style Rules (IDE0001-IDE0380) +# Style rules support consistent code style in your codebase + + +[*.{cs,vb}] + +# Simplify name +dotnet_diagnostic.IDE0001.severity = warning + +# Simplify member access +dotnet_diagnostic.IDE0002.severity = warning + +# this and Me preferences +dotnet_diagnostic.IDE0003.severity = suggestion +dotnet_diagnostic.IDE0009.severity = suggestion + +# Remove unnecessary cast +dotnet_diagnostic.IDE0004.severity = error + +# Remove unnecessary import +dotnet_diagnostic.IDE0005.severity = warning + +# Add missing cases to switch statement +dotnet_diagnostic.IDE0010.severity = warning + +# Add braces +dotnet_diagnostic.IDE0011.severity = silent + +# Use throw expression +dotnet_diagnostic.IDE0016.severity = warning + +# Use object initializers +dotnet_diagnostic.IDE0017.severity = suggestion + +# Inline variable declaration +dotnet_diagnostic.IDE0018.severity = suggestion + +# Use pattern matching to avoid 'as' followed by a 'null' check +dotnet_diagnostic.IDE0019.severity = warning + +# Use pattern matching to avoid 'is' check followed by a cast +dotnet_diagnostic.IDE0020.severity = warning + +# Use expression body for constructors +dotnet_diagnostic.IDE0021.severity = silent + +# Use expression body for methods +dotnet_diagnostic.IDE0022.severity = silent + +# Use expression body for operators +dotnet_diagnostic.IDE0023.severity = silent +dotnet_diagnostic.IDE0024.severity = silent + +# Use expression body for properties +dotnet_diagnostic.IDE0025.severity = silent + +# Use expression body for indexers +dotnet_diagnostic.IDE0026.severity = suggestion + +# Use expression body for accessors +dotnet_diagnostic.IDE0027.severity = silent + +# Use collection initializers +dotnet_diagnostic.IDE0028.severity = suggestion + +# Use coalesce expression +dotnet_diagnostic.IDE0029.severity = suggestion + +# Use coalesce expression (nullable types) +dotnet_diagnostic.IDE0030.severity = suggestion + +# Use null propagation +dotnet_diagnostic.IDE0031.severity = suggestion + +# Use auto property +dotnet_diagnostic.IDE0032.severity = suggestion + +# Use explicitly provided tuple name +dotnet_diagnostic.IDE0033.severity = suggestion + +# Simplify 'default' expression +dotnet_diagnostic.IDE0034.severity = suggestion + +# Remove unreachable code +dotnet_diagnostic.IDE0035.severity = error + +# Order modifiers +dotnet_diagnostic.IDE0036.severity = warning + +# Use inferred member name +dotnet_diagnostic.IDE0037.severity = suggestion + +# Use pattern matching to avoid is check followed by a cast (without variable) +dotnet_diagnostic.IDE0038.severity = warning + +# Use local function instead of lambda +dotnet_diagnostic.IDE0039.severity = suggestion + +# IDE0040 is declared exactly once, in the "Accessibility modifiers" section above. A second entry +# here used to override that value (later entries win), which made the effective modifier policy +# ambiguous and let repos believe the rule had been turned off. + +# Use is null check +dotnet_diagnostic.IDE0041.severity = suggestion + +# Deconstruct variable declaration +dotnet_diagnostic.IDE0042.severity = suggestion + +# Format string contains invalid placeholder +dotnet_diagnostic.IDE0043.severity = warning + +# Add readonly modifier +dotnet_diagnostic.IDE0044.severity = warning + +# Use conditional expression for assignment +dotnet_diagnostic.IDE0045.severity = suggestion + +# Use conditional expression for return +dotnet_diagnostic.IDE0046.severity = suggestion + +# Parentheses preferences +dotnet_diagnostic.IDE0047.severity = warning +dotnet_diagnostic.IDE0048.severity = warning + +# Use language keywords instead of framework type names for type references +dotnet_diagnostic.IDE0049.severity = warning + +# Convert anonymous type to tuple +dotnet_diagnostic.IDE0050.severity = suggestion + +# Remove unused private member +dotnet_diagnostic.IDE0051.severity = warning + +# Remove unread private member +dotnet_diagnostic.IDE0052.severity = error + +# Use expression body for lambdas +dotnet_diagnostic.IDE0053.severity = suggestion + +# Use compound assignment +dotnet_diagnostic.IDE0054.severity = suggestion + +# Fix formatting +dotnet_diagnostic.IDE0055.severity = suggestion + +# Use index operator +dotnet_diagnostic.IDE0056.severity = suggestion + +# Use range operator +dotnet_diagnostic.IDE0057.severity = suggestion + +# Remove unnecessary expression value +dotnet_diagnostic.IDE0058.severity = silent + +# Remove unnecessary value assignment +dotnet_diagnostic.IDE0059.severity = error + +# Remove unused parameter +dotnet_diagnostic.IDE0060.severity = error + +# Use expression body for local functions +dotnet_diagnostic.IDE0061.severity = suggestion + +# Make local function static +dotnet_diagnostic.IDE0062.severity = suggestion + +# Use simple 'using' statement +dotnet_diagnostic.IDE0063.severity = suggestion + +# Make struct fields writable +dotnet_diagnostic.IDE0064.severity = suggestion + +# 'using' directive placement +dotnet_diagnostic.IDE0065.severity = error + +# Use switch expression +dotnet_diagnostic.IDE0066.severity = warning + +# Use 'System.HashCode.Combine' +dotnet_diagnostic.IDE0070.severity = warning + +# Add missing cases to switch expression +dotnet_diagnostic.IDE0072.severity = suggestion + +# Require file header +dotnet_diagnostic.IDE0073.severity = warning + +# Use compound assignment +dotnet_diagnostic.IDE0074.severity = suggestion + +# Simplify conditional expression +dotnet_diagnostic.IDE0075.severity = suggestion + +# Remove invalid global 'SuppressMessageAttribute' +dotnet_diagnostic.IDE0076.severity = silent + +# Avoid legacy format target in global 'SuppressMessageAttribute' +dotnet_diagnostic.IDE0077.severity = warning + +# Use pattern matching +dotnet_diagnostic.IDE0078.severity = warning + +# Remove unnecessary suppression +dotnet_diagnostic.IDE0079.severity = error + +# Remove unnecessary suppression operator +dotnet_diagnostic.IDE0080.severity = suggestion + +# Remove ByVal +dotnet_diagnostic.IDE0081.severity = error + +# Convert typeof to nameof +dotnet_diagnostic.IDE0082.severity = warning + +# Use pattern matching (not operator) +dotnet_diagnostic.IDE0083.severity = warning + +# Use pattern matching (IsNot operator) +dotnet_diagnostic.IDE0084.severity = suggestion + +# Simplify new expression +dotnet_diagnostic.IDE0090.severity = warning + +# Remove unnecessary equality operator +dotnet_diagnostic.IDE0100.severity = silent + +# Use conditional delegate call +dotnet_diagnostic.IDE0105.severity = warning + +# Remove unnecessary discard +dotnet_diagnostic.IDE0110.severity = warning + +# Simplify LINQ expression +dotnet_diagnostic.IDE0120.severity = warning + +# Simplify LINQ type check and cast +dotnet_diagnostic.IDE0121.severity = warning + +# Simplify object creation +dotnet_diagnostic.IDE0140.severity = suggestion + +# Prefer 'null' check over type check +dotnet_diagnostic.IDE0150.severity = warning + +# Use block-scoped namespace +dotnet_diagnostic.IDE0160.severity = suggestion + +# Use file-scoped namespace +dotnet_diagnostic.IDE0161.severity = suggestion + +# Simplify property pattern +dotnet_diagnostic.IDE0170.severity = warning + +# Use tuple to swap values +dotnet_diagnostic.IDE0180.severity = suggestion + +# Unnecessary lambda expression +dotnet_diagnostic.IDE0200.severity = warning + +# Convert to top-level statements +dotnet_diagnostic.IDE0210.severity = warning + +# Convert to 'Program.Main' style program +dotnet_diagnostic.IDE0211.severity = suggestion + +# Add explicit cast in foreach loop +dotnet_diagnostic.IDE0220.severity = error + +# Use UTF-8 string literal +dotnet_diagnostic.IDE0230.severity = warning + +# Nullable directive is redundant +dotnet_diagnostic.IDE0240.severity = error + +# Nullable directive is unnecessary +dotnet_diagnostic.IDE0241.severity = error + +# Struct can be made 'readonly' +dotnet_diagnostic.IDE0250.severity = warning + +# Member can be made 'readonly' +dotnet_diagnostic.IDE0251.severity = warning + +# Use pattern matching +dotnet_diagnostic.IDE0260.severity = warning + +# Null check can be simplified +dotnet_diagnostic.IDE0270.severity = warning + +# Use 'nameof' +dotnet_diagnostic.IDE0280.severity = error + +# Use primary constructor +dotnet_diagnostic.IDE0290.severity = suggestion + +# Use collection expression for array +dotnet_diagnostic.IDE0300.severity = warning + +# Use collection expression for empty +dotnet_diagnostic.IDE0301.severity = suggestion + +# Use collection expression for stackalloc +dotnet_diagnostic.IDE0302.severity = warning + +# Use collection expression for Create() +dotnet_diagnostic.IDE0303.severity = warning + +# Use collection expression for builder +dotnet_diagnostic.IDE0304.severity = warning + +# Use collection expression for fluent +dotnet_diagnostic.IDE0305.severity = warning + +# Use collection expression for new +dotnet_diagnostic.IDE0306.severity = warning + +# Make anonymous function static +dotnet_diagnostic.IDE0320.severity = warning + +# Prefer 'System.Threading.Lock' +dotnet_diagnostic.IDE0330.severity = warning + +# Use unbound generic type +dotnet_diagnostic.IDE0340.severity = warning + +# Use implicitly typed lambda +dotnet_diagnostic.IDE0350.severity = warning + +# Simplify property accessor +dotnet_diagnostic.IDE0360.severity = suggestion + +# Remove unnecessary `unsafe` modifier +dotnet_diagnostic.IDE0380.severity = warning + +# Remove unnecessary suppression (null-forgiving operator) +dotnet_diagnostic.IDE0370.severity = warning + +# Naming rule violation - must stay visible: the field-naming policy is only enforced when this +# diagnostic is reported, because naming rules without their own severity inherit this one. +dotnet_diagnostic.IDE1006.severity = warning + +# Embedded statements must be on their own line +dotnet_diagnostic.IDE2001.severity = warning + +# Consecutive braces must not have blank line between them +dotnet_diagnostic.IDE2002.severity = warning + +# Blank line required between block and subsequent statement +dotnet_diagnostic.IDE2003.severity = warning + +# Blank line not allowed after constructor initializer colon +dotnet_diagnostic.IDE2004.severity = warning + +# Blank line not allowed after conditional expression token +dotnet_diagnostic.IDE2005.severity = warning + +# Blank line not allowed after arrow expression clause token +dotnet_diagnostic.IDE2006.severity = warning + +# C# Style Rules - var preferences + +[*.{cs,csx,cake}] + +dotnet_diagnostic.IDE0007.severity = warning +dotnet_diagnostic.IDE0008.severity = none +dotnet_diagnostic.PDS0003.severity = warning +dotnet_diagnostic.PDS0004.severity = warning + +# Usage Rules (CA1801, CA1816, CA2200-CA2267) +# Usage rules support proper usage of .NET + + +[*.{cs,vb}] + +# Review unused parameters +dotnet_diagnostic.CA1801.severity = warning + +# Call GC.SuppressFinalize correctly +dotnet_diagnostic.CA1816.severity = error + +# Rethrow to preserve stack details +dotnet_diagnostic.CA2200.severity = error + +# Do not raise reserved exception types +dotnet_diagnostic.CA2201.severity = error + +# Initialize value type static fields inline +dotnet_diagnostic.CA2207.severity = warning + +# Instantiate argument exceptions correctly +dotnet_diagnostic.CA2208.severity = error + +# Non-constant fields should not be visible +dotnet_diagnostic.CA2211.severity = error + +# Disposable fields should be disposed +dotnet_diagnostic.CA2213.severity = error + +# Do not call overridable methods in constructors +dotnet_diagnostic.CA2214.severity = warning + +# Dispose methods should call base class dispose +dotnet_diagnostic.CA2215.severity = error + +# Disposable types should declare finalizer +dotnet_diagnostic.CA2216.severity = warning + +# Do not mark enums with FlagsAttribute +dotnet_diagnostic.CA2217.severity = error + +# Override GetHashCode on overriding Equals +dotnet_diagnostic.CA2218.severity = warning + +# Do not raise exceptions in exception clauses +dotnet_diagnostic.CA2219.severity = error + +# Override Equals on overloading operator equals +dotnet_diagnostic.CA2224.severity = warning + +# Operator overloads have named alternates +dotnet_diagnostic.CA2225.severity = warning + +# Operators should have symmetrical overloads +dotnet_diagnostic.CA2226.severity = warning + +# Collection properties should be read only +dotnet_diagnostic.CA2227.severity = error + +# Implement serialization constructors +dotnet_diagnostic.CA2229.severity = warning + +# Overload operator equals on overriding ValueType.Equals +dotnet_diagnostic.CA2231.severity = warning + +# Pass System.Uri objects instead of strings +dotnet_diagnostic.CA2234.severity = warning + +# Mark all non-serializable fields +dotnet_diagnostic.CA2235.severity = suggestion + +# Mark ISerializable types with SerializableAttribute +dotnet_diagnostic.CA2237.severity = warning + +# Provide correct arguments to formatting methods +dotnet_diagnostic.CA2241.severity = warning + +# Test for NaN correctly +dotnet_diagnostic.CA2242.severity = warning + +# Attribute string literals should parse correctly +dotnet_diagnostic.CA2243.severity = warning + +# Do not duplicate indexed element initializations +dotnet_diagnostic.CA2244.severity = warning + +# Do not assign a property to itself +dotnet_diagnostic.CA2245.severity = error + +# Do not assign a symbol and its member in the same statement +dotnet_diagnostic.CA2246.severity = warning + +# Argument passed to TaskCompletionSource constructor should be TaskCreationOptions enum instead of TaskContinuationOptions enum +dotnet_diagnostic.CA2247.severity = warning + +# Provide correct enum argument to Enum.HasFlag +dotnet_diagnostic.CA2248.severity = warning + +# Consider using String.Contains instead of String.IndexOf +dotnet_diagnostic.CA2249.severity = error + +# Use ThrowIfCancellationRequested +dotnet_diagnostic.CA2250.severity = warning + +# Use String.Equals over String.Compare +dotnet_diagnostic.CA2251.severity = warning + +# Opt in to preview features +dotnet_diagnostic.CA2252.severity = suggestion + +# Named placeholders should not be numeric values +dotnet_diagnostic.CA2253.severity = warning + +# Template should be a static expression +dotnet_diagnostic.CA2254.severity = warning + +# The ModuleInitializer attribute should not be used in libraries +dotnet_diagnostic.CA2255.severity = warning + +# All members declared in parent interfaces must have an implementation in a DynamicInterfaceCastableImplementation-attributed interface +dotnet_diagnostic.CA2256.severity = warning + +# Members defined on an interface with 'DynamicInterfaceCastableImplementationAttribute' should be 'static' +dotnet_diagnostic.CA2257.severity = warning + +# Providing a 'DynamicInterfaceCastableImplementation' interface in Visual Basic is unsupported +dotnet_diagnostic.CA2258.severity = warning + +# Ensure ThreadStatic is only used with static fields +dotnet_diagnostic.CA2259.severity = error + +# Implement generic math interfaces correctly +dotnet_diagnostic.CA2260.severity = warning + +# Do not use ConfigureAwaitOptions.SuppressThrowing with Task +dotnet_diagnostic.CA2261.severity = warning + +# Set 'MaxResponseHeadersLength' properly +dotnet_diagnostic.CA2262.severity = warning + +# Do not pass a non-nullable value to 'ArgumentNullException.ThrowIfNull' +dotnet_diagnostic.CA2264.severity = warning + +# Do not compare Span to null or default +dotnet_diagnostic.CA2265.severity = error + +# Use correct type parameter +dotnet_diagnostic.CA2266.severity = warning + +# Use correct type parameter for 'IAsyncEnumerable.ConfigureAwait' +dotnet_diagnostic.CA2267.severity = warning + +# External Analyzer Rules +# Rules from StyleCop, Async, Dispose, Regex, JSON, MSTest, and other third-party analyzers + + + +[*.{cs,vb}] + +# StyleCop Analyzer Rules (SA) + +# A violation of this rule occurs when a compilation (project) contains files with DocumentationMode set to None +dotnet_diagnostic.SA0001.severity = warning + +# The spacing around a C# keyword is incorrect +dotnet_diagnostic.SA1000.severity = warning + +# DoNotPrefixCallsWithBaseUnlessLocalImplementationExists +dotnet_diagnostic.SA1100.severity = warning + +# StatementMustNotUseUnnecessaryParenthesis +dotnet_diagnostic.SA1119.severity = suggestion + +# CommentsMustContainText +dotnet_diagnostic.SA1120.severity = warning + +# DoNotUseRegions +dotnet_diagnostic.SA1124.severity = warning + +# UsingDirectivesMustBePlacedCorrectly +dotnet_diagnostic.SA1200.severity = warning + +# ElementsMustAppearInTheCorrectOrder +dotnet_diagnostic.SA1201.severity = warning + +# ElementsMustBeOrderedByAccess +dotnet_diagnostic.SA1202.severity = warning + +# ElementMustBeginWithUpperCaseLetter +dotnet_diagnostic.SA1300.severity = error + +# InterfaceNamesMustBeginWithI +dotnet_diagnostic.SA1302.severity = error + +# ConstFieldNamesMustBeginWithUpperCaseLetter +dotnet_diagnostic.SA1303.severity = error + +# FieldNamesMustNotUseHungarianNotation +dotnet_diagnostic.SA1305.severity = warning + +# VariableNamesMustNotBePrefixed +dotnet_diagnostic.SA1308.severity = error + +# A field name in C# begins with an underscore +dotnet_diagnostic.SA1309.severity = silent + +# VariableNamesMustBeginWithLowerCaseLetter +dotnet_diagnostic.SA1312.severity = warning + +# ParameterNamesMustBeginWithLowerCaseLetter +dotnet_diagnostic.SA1313.severity = error + +# TupleElementNamesShouldUseCorrectCasing +dotnet_diagnostic.SA1316.severity = warning + +# AccessModifierMustBeDeclared +dotnet_diagnostic.SA1400.severity = warning + +# FieldsMustBePrivate +dotnet_diagnostic.SA1401.severity = error + +# FileMayOnlyContainASingleNamespace +dotnet_diagnostic.SA1403.severity = error + +# A Code Analysis SuppressMessage attribute does not include a justification +dotnet_diagnostic.SA1404.severity = error + +# DebugAssertMustProvideMessageText +dotnet_diagnostic.SA1405.severity = warning + +# DebugFailMustProvideMessageText +dotnet_diagnostic.SA1406.severity = warning + +# RemoveUnnecessaryCode +dotnet_diagnostic.SA1409.severity = error + +# BracesForMultiLineStatementsMustNotShareLine +dotnet_diagnostic.SA1500.severity = error + +# ElementMustNotBeOnSingleLine +dotnet_diagnostic.SA1502.severity = error + +# BracesMustNotBeOmitted +dotnet_diagnostic.SA1503.severity = error + +# ClosingBraceMustBeFollowedByBlankLine +dotnet_diagnostic.SA1513.severity = warning + +# UseBracesConsistently +dotnet_diagnostic.SA1520.severity = error + +# File header copyright text should match +dotnet_diagnostic.SA1636.severity = none + +# ElementDocumentationMustBeSpelledCorrectly +dotnet_diagnostic.SA1650.severity = warning + +# Dispose Analyzer Rules (IDISP) + +# Dispose created +dotnet_diagnostic.IDISP001.severity = error + +# Dispose member +dotnet_diagnostic.IDISP002.severity = error + +# Dispose previous before re-assigning +dotnet_diagnostic.IDISP003.severity = error + +# Don't ignore created IDisposable +dotnet_diagnostic.IDISP004.severity = error + +# Return type should indicate that the value should be disposed +dotnet_diagnostic.IDISP005.severity = error + +# Implement IDisposable +dotnet_diagnostic.IDISP006.severity = error + +# Don't dispose injected +dotnet_diagnostic.IDISP007.severity = error + +# Don't assign member with injected and created disposables +dotnet_diagnostic.IDISP008.severity = error + +# Add IDisposable interface +dotnet_diagnostic.IDISP009.severity = error + +# Call base.Dispose(disposing) +dotnet_diagnostic.IDISP010.severity = error + +# Don't return disposed instance +dotnet_diagnostic.IDISP011.severity = warning + +# Property should not return created disposable +dotnet_diagnostic.IDISP012.severity = error + +# Await in using +dotnet_diagnostic.IDISP013.severity = error + +# Use a single instance of HttpClient +dotnet_diagnostic.IDISP014.severity = warning + +# Member should not return created and cached instance +dotnet_diagnostic.IDISP015.severity = error + +# Don't use disposed instance +dotnet_diagnostic.IDISP016.severity = error + +# Prefer using +dotnet_diagnostic.IDISP017.severity = error + +# Inline variable declaration +dotnet_diagnostic.IDISP018.severity = error + +# Call SuppressFinalize +dotnet_diagnostic.IDISP019.severity = error + +# Use pattern matching to avoid is check followed by a cast (with variable) +dotnet_diagnostic.IDISP020.severity = error + +# Call this.Dispose(true) +dotnet_diagnostic.IDISP021.severity = error + +# Call this.Dispose(false) +dotnet_diagnostic.IDISP022.severity = error + +# Don't use reference types in finalizer context +dotnet_diagnostic.IDISP023.severity = error + +# Don't call GC.SuppressFinalize(this) when the type is sealed and has no finalizer +dotnet_diagnostic.IDISP024.severity = error + +# Class with no virtual dispose method should be sealed +dotnet_diagnostic.IDISP025.severity = error + +# Class with no virtual DisposeAsyncCore method should be sealed +dotnet_diagnostic.IDISP026.severity = error + +# Async Analyzer Rules (ASYNC, VSTHRD, RCS) + +# Asynchronous method names should end with Async +dotnet_diagnostic.ASYNC0001.severity = error + +# Non asynchronous method names should not end with Async +dotnet_diagnostic.ASYNC0002.severity = error + +# Avoid void returning asynchronous method +dotnet_diagnostic.ASYNC0003.severity = warning + +# Use ConfigureAwait(false) on await expression +dotnet_diagnostic.ASYNC0004.severity = error + +# Do not use blocking call (make method async) +dotnet_diagnostic.MA0045.severity = none + +# Call 'ConfigureAwait(false)' +dotnet_diagnostic.RCS1090.severity = error + +# Return completed task instead of returning null +dotnet_diagnostic.RCS1210.severity = error + +# AsyncifyInvocation: Use Task Async +dotnet_diagnostic.AsyncifyInvocation.severity = error + +# AsyncifyVariable: Use Task Async +dotnet_diagnostic.AsyncifyVariable.severity = error + +# Avoid legacy thread switching methods +dotnet_diagnostic.VSTHRD001.severity = error + +# Avoid problematic synchronous waits +dotnet_diagnostic.VSTHRD002.severity = error + +# Avoid awaiting foreign Tasks +dotnet_diagnostic.VSTHRD003.severity = error + +# Await SwitchToMainThreadAsync +dotnet_diagnostic.VSTHRD004.severity = error + +# Invoke single-threaded types on Main thread +dotnet_diagnostic.VSTHRD010.severity = error + +# Use AsyncLazy +dotnet_diagnostic.VSTHRD011.severity = error + +# Provide JoinableTaskFactory where allowed +dotnet_diagnostic.VSTHRD012.severity = error + +# Avoid async void methods +dotnet_diagnostic.VSTHRD100.severity = error + +# Avoid unsupported async delegates +dotnet_diagnostic.VSTHRD101.severity = error + +# Implement internal logic asynchronously +dotnet_diagnostic.VSTHRD102.severity = error + +# Call async methods when in an async method +dotnet_diagnostic.VSTHRD103.severity = error + +# Offer async option +dotnet_diagnostic.VSTHRD104.severity = warning + +# Avoid method overloads that assume TaskScheduler.Current +dotnet_diagnostic.VSTHRD105.severity = warning + +# Use InvokeAsync to raise async events +dotnet_diagnostic.VSTHRD106.severity = error + +# Await Task within using expression +dotnet_diagnostic.VSTHRD107.severity = error + +# Assert thread affinity unconditionally +dotnet_diagnostic.VSTHRD108.severity = error + +# Switch instead of assert in async methods +dotnet_diagnostic.VSTHRD109.severity = error + +# Observe result of async calls +dotnet_diagnostic.VSTHRD110.severity = error + +# Use .ConfigureAwait(bool) +dotnet_diagnostic.VSTHRD111.severity = error + +# Implement System.IAsyncDisposable +dotnet_diagnostic.VSTHRD112.severity = error + +# Check for System.IAsyncDisposable +dotnet_diagnostic.VSTHRD113.severity = error + +# Avoid returning a null Task +dotnet_diagnostic.VSTHRD114.severity = error + +# Use "Async" suffix for async methods +dotnet_diagnostic.VSTHRD200.severity = error + +# Asynchronous method name should end with 'Async' +dotnet_diagnostic.RCS1046.severity = error + +# Non-asynchronous method name should not end with 'Async' +dotnet_diagnostic.RCS1047.severity = error + +# Class Analyzer Rules + +# Seal Class +dotnet_diagnostic.CLASS0001.severity = warning + +# Enum Analyzer Rules + +# Default switch label +dotnet_diagnostic.ENUM0001.severity = error + +# Merge switch sections +dotnet_diagnostic.ENUM0002.severity = warning + +# Populate switch +dotnet_diagnostic.ENUM0003.severity = warning + +# Regex Analyzer Rules (RE) + +# Invalid regex pattern +dotnet_diagnostic.RE0001.severity = warning + +# JSON Analyzer Rules + +# Invalid JSON pattern +dotnet_diagnostic.JSON001.severity = suggestion + +# Probable JSON string detected +dotnet_diagnostic.JSON002.severity = suggestion + +# Return Analyzer Rules + +# Do not return null +dotnet_diagnostic.RETURN0001.severity = warning + +# Roslynator Analyzer Rules (RCS) + +# Simplify boolean comparison +dotnet_diagnostic.RCS1049.severity = silent + +# Remove unnecessary 'Imports' or 'using' directive +dotnet_diagnostic.RemoveUnnecessaryImportsFixable.severity = warning + +# MSTest Analyzer Rules (MSTEST) + +# Explicitly enable or disable tests parallelization +dotnet_diagnostic.MSTEST0001.severity = warning + +# Test classes should have valid layout +dotnet_diagnostic.MSTEST0002.severity = warning + +# Test methods should have valid layout +dotnet_diagnostic.MSTEST0003.severity = warning + +# Public types should be test classes +dotnet_diagnostic.MSTEST0004.severity = none + +# Test context property should have valid layout +dotnet_diagnostic.MSTEST0005.severity = warning + +# Avoid [ExpectedException] +dotnet_diagnostic.MSTEST0006.severity = warning + +# Use test attributes only on test methods +dotnet_diagnostic.MSTEST0007.severity = warning + +# TestInitialize method should have valid layout +dotnet_diagnostic.MSTEST0008.severity = warning + +# TestCleanup method should have valid layout +dotnet_diagnostic.MSTEST0009.severity = warning + +# ClassInitialize method should have valid layout +dotnet_diagnostic.MSTEST0010.severity = warning + +# ClassCleanup method should have valid layout +dotnet_diagnostic.MSTEST0011.severity = warning + +# AssemblyInitialize method should have valid layout +dotnet_diagnostic.MSTEST0012.severity = warning + +# AssemblyCleanup method should have valid layout +dotnet_diagnostic.MSTEST0013.severity = warning + +# DataRow should be valid +dotnet_diagnostic.MSTEST0014.severity = warning + +# Test method should not be ignored +dotnet_diagnostic.MSTEST0015.severity = warning + +# Test class should have test method +dotnet_diagnostic.MSTEST0016.severity = warning + +# Assertion arguments should be passed in the correct order +dotnet_diagnostic.MSTEST0017.severity = warning + +# DynamicData should be valid +dotnet_diagnostic.MSTEST0018.severity = warning + +# Prefer TestInitialize methods over constructors +dotnet_diagnostic.MSTEST0019.severity = warning + +# Prefer constructors over TestInitialize methods +dotnet_diagnostic.MSTEST0020.severity = warning + +# Prefer Dispose over TestCleanup methods +dotnet_diagnostic.MSTEST0021.severity = warning + +# Prefer TestCleanup methods over Dispose +dotnet_diagnostic.MSTEST0022.severity = warning + +# Do not negate boolean assertions +dotnet_diagnostic.MSTEST0023.severity = warning + +# Do not store TestContext in a static member +dotnet_diagnostic.MSTEST0024.severity = warning + +# Use 'Assert.Fail' instead of an always-failing assert +dotnet_diagnostic.MSTEST0025.severity = warning + +# Avoid conditional access in assertions +dotnet_diagnostic.MSTEST0026.severity = warning + +# Non-nullable reference not initialized suppressor +dotnet_diagnostic.MSTEST0027.severity = none + +# Non-nullable reference not initialized suppressor +dotnet_diagnostic.MSTEST0028.severity = none + +# Public method should be test method +dotnet_diagnostic.MSTEST0029.severity = warning + +# Type containing [TestMethod] should be marked with [TestClass] +dotnet_diagnostic.MSTEST0030.severity = warning + +# System.ComponentModel.DescriptionAttribute has no effect on test methods +dotnet_diagnostic.MSTEST0031.severity = warning + +# Review or remove the assertion as its condition is known to be always true +dotnet_diagnostic.MSTEST0032.severity = warning + +# Non-nullable reference not initialized suppressor +dotnet_diagnostic.MSTEST0033.severity = none + +# Use ClassCleanupBehavior.EndOfClass with the [ClassCleanup] +dotnet_diagnostic.MSTEST0034.severity = warning + +# [DeploymentItem] can be specified only on test class or test method +dotnet_diagnostic.MSTEST0035.severity = warning + +# Do not use shadowing inside test class +dotnet_diagnostic.MSTEST0036.severity = warning + +# Use proper 'Assert' methods +dotnet_diagnostic.MSTEST0037.severity = warning + +# Don't use 'Assert.AreSame' or 'Assert.AreNotSame' with value types +dotnet_diagnostic.MSTEST0038.severity = warning + +# Use newer 'Assert.Throws' methods +dotnet_diagnostic.MSTEST0039.severity = warning + +# Do not assert inside 'async void' contexts +dotnet_diagnostic.MSTEST0040.severity = warning + +# Use 'ConditionBaseAttribute' on test classes +dotnet_diagnostic.MSTEST0041.severity = warning + +# Obsolete API Rules (SYSLIB) +# Rules for obsolete .NET APIs and platform-specific warnings + + +[*.{cs,vb}] + +# The UTF-7 encoding is insecure +dotnet_diagnostic.SYSLIB0001.severity = error + +# PrincipalPermissionAttribute is obsolete +dotnet_diagnostic.SYSLIB0002.severity = error + +# Code access security is not supported +dotnet_diagnostic.SYSLIB0003.severity = error + +# The constrained execution region (CER) feature is not supported +dotnet_diagnostic.SYSLIB0004.severity = error + +# The global assembly cache (GAC) is not supported +dotnet_diagnostic.SYSLIB0005.severity = error + +# Thread.Abort is not supported +dotnet_diagnostic.SYSLIB0006.severity = error + +# Default implementations of cryptography algorithms not supported +dotnet_diagnostic.SYSLIB0007.severity = error + +# CreatePdbGenerator is not supported +dotnet_diagnostic.SYSLIB0008.severity = error + +# The AuthenticationManager Authenticate and PreAuthenticate methods are not supported +dotnet_diagnostic.SYSLIB0009.severity = error + +# Unsupported remoting APIs +dotnet_diagnostic.SYSLIB0010.severity = error + +# BinaryFormatter serialization is obsolete +dotnet_diagnostic.SYSLIB0011.severity = error + +# Type or member is obsolete +dotnet_diagnostic.SYSLIB0012.severity = error + +# EscapeUriString is obsolete +dotnet_diagnostic.SYSLIB0013.severity = error + +# WebRequest, HttpWebRequest, ServicePoint, WebClient are obsolete +dotnet_diagnostic.SYSLIB0014.severity = error + +# DisablePrivateReflectionAttribute is obsolete +dotnet_diagnostic.SYSLIB0015.severity = error + +# GetContextInfo() is obsolete +dotnet_diagnostic.SYSLIB0016.severity = error + +# Strong-name signing is not supported and throws PlatformNotSupportedException +dotnet_diagnostic.SYSLIB0017.severity = error + +# Reflection-only loading is not supported and throws PlatformNotSupportedException +dotnet_diagnostic.SYSLIB0018.severity = error + +# Some RuntimeEnvironment APIs are obsolete +dotnet_diagnostic.SYSLIB0019.severity = error + +# IgnoreNullValues is obsolete +dotnet_diagnostic.SYSLIB0020.severity = error + +# Derived cryptographic types are obsolete +dotnet_diagnostic.SYSLIB0021.severity = error + +# The Rijndael and RijndaelManaged types are obsolete +dotnet_diagnostic.SYSLIB0022.severity = error + +# RNGCryptoServiceProvider is obsolete +dotnet_diagnostic.SYSLIB0023.severity = error + +# Creating and unloading AppDomains is not supported and throws an exception +dotnet_diagnostic.SYSLIB0024.severity = error + +# SuppressIldasmAttribute is obsolete +dotnet_diagnostic.SYSLIB0025.severity = error + +# X509Certificate and X509Certificate2 are immutable +dotnet_diagnostic.SYSLIB0026.severity = error + +# PublicKey.Key is obsolete +dotnet_diagnostic.SYSLIB0027.severity = error + +# X509Certificate2.PrivateKey is obsolete +dotnet_diagnostic.SYSLIB0028.severity = error + +# ProduceLegacyHmacValues is obsolete +dotnet_diagnostic.SYSLIB0029.severity = error + +# HMACSHA1 always uses the algorithm implementation provided by the platform +dotnet_diagnostic.SYSLIB0030.severity = error + +# EncodeOID is obsolete +dotnet_diagnostic.SYSLIB0031.severity = error + +# Recovery from corrupted process state exceptions is not supported +dotnet_diagnostic.SYSLIB0032.severity = error + +# Rfc2898DeriveBytes.CryptDeriveKey is obsolete +dotnet_diagnostic.SYSLIB0033.severity = error + +# CmsSigner(CspParameters) constructor is obsolete +dotnet_diagnostic.SYSLIB0034.severity = error + +# ComputeCounterSignature without specifying a CmsSigner is obsolete +dotnet_diagnostic.SYSLIB0035.severity = error + +# Regex.CompileToAssembly is obsolete +dotnet_diagnostic.SYSLIB0036.severity = error + +# AssemblyName members HashAlgorithm, ProcessorArchitecture, and VersionCompatibility are obsolete +dotnet_diagnostic.SYSLIB0037.severity = error + +# SerializationFormat.Binary is obsolete +dotnet_diagnostic.SYSLIB0038.severity = error + +# SslProtocols.Tls and SslProtocols.Tls11 are obsolete +dotnet_diagnostic.SYSLIB0039.severity = error + +# EncryptionPolicy.NoEncryption and EncryptionPolicy.AllowNoEncryption are obsolete +dotnet_diagnostic.SYSLIB0040.severity = error + +# Some Rfc2898DeriveBytes constructors are obsolete +dotnet_diagnostic.SYSLIB0041.severity = error + +# FromXmlString and ToXmlString on ECC types are obsolete +dotnet_diagnostic.SYSLIB0042.severity = error + +# ECDiffieHellmanPublicKey.ToByteArray is obsolete +dotnet_diagnostic.SYSLIB0043.severity = error + +# AssemblyName.CodeBase and AssemblyName.EscapedCodeBase are obsolete +dotnet_diagnostic.SYSLIB0044.severity = error + +# Some cryptographic factory methods are obsolete +dotnet_diagnostic.SYSLIB0045.severity = error + +# ControlledExecution.Run should not be used +dotnet_diagnostic.SYSLIB0046.severity = error + +# XmlSecureResolver is obsolete +dotnet_diagnostic.SYSLIB0047.severity = error + +# RSA.EncryptValue(Byte[]) and RSA.DecryptValue(Byte[]) are obsolete +dotnet_diagnostic.SYSLIB0048.severity = error + +# JsonSerializerOptions.AddContext is obsolete +dotnet_diagnostic.SYSLIB0049.severity = error + +# Formatter-based serialization is obsolete +dotnet_diagnostic.SYSLIB0050.severity = error + +# APIs that support obsolete formatter-based serialization are obsolete +dotnet_diagnostic.SYSLIB0051.severity = error + +# APIs that support obsolete mechanisms for Regex extensibility are obsolete +dotnet_diagnostic.SYSLIB0052.severity = error + +# AesGcm should indicate the required tag size for encryption and decryption +dotnet_diagnostic.SYSLIB0053.severity = error + +# Thread.VolatileRead and Thread.VolatileWrite are obsolete +dotnet_diagnostic.SYSLIB0054.severity = warning + +# AdvSimd.ShiftRightLogicalRoundedNarrowingSaturate* methods with signed parameters are obsolete +dotnet_diagnostic.SYSLIB0055.severity = warning + +# Assembly.LoadFrom that takes an AssemblyHashAlgorithm is obsolete +dotnet_diagnostic.SYSLIB0056.severity = warning + +# X509Certificate2 and X509Certificate constructors for binary and file content are obsolete +dotnet_diagnostic.SYSLIB0057.severity = warning + +# The KeyExchangeAlgorithm, KeyExchangeStrength, CipherAlgorithm, CipherAlgorithmStrength, HashAlgorithm, and HashStrength properties of SslStream are obsolete +dotnet_diagnostic.SYSLIB0058.severity = warning + +# SystemEvents.EventsThreadShutdown callbacks aren't run before the process exits +dotnet_diagnostic.SYSLIB0059.severity = warning + +# Rfc2898DeriveBytes constructors are obsolete +dotnet_diagnostic.SYSLIB0060.severity = error + +# System.Linq.Queryable.MaxBy and System.Linq.Queryable.MinBy taking an IComparer are obsolete +dotnet_diagnostic.SYSLIB0061.severity = error + +# Source Generator Diagnostics (SYSLIB1xxx) + +# Logging method names can't start with an underscore +dotnet_diagnostic.SYSLIB1001.severity = error + +# Don't include log level parameters as templates in the logging message +dotnet_diagnostic.SYSLIB1002.severity = error + +# Logging method parameter names can't start with an underscore +dotnet_diagnostic.SYSLIB1003.severity = error + +# Could not find a required type definition +dotnet_diagnostic.SYSLIB1005.severity = error + +# Multiple logging methods cannot use the same event ID +dotnet_diagnostic.SYSLIB1006.severity = error + +# Logging methods must return void +dotnet_diagnostic.SYSLIB1007.severity = error + +# One of the arguments to a logging method must implement the ILogger interface +dotnet_diagnostic.SYSLIB1008.severity = error + +# Logging methods must be static +dotnet_diagnostic.SYSLIB1009.severity = error + +# Logging methods must be partial +dotnet_diagnostic.SYSLIB1010.severity = error + +# Logging methods cannot be generic +dotnet_diagnostic.SYSLIB1011.severity = error + +# Redundant qualifier in logging message +dotnet_diagnostic.SYSLIB1012.severity = error + +# Don't include exception parameters as templates in the logging message +dotnet_diagnostic.SYSLIB1013.severity = error + +# Logging template has no corresponding method argument +dotnet_diagnostic.SYSLIB1014.severity = error + +# Argument is not referenced from the logging message +dotnet_diagnostic.SYSLIB1015.severity = error + +# Logging methods cannot have a body +dotnet_diagnostic.SYSLIB1016.severity = error + +# A LogLevel value must be supplied in the LoggerMessage attribute or as a parameter to the logging method +dotnet_diagnostic.SYSLIB1017.severity = error + +# Don't include logger parameters as templates in the logging message +dotnet_diagnostic.SYSLIB1018.severity = error + +# Couldn't find a field of type ILogger +dotnet_diagnostic.SYSLIB1019.severity = error + +# Found multiple fields of type ILogger +dotnet_diagnostic.SYSLIB1020.severity = error + +# Multiple message-template item names differ only by case +dotnet_diagnostic.SYSLIB1021.severity = error + +# Can't have malformed format strings +dotnet_diagnostic.SYSLIB1022.severity = error + +# Generating more than six arguments is not supported +dotnet_diagnostic.SYSLIB1023.severity = error + +# System.Text.Json source generator did not generate output for type +dotnet_diagnostic.SYSLIB1030.severity = error + +# System.Text.Json source generator encountered a duplicate type info property name +dotnet_diagnostic.SYSLIB1031.severity = error + +# Context classes to be augmented by the System.Text.Json source generator must be declared as partial +dotnet_diagnostic.SYSLIB1032.severity = error + +# System.Text.Json source generator encountered a type with multiple [JsonConstructor] annotations +dotnet_diagnostic.SYSLIB1033.severity = warning + +# JsonSourceGenerator encountered a [JsonStringEnumConverter] annotation +dotnet_diagnostic.SYSLIB1034.severity = warning + +# System.Text.Json source generator encountered a type with multiple [JsonExtensionData] annotations +dotnet_diagnostic.SYSLIB1035.severity = error + +# System.Text.Json source generator encountered an invalid [JsonExtensionData] annotation +dotnet_diagnostic.SYSLIB1036.severity = error + +# System.Text.Json source generator encountered a type with init-only properties which are not supported for deserialization +dotnet_diagnostic.SYSLIB1037.severity = error + +# System.Text.Json source generator encountered a property annotated with [JsonInclude] but with inaccessible accessors +dotnet_diagnostic.SYSLIB1038.severity = error + +# Invalid GeneratedRegexAttribute usage +dotnet_diagnostic.SYSLIB1040.severity = error + +# Multiple GeneratedRegexAttribute attributes were applied to the same method, but only one is allowed +dotnet_diagnostic.SYSLIB1041.severity = error + +# The specified regular expression is invalid +dotnet_diagnostic.SYSLIB1042.severity = error + +# A GeneratedRegexAttribute method must be partial, parameterless, non-generic, and non-abstract, and return Regex +dotnet_diagnostic.SYSLIB1043.severity = error + +# The regex generator couldn't generate a complete source implementation for the specified regular expression due to an internal limitation +dotnet_diagnostic.SYSLIB1044.severity = error + +# Use GeneratedRegexAttribute to generate the regular expression implementation at compile time +dotnet_diagnostic.SYSLIB1045.severity = warning + +# Invalid LibraryImportAttribute usage +dotnet_diagnostic.SYSLIB1050.severity = error + +# The specified type is not supported by source-generated p/invokes +dotnet_diagnostic.SYSLIB1051.severity = error + +# The specified configuration is not supported by source-generated p/invokes +dotnet_diagnostic.SYSLIB1052.severity = error + +# The specified LibraryImportAttribute arguments cannot be forwarded to DllImportAttribute +dotnet_diagnostic.SYSLIB1053.severity = error + +# Use LibraryImportAttribute instead of DllImportAttribute to generate p/invoke marshalling code at compile time +dotnet_diagnostic.SYSLIB1054.severity = error + +# Invalid CustomMarshallerAttribute usage +dotnet_diagnostic.SYSLIB1055.severity = error + +# The specified native type is invalid +dotnet_diagnostic.SYSLIB1056.severity = error + +# The marshaller type does not have the required shape +dotnet_diagnostic.SYSLIB1057.severity = error + +# Invalid NativeMarshallingAttribute usage +dotnet_diagnostic.SYSLIB1058.severity = error + +# The marshaller type does not support an allocating constructor +dotnet_diagnostic.SYSLIB1059.severity = error + +# The specified marshaller type is invalid +dotnet_diagnostic.SYSLIB1060.severity = error + +# The marshaller type has incompatible method signatures +dotnet_diagnostic.SYSLIB1061.severity = error + +# The project must be updated with true +dotnet_diagnostic.SYSLIB1062.severity = error + +# Invalid JSImportAttribute usage +dotnet_diagnostic.SYSLIB1070.severity = error + +# Invalid JSExportAttribute usage +dotnet_diagnostic.SYSLIB1071.severity = error + +# The specified type is not supported by source-generated JavaScript interop +dotnet_diagnostic.SYSLIB1072.severity = error + +# The specified configuration is not supported by source-generated JavaScript interop +dotnet_diagnostic.SYSLIB1073.severity = error + +# JSImportAttribute requires unsafe code +dotnet_diagnostic.SYSLIB1074.severity = error + +# JSExportAttribute requires unsafe code +dotnet_diagnostic.SYSLIB1075.severity = error + +# Invalid GeneratedComInterfaceAttribute usage +dotnet_diagnostic.SYSLIB1090.severity = error + +# Method is declared in different partial declaration than the GeneratedComInterface attribute +dotnet_diagnostic.SYSLIB1091.severity = error + +# Usage of LibraryImport or GeneratedComInterface attribute does not follow recommendation +dotnet_diagnostic.SYSLIB1092.severity = error + +# Analysis for COM interface generation has failed +dotnet_diagnostic.SYSLIB1093.severity = error + +# The base COM interface failed to generate source. Code will not be generated for this interface +dotnet_diagnostic.SYSLIB1094.severity = error + +# Invalid GeneratedComClassAttribute usage +dotnet_diagnostic.SYSLIB1095.severity = error + +# Use GeneratedComInterfaceAttribute instead of ComImportAttribute to generate COM marshalling code at compile time +dotnet_diagnostic.SYSLIB1096.severity = error + +# This type implements at least one type with the GeneratedComInterfaceAttribute attribute +dotnet_diagnostic.SYSLIB1097.severity = error + +# .NET COM hosting with EnableComHosting only supports built-in COM interop +dotnet_diagnostic.SYSLIB1098.severity = error + +# COM Interop APIs on System.Runtime.InteropServices.Marshal do not support source-generated COM and will fail at run time +dotnet_diagnostic.SYSLIB1099.severity = error + +# Type is not supported +dotnet_diagnostic.SYSLIB1100.severity = error + +# Property on type is not supported +dotnet_diagnostic.SYSLIB1101.severity = error + +# Project's language version must be at least C# 11 +dotnet_diagnostic.SYSLIB1102.severity = error + +# Value types are invalid inputs to configuration 'Bind' methods +dotnet_diagnostic.SYSLIB1103.severity = error + +# Generator cannot determine the target configuration type +dotnet_diagnostic.SYSLIB1104.severity = error + +# Can't use ValidateObjectMembersAttribute or ValidateEnumeratedItemsAttribute on fields or properties with open generic types +dotnet_diagnostic.SYSLIB1201.severity = error + +# A member type has no fields or properties to validate +dotnet_diagnostic.SYSLIB1202.severity = error + +# A type has no fields or properties to validate +dotnet_diagnostic.SYSLIB1203.severity = error + +# Deriving from a GeneratedComInterface-attributed interface defined in another assembly is not supported +dotnet_diagnostic.SYSLIB1230.severity = error + +# Other Platform Diagnostics + +# SVE is a preview feature can be used by enabling EnablePreviewFeatures flag +dotnet_diagnostic.SYSLIB5003.severity = error + +# CS9035 - should be warning or error +dotnet_diagnostic.CS9035.severity = none + +# For Verify package based content +[*.{received,verified}.{cs,txt}] +charset = "utf-8-bom" +end_of_line = lf +indent_size = unset +indent_style = unset +insert_final_newline = false +tab_width = unset +trim_trailing_whitespace = false + +# The following sections are about +# ignoring certain rules for specific files or folders, such as settings, models, DTOs, etc. +# including when using the Extensions/ folder for matching non-local namespace extensions such as +# - Extensions/System/Linq/EnumerableExtensions.cs +# - Extensions/Microsoft/Extensions/DepdendencyInjection/ServiceCollectionExtensions.cs + +# If it's a settings, model, dto, etc file, ignore the 'properties' cannot have arrays +# and other annoying rules - the following section is duplicated +[**/*{ValueObjects,Settings,Options,Model,Models,DTO,Entity,Response,Request}{.Nav,}.{cs,vb}] +dotnet_diagnostic.CA1002.severity = none +dotnet_diagnostic.CA1024.severity = none +dotnet_diagnostic.CA1056.severity = none +dotnet_diagnostic.CA1819.severity = none +dotnet_diagnostic.CA2227.severity = none +dotnet_diagnostic.CS8618.severity = none +dotnet_diagnostic.CS8767.severity = none + +[**/{ValueObjects,Migrations,Entities,Models,DTOs,Settings,Options,Configuration,Requests,Responses}/**.{cs,vb}] +dotnet_diagnostic.CA1002.severity = none +dotnet_diagnostic.CA1024.severity = none +dotnet_diagnostic.CA1056.severity = none +dotnet_diagnostic.CA1819.severity = none +dotnet_diagnostic.CA2227.severity = none +dotnet_diagnostic.CS8618.severity = none +dotnet_diagnostic.CS8767.severity = none + +[**/Migrations/**.{cs,vb}] +# EF Core migrations (and their .Designer.cs snapshots) are generated; any diagnostics are worthless. +generated_code = true +dotnet_diagnostic.CA1062.severity = none +dotnet_diagnostic.CA1825.severity = none +dotnet_diagnostic.CA1861.severity = none +dotnet_diagnostic.IDE0053.severity = none +dotnet_diagnostic.IDE0300.severity = none +dotnet_diagnostic.RCS1021.severity = none +dotnet_diagnostic.RCS1205.severity = none +# PDS0004: Use correct acronym capitalization +dotnet_diagnostic.PDS0004.severity = none + +[**/Extensions/**.{cs,vb}] +# Files under the Extensions/ namespace-reset convention must not force developers to add pragmas; +# suppress only the namespace-conflict diagnostics that arise from declaring framework-style +# namespaces. Accessibility/design rules (for example CA1034) are deliberately not suppressed here. +dotnet_diagnostic.IDE0130.severity = none +dotnet_diagnostic.CA1724.severity = none +dotnet_diagnostic.CS0436.severity = none +dotnet_diagnostic.CS1591.severity = none +dotnet_diagnostic.IDE0005.severity = none + +[**/Generated/**/*.{cs,vb}] +generated_code = true +dotnet_diagnostic.CS8602.severity = none + +[**/*.Designer.cs] +# Designer/design-time generated content +generated_code = true + +[**/obj/**/*.cs] +[**/bin/**/*.cs] +# Compiler/build output (generated AssemblyInfo, GlobalUsings, etc.) +generated_code = true diff --git a/src/tests/Fixtures/Purview.ComponentClosureFixture/Fixture.Generator/global.json b/src/tests/Fixtures/Purview.ComponentClosureFixture/Fixture.Generator/global.json new file mode 100644 index 0000000..0346864 --- /dev/null +++ b/src/tests/Fixtures/Purview.ComponentClosureFixture/Fixture.Generator/global.json @@ -0,0 +1,8 @@ +{ +"test": { +"runner": "Microsoft.Testing.Platform" +}, +"msbuild-sdks": { +"Purview.BuildSdk": "1.1.0" +} +} diff --git a/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/AvoidSymbolsInPipelineModelAnalyzerTests.cs b/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/AvoidSymbolsInPipelineModelAnalyzerTests.cs index 8ad0361..b35592a 100644 --- a/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/AvoidSymbolsInPipelineModelAnalyzerTests.cs +++ b/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/AvoidSymbolsInPipelineModelAnalyzerTests.cs @@ -76,4 +76,120 @@ public sealed record NotAModel(ISymbol Symbol); await Assert.That(result).HasNoDiagnostics(); } + + /// + /// The shape the framework's own generators used: the transform returns the symbol directly, so there + /// is no named model type for AnalyzeModelType to walk and the rule used to stay silent. + /// + [Test] + public async Task TransformReturningSymbolDirectly_ReportsDiagnostic(CancellationToken cancellationToken) + { + const string source = """ + using Microsoft.CodeAnalysis; + + public class MyGenerator : IIncrementalGenerator + { + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var pipeline = GetPipeline(context); + } + + IncrementalValuesProvider GetPipeline(IncrementalGeneratorInitializationContext context) => + context.SyntaxProvider.ForAttributeWithMetadataName( + "System.ObsoleteAttribute", + static (node, _) => true, + static (ctx, _) => (INamedTypeSymbol)ctx.TargetSymbol + ); + } + """; + + var result = await AnalyzeAsync(source, cancellationToken); + + await Assert.That(result).HasDiagnostic(AvoidSymbolsInPipelineModelAnalyzer.Rule.Id); + } + + /// + /// A tuple carrying the symbol through a combine. The tuple is not a named model type either. + /// + [Test] + public async Task TupleCarryingSymbolThroughCombine_ReportsDiagnostic(CancellationToken cancellationToken) + { + const string source = """ + using Microsoft.CodeAnalysis; + + public class MyGenerator : IIncrementalGenerator + { + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var pipeline = GetPipeline(context); + } + + IncrementalValuesProvider<(INamedTypeSymbol Symbol, string Name)> GetPipeline( + IncrementalGeneratorInitializationContext context + ) => + context.SyntaxProvider.ForAttributeWithMetadataName( + "System.ObsoleteAttribute", + static (node, _) => true, + static (ctx, _) => ((INamedTypeSymbol)ctx.TargetSymbol, ctx.TargetSymbol.Name) + ); + } + """; + + var result = await AnalyzeAsync(source, cancellationToken); + + await Assert.That(result).HasDiagnostic(AvoidSymbolsInPipelineModelAnalyzer.Rule.Id); + } + + /// + /// A Select that projects the compilation into the pipeline value. + /// + [Test] + public async Task SelectProjectingCompilation_ReportsDiagnostic(CancellationToken cancellationToken) + { + const string source = """ + using Microsoft.CodeAnalysis; + + public class MyGenerator : IIncrementalGenerator + { + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var pipeline = context.CompilationProvider.Select(static (compilation, _) => compilation); + } + } + """; + + var result = await AnalyzeAsync(source, cancellationToken); + + await Assert.That(result).HasDiagnostic(AvoidSymbolsInPipelineModelAnalyzer.Rule.Id); + } + + /// + /// A transform that extracts an equatable value is the fix, and must stay silent. + /// + [Test] + public async Task TransformReturningEquatableValue_DoesNotReportDiagnostic(CancellationToken cancellationToken) + { + const string source = """ + using Microsoft.CodeAnalysis; + + public class MyGenerator : IIncrementalGenerator + { + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var pipeline = GetPipeline(context); + } + + IncrementalValuesProvider GetPipeline(IncrementalGeneratorInitializationContext context) => + context.SyntaxProvider.ForAttributeWithMetadataName( + "System.ObsoleteAttribute", + static (node, _) => true, + static (ctx, _) => ctx.TargetSymbol.ToDisplayString() + ); + } + """; + + var result = await AnalyzeAsync(source, cancellationToken); + + await Assert.That(result).HasNoDiagnostics(); + } } diff --git a/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/DoNotThrowFromGeneratorAnalyzerTests.cs b/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/DoNotThrowFromGeneratorAnalyzerTests.cs new file mode 100644 index 0000000..440e182 --- /dev/null +++ b/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/DoNotThrowFromGeneratorAnalyzerTests.cs @@ -0,0 +1,372 @@ +using Purview.SourceGeneratorFramework.Testing.TUnit; + +namespace Purview.SourceGeneratorFramework.Analyzers; + +public sealed class DoNotThrowFromGeneratorAnalyzerTests + : TUnitDiagnosticAnalyzerTestBase +{ + [Test] + public async Task ThrowInInitialize_ReportsDiagnostic(CancellationToken cancellationToken) + { + const string source = """ + using System; + using Microsoft.CodeAnalysis; + + public class MyGenerator : IIncrementalGenerator + { + public void Initialize(IncrementalGeneratorInitializationContext context) + { + throw new InvalidOperationException("nope"); + } + } + """; + + var result = await AnalyzeAsync(source, cancellationToken); + + await Assert.That(result).HasDiagnostic(DoNotThrowFromGeneratorAnalyzer.Rule.Id); + } + + /// + /// The shape the framework's own generator used: a throw in a switch expression inside an emit + /// helper, several calls away from the pipeline and not inside any lambda. + /// + [Test] + public async Task ThrowInAGeneratorHelperMethod_ReportsDiagnostic(CancellationToken cancellationToken) + { + const string source = """ + using System; + using Microsoft.CodeAnalysis; + + public class MyGenerator : IIncrementalGenerator + { + public void Initialize(IncrementalGeneratorInitializationContext context) { } + + static string Describe(int kind) => + kind switch + { + 0 => "zero", + _ => throw new InvalidOperationException("unsupported"), + }; + } + """; + + var result = await AnalyzeAsync(source, cancellationToken); + + await Assert.That(result).HasDiagnostic(DoNotThrowFromGeneratorAnalyzer.Rule.Id); + } + + /// + /// A lambda handed to a pipeline method runs during generation wherever it is written, so the rule does + /// not depend on it sitting inside the generator type. + /// + [Test] + public async Task ThrowInAPipelineLambdaOutsideAGeneratorType_ReportsDiagnostic(CancellationToken cancellationToken) + { + const string source = """ + using System; + using Microsoft.CodeAnalysis; + + public static class PipelineHelper + { + public static IncrementalValuesProvider Build( + IncrementalGeneratorInitializationContext context + ) => + context.SyntaxProvider.ForAttributeWithMetadataName( + "System.ObsoleteAttribute", + static (node, _) => true, + static (ctx, _) => + { + if (ctx.TargetSymbol.Name.Length == 0) + throw new InvalidOperationException("nope"); + + return ctx.TargetSymbol.Name; + } + ); + } + """; + + var result = await AnalyzeAsync(source, cancellationToken); + + await Assert.That(result).HasDiagnostic(DoNotThrowFromGeneratorAnalyzer.Rule.Id); + } + + [Test] + public async Task RethrowInAGenerator_ReportsDiagnostic(CancellationToken cancellationToken) + { + const string source = """ + using System; + using Microsoft.CodeAnalysis; + + public class MyGenerator : IIncrementalGenerator + { + public void Initialize(IncrementalGeneratorInitializationContext context) + { + try + { + Work(); + } + catch (InvalidOperationException) + { + throw; + } + } + + static void Work() { } + } + """; + + var result = await AnalyzeAsync(source, cancellationToken); + + await Assert.That(result).HasDiagnostic(DoNotThrowFromGeneratorAnalyzer.Rule.Id); + } + + /// + /// The cancellation guard in front of a broad catch: the single most correct thing a generator can do, + /// and the shape this rule flagged as a defect until a consuming repository caught it. + /// + [Test] + public async Task CancellationRethrowAheadOfABroadCatch_DoesNotReportDiagnostic(CancellationToken cancellationToken) + { + const string source = """ + using System; + using Microsoft.CodeAnalysis; + + public class MyGenerator : IIncrementalGenerator + { + static readonly DiagnosticDescriptor Rule = new( + "ABC0001", + "t", + "t", + "c", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + public void Initialize(IncrementalGeneratorInitializationContext context) { } + + static void ExecuteSafely(SourceProductionContext context) + { + try + { + Emit(context); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception) + { + context.ReportDiagnostic(Diagnostic.Create(Rule, Location.None)); + } + } + + static void Emit(SourceProductionContext context) { } + } + """; + + var result = await AnalyzeAsync(source, cancellationToken); + + await Assert.That(result).HasNoDiagnostics(); + } + + /// + /// TaskCanceledException derives from OperationCanceledException, so it is cancellation too. + /// + [Test] + public async Task TaskCanceledRethrow_DoesNotReportDiagnostic(CancellationToken cancellationToken) + { + const string source = """ + using System; + using System.Threading.Tasks; + using Microsoft.CodeAnalysis; + + public class MyGenerator : IIncrementalGenerator + { + public void Initialize(IncrementalGeneratorInitializationContext context) + { + try + { + Work(); + } + catch (TaskCanceledException) + { + throw; + } + } + + static void Work() { } + } + """; + + var result = await AnalyzeAsync(source, cancellationToken); + + await Assert.That(result).HasNoDiagnostics(); + } + + /// + /// Throwing cancellation directly is also propagation, not a defect. + /// + [Test] + public async Task ThrowingOperationCanceledDirectly_DoesNotReportDiagnostic(CancellationToken cancellationToken) + { + const string source = """ + using System; + using Microsoft.CodeAnalysis; + + public class MyGenerator : IIncrementalGenerator + { + public void Initialize(IncrementalGeneratorInitializationContext context) + { + throw new OperationCanceledException(); + } + } + """; + + var result = await AnalyzeAsync(source, cancellationToken); + + await Assert.That(result).HasNoDiagnostics(); + } + + /// + /// A rethrow from a broad catch still escapes, and is still a defect. + /// + [Test] + public async Task RethrowFromABroadCatch_StillReportsDiagnostic(CancellationToken cancellationToken) + { + const string source = """ + using System; + using Microsoft.CodeAnalysis; + + public class MyGenerator : IIncrementalGenerator + { + public void Initialize(IncrementalGeneratorInitializationContext context) + { + try + { + Work(); + } + catch (Exception) + { + throw; + } + } + + static void Work() { } + } + """; + + var result = await AnalyzeAsync(source, cancellationToken); + + await Assert.That(result).HasDiagnostic(DoNotThrowFromGeneratorAnalyzer.Rule.Id); + } + + /// + /// Reporting a diagnostic is the sanctioned alternative and must stay silent. + /// + [Test] + public async Task ReportingADiagnosticInsteadOfThrowing_DoesNotReportDiagnostic(CancellationToken cancellationToken) + { + const string source = """ + using Microsoft.CodeAnalysis; + + public class MyGenerator : IIncrementalGenerator + { + static readonly DiagnosticDescriptor Rule = new( + "ABC0001", + "t", + "t", + "c", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + public void Initialize(IncrementalGeneratorInitializationContext context) => + context.RegisterSourceOutput( + context.CompilationProvider, + static (spc, compilation) => spc.ReportDiagnostic(Diagnostic.Create(Rule, Location.None)) + ); + } + """; + + var result = await AnalyzeAsync(source, cancellationToken); + + await Assert.That(result).HasNoDiagnostics(); + } + + /// + /// An analyzer is not a generator: its exceptions become AD0001, which is a separate concern this + /// rule deliberately does not cover, and its Initialize guard is the established pattern. + /// + [Test] + public async Task ThrowInAnAnalyzer_DoesNotReportDiagnostic(CancellationToken cancellationToken) + { + const string source = """ + using System; + using System.Collections.Immutable; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.Diagnostics; + + public class MyAnalyzer : DiagnosticAnalyzer + { + public override ImmutableArray SupportedDiagnostics => []; + + public override void Initialize(AnalysisContext context) + { + if (context is null) + throw new ArgumentNullException(nameof(context)); + } + } + """; + + var result = await AnalyzeAsync(source, cancellationToken); + + await Assert.That(result).HasNoDiagnostics(); + } + + /// + /// An ordinary class that happens to sit in the same assembly is not generation code. + /// + [Test] + public async Task ThrowInAnUnrelatedType_DoesNotReportDiagnostic(CancellationToken cancellationToken) + { + const string source = """ + using System; + + public static class Helper + { + public static void Validate(string value) + { + if (value is null) + throw new ArgumentNullException(nameof(value)); + } + } + """; + + var result = await AnalyzeAsync(source, cancellationToken); + + await Assert.That(result).HasNoDiagnostics(); + } + + /// + /// LINQ shares method names with the pipeline, and a lambda passed to it is ordinary code. + /// + [Test] + public async Task ThrowInALinqLambdaOutsideAGenerator_DoesNotReportDiagnostic(CancellationToken cancellationToken) + { + const string source = """ + using System; + using System.Collections.Generic; + using System.Linq; + + public static class Helper + { + public static IEnumerable Run(IEnumerable values) => + values.Select(static value => value ?? throw new InvalidOperationException("nope")); + } + """; + + var result = await AnalyzeAsync(source, cancellationToken); + + await Assert.That(result).HasNoDiagnostics(); + } +} diff --git a/src/tests/SourceGeneratorFramework.BuildIntegrationTests/AnalyzerClosureTests.cs b/src/tests/SourceGeneratorFramework.BuildIntegrationTests/AnalyzerClosureTests.cs index cbb9b20..d6e472d 100644 --- a/src/tests/SourceGeneratorFramework.BuildIntegrationTests/AnalyzerClosureTests.cs +++ b/src/tests/SourceGeneratorFramework.BuildIntegrationTests/AnalyzerClosureTests.cs @@ -164,7 +164,13 @@ public async Task ConsumerAnalyzerSet_ExcludesUnmergedComponentAnalyzer(Cancella ); var start = output.IndexOf('{', StringComparison.Ordinal); - var end = output.LastIndexOf('}'); + var end = +#if NET11_0_OR_GREATER + output.LastIndexOf('}', StringComparison.Ordinal); +#else + output.LastIndexOf('}'); +#endif + if (start < 0 || end <= start) { throw new InvalidOperationException( @@ -244,7 +250,12 @@ static async Task> ResolveAnalyzerClosureAsync(string projectPath, ); var start = output.IndexOf('{', StringComparison.Ordinal); - var end = output.LastIndexOf('}'); + var end = +#if NET11_0_OR_GREATER + output.LastIndexOf('}', StringComparison.Ordinal); +#else + output.LastIndexOf('}'); +#endif if (start < 0 || end <= start) { throw new InvalidOperationException( diff --git a/src/tests/SourceGeneratorFramework.ExampleGenerator.UnitTests/AttributeDataModelTypeLibraryCacheTests.cs b/src/tests/SourceGeneratorFramework.ExampleGenerator.UnitTests/AttributeDataModelTypeLibraryCacheTests.cs index 57abb0f..861c051 100644 --- a/src/tests/SourceGeneratorFramework.ExampleGenerator.UnitTests/AttributeDataModelTypeLibraryCacheTests.cs +++ b/src/tests/SourceGeneratorFramework.ExampleGenerator.UnitTests/AttributeDataModelTypeLibraryCacheTests.cs @@ -116,7 +116,6 @@ public async Task IdenticalRerun_AllFrameworkStagesCached(CancellationToken canc "GetAttributeDataTargets", "GetTypeLibrarySpecClassNames", "GetTypeLibraryTargets", - "GetFrameworkTypeLibraryTree", "GetGenerationConfiguration", "GetGenerationContext_EmptyCapabilities", ]; diff --git a/src/tests/SourceGeneratorFramework.Generators.UnitTests/GeneratorCompilationRetentionTests.cs b/src/tests/SourceGeneratorFramework.Generators.UnitTests/GeneratorCompilationRetentionTests.cs new file mode 100644 index 0000000..0752ace --- /dev/null +++ b/src/tests/SourceGeneratorFramework.Generators.UnitTests/GeneratorCompilationRetentionTests.cs @@ -0,0 +1,137 @@ +using System.Runtime.CompilerServices; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace Purview.SourceGeneratorFramework.Generators; + +/// +/// Proves that a generator's pipeline nodes do not root the compilations of earlier runs. +/// +/// +/// +/// Retention is the half of the PSGFR33 story that a cache test cannot see, and it is worth being +/// precise about which half it is. A node value holding an compares by reference, so +/// a fresh compilation makes the node report Modified and the new value replaces the old +/// one — which releases the previous compilation rather than rooting it. The cost of a symbol in a node is +/// therefore a guaranteed cache miss and the downstream re-execution that follows, not a leak. +/// +/// +/// A leak needs the opposite shape: a value that compares equal while still referencing a symbol, +/// so Roslyn keeps the stale cached instance and the compilation behind it. These tests pin that it does +/// not happen, for the generator whose pipeline was rewritten to build its model inside the transform and +/// for the one that still carries a symbol through a combine. +/// +/// +public class GeneratorCompilationRetentionTests +{ + const string TypeLibrarySource = """ + using Purview.SourceGeneratorFramework; + using Purview.SourceGeneratorFramework.Generators; + + namespace Test; + + [GenerateTypeLibrary] + static partial class TypeLibraryModel + { + [TypeRef("Test")] + static readonly TypeIdentity MyAttribute = default!; + } + """; + + [Test] + public async Task TypeLibraryGenerator_GivenRepeatedRuns_DoesNotRetainTheFirstCompilation() + { + // Arrange / Act + var (firstCompilation, driver) = RunAndDiscardFirstCompilation(TypeLibrarySource); + + for (var attempt = 0; attempt < 3; attempt++) + { + GC.Collect(2, GCCollectionMode.Forced, blocking: true, compacting: true); + GC.WaitForPendingFinalizers(); + } + + // Assert - the pipeline builds the whole target inside the ForAttributeWithMetadataName transform, + // so the cached node value is a GeneratorResult and never a symbol. Nothing roots the compilation. + await Assert.That(firstCompilation.IsAlive).IsFalse(); + + GC.KeepAlive(driver); + } + + const string AttributeDataSource = """ + using Purview.SourceGeneratorFramework.Generators; + + namespace Test; + + [Generate(typeof(System.Attribute))] + public readonly partial record struct AttributeData(bool Enabled); + """; + + [Test] + public async Task AttributeDataModelGenerator_GivenRepeatedRuns_DoesNotRetainTheFirstCompilation() + { + // Arrange / Act + var (firstCompilation, driver) = RunAndDiscardFirstCompilation( + AttributeDataSource + ); + + for (var attempt = 0; attempt < 3; attempt++) + { + GC.Collect(2, GCCollectionMode.Forced, blocking: true, compacting: true); + GC.WaitForPendingFinalizers(); + } + + // Assert + await Assert.That(firstCompilation.IsAlive).IsFalse(); + + GC.KeepAlive(driver); + } + + /// + /// Runs the generator over a sequence of fresh compilations built from identical source, discards every + /// strong reference to the first, and returns a weak reference to it alongside the still-live driver. + /// + /// + /// The driver has to come back with it: it is the thing that holds the node tables, and letting it fall + /// out of scope would make the compilation collectable no matter what the pipeline retains. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + static (WeakReference FirstCompilation, GeneratorDriver Driver) RunAndDiscardFirstCompilation( + string source, + int runs = 4 + ) + where TGenerator : IIncrementalGenerator, new() + { + GeneratorDriver driver = CSharpGeneratorDriver.Create( + [new TGenerator().AsSourceGenerator()], + driverOptions: new GeneratorDriverOptions( + IncrementalGeneratorOutputKind.None, + trackIncrementalGeneratorSteps: true + ) + ); + + WeakReference firstReference; + (firstReference, driver) = RunFirst(driver, source); + + for (var run = 1; run < runs; run++) + { + // A new compilation per run, as an IDE produces on every keystroke. The source is identical, so + // every stage reports Unchanged and Roslyn keeps the value cached from the *first* run. + driver = driver.RunGeneratorsAndUpdateCompilation(TestCompilation.Create(source), out _, out _); + } + + return (firstReference, driver); + } + + /// + /// Performs the first run in its own frame, so the only strong reference to that compilation dies with + /// the method rather than living on as a local in the caller. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + static (WeakReference FirstCompilation, GeneratorDriver Driver) RunFirst(GeneratorDriver driver, string source) + { + var first = TestCompilation.Create(source); + + // The updated compilation is derived from the input and would root it, so it is discarded too. + return (new WeakReference(first), driver.RunGeneratorsAndUpdateCompilation(first, out _, out _)); + } +} diff --git a/src/tests/SourceGeneratorFramework.Generators.UnitTests/TypeLibraryGeneratorCacheTests.cs b/src/tests/SourceGeneratorFramework.Generators.UnitTests/TypeLibraryGeneratorCacheTests.cs index 8fb169c..3c230a8 100644 --- a/src/tests/SourceGeneratorFramework.Generators.UnitTests/TypeLibraryGeneratorCacheTests.cs +++ b/src/tests/SourceGeneratorFramework.Generators.UnitTests/TypeLibraryGeneratorCacheTests.cs @@ -42,7 +42,6 @@ public async Task IdenticalRerun_AllStagesCached(CancellationToken cancellationT // ForAttributeWithMetadataName steps can report Modified on rerun because the post-initialization // attribute source is regenerated as a new tree.) await Assert.That(result.Runs[1]).StepIsCached("GetTypeLibraryTargets"); - await Assert.That(result.Runs[1]).StepIsCached("GetFrameworkTypeLibraryTree"); await Assert.That(result.Runs[1]).StepIsCached("GetGenerationConfiguration"); await Assert.That(result.Runs[1]).StepIsCached("GetGenerationContext_EmptyCapabilities"); } @@ -70,7 +69,6 @@ CancellationToken cancellationToken await Assert.That(result.Runs[1]).StepIsModified("GetGenerationConfiguration"); await Assert.That(result.Runs[1]).StepIsCached("GetTypeLibraryTargets"); - await Assert.That(result.Runs[1]).StepIsCached("GetFrameworkTypeLibraryTree"); } [Test] diff --git a/src/tests/SourceGeneratorFramework.UnitTests/CodeQueryTests.cs b/src/tests/SourceGeneratorFramework.UnitTests/CodeQueryTests.cs index 187f6b2..b449e9d 100644 --- a/src/tests/SourceGeneratorFramework.UnitTests/CodeQueryTests.cs +++ b/src/tests/SourceGeneratorFramework.UnitTests/CodeQueryTests.cs @@ -323,13 +323,17 @@ public async Task GetAllAndCount_ReturnAllMatches() await Assert.That(query.Count()).IsEqualTo(2); await Assert.That(query.GetAll().Length).IsEqualTo(3); - await Assert - .That( - query - .GetAll(static method => method.Identifier.ValueText.StartsWith('C')) - .Length - ) - .IsEqualTo(1); + await Assert.That(query.GetAll(ValueStartsWith()).Length).IsEqualTo(1); + + static Func ValueStartsWith() + { + return static method => method.Identifier.ValueText +#if NET11_0_OR_GREATER + .StartsWith('C', StringComparison.Ordinal); +#else + .StartsWith('C'); +#endif + } } [Test] diff --git a/src/tests/SourceGeneratorFramework.UnitTests/CompilationResultTests.cs b/src/tests/SourceGeneratorFramework.UnitTests/CompilationResultTests.cs index 2d1b214..c7eeac0 100644 --- a/src/tests/SourceGeneratorFramework.UnitTests/CompilationResultTests.cs +++ b/src/tests/SourceGeneratorFramework.UnitTests/CompilationResultTests.cs @@ -64,10 +64,9 @@ public async Task CompileToAssembly_Dispose_DoesNotThrow(CancellationToken cance SourceGeneratorTestRunner runner = new(); SourceGeneratorTestOptions options = new() { CompileToAssembly = true }; - using (var result = await runner.RunAsync("public sealed class Input { }", options, cancellationToken)) - { - await Assert.That(result.CompilationResult.Assembly).IsNotNull(); - } + using var result = await runner.RunAsync("public sealed class Input { }", options, cancellationToken); + + await Assert.That(result.CompilationResult.Assembly).IsNotNull(); } [Test] diff --git a/src/tests/SourceGeneratorShared.UnitTests/ComponentGuardTests.cs b/src/tests/SourceGeneratorShared.UnitTests/ComponentGuardTests.cs new file mode 100644 index 0000000..07ec367 --- /dev/null +++ b/src/tests/SourceGeneratorShared.UnitTests/ComponentGuardTests.cs @@ -0,0 +1,312 @@ +using System.Globalization; +using System.Reflection; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Purview.SourceGeneratorFramework; + +/// +/// Covers and the defaults derives from the +/// component itself. +/// +public class ComponentGuardTests +{ + [Test] + public async Task TryRun_GivenABodyThatSucceeds_ReturnsTrueAndNoFailure() + { + var ran = false; + + var succeeded = ComponentGuard.TryRun(() => ran = true, out var failure); + + await Assert.That(succeeded).IsTrue(); + await Assert.That(failure).IsNull(); + await Assert.That(ran).IsTrue(); + } + + [Test] + public async Task TryRun_GivenABodyThatThrows_ReturnsFalseAndDescribesTheFailure() + { + var succeeded = ComponentGuard.TryRun( + () => throw new InvalidOperationException("boom"), + out var failure + ); + + await Assert.That(succeeded).IsFalse(); + await Assert.That(failure).IsNotNull(); + + var message = failure!.GetMessage(CultureInfo.InvariantCulture); + await Assert.That(message).Contains(nameof(SampleGenerator)); + await Assert.That(message).Contains(nameof(InvalidOperationException)); + await Assert.That(message).Contains("boom"); + } + + /// + /// Roslyn cancels analysis on every keystroke. Swallowing that would turn a normal cancellation into a + /// reported defect, and would hide from Roslyn that the work was abandoned. + /// + [Test] + public async Task TryRun_GivenCancellation_LetsTheCancellationPropagate() + { + using CancellationTokenSource source = new(); + await source.CancelAsync(); + + await Assert + .That(() => + ComponentGuard.TryRun( + () => source.Token.ThrowIfCancellationRequested(), + out _, + cancellationToken: source.Token + ) + ) + .Throws(); + } + + /// + /// An raised when nothing was cancelled is an ordinary defect, + /// not a cancellation, and must still be reported. + /// + [Test] + public async Task TryRun_GivenAnOperationCanceledExceptionWithoutCancellation_ReportsIt() + { + var succeeded = ComponentGuard.TryRun( + () => throw new OperationCanceledException("not really cancelled"), + out var failure + ); + + await Assert.That(succeeded).IsFalse(); + await Assert.That(failure).IsNotNull(); + } + + [Test] + public async Task Run_GivenAReportDelegateAndAFailure_ReportsOnce() + { + List reported = []; + + ComponentGuard.Run(reported.Add, () => throw new InvalidOperationException("boom")); + + await Assert.That(reported).HasSingleItem(); + await Assert.That(reported[0].Id).IsEqualTo(ComponentFailure.For().Id); + } + + [Test] + public async Task Run_GivenAReportDelegateAndSuccess_ReportsNothing() + { + List reported = []; + + ComponentGuard.Run(reported.Add, static () => { }); + + await Assert.That(reported).IsEmpty(); + } + + // --------------------------------------------------------------------------------------------- + // Defaults derived from the consuming codebase + // --------------------------------------------------------------------------------------------- + + [Test] + public async Task For_GivenAGenerator_DerivesTheIdFromItsNamespace() + { + var descriptor = ComponentFailure.For(); + + // Namespace is Purview.SourceGeneratorFramework -> P, S -> "PS", capped at four initials. + await Assert.That(descriptor.Id).IsEqualTo(ComponentFailure.DeriveIdPrefix(typeof(SampleGenerator)) + "0001"); + await Assert.That(descriptor.Id).EndsWith("0001"); + } + + [Test] + public async Task For_GivenAGenerator_NamesTheComponentKind() + { + await Assert + .That(ComponentFailure.For().Title.ToString(CultureInfo.InvariantCulture)) + .IsEqualTo("Source generator failed"); + await Assert + .That(ComponentFailure.For().Title.ToString(CultureInfo.InvariantCulture)) + .IsEqualTo("Analyzer failed"); + await Assert + .That(ComponentFailure.For().Title.ToString(CultureInfo.InvariantCulture)) + .IsEqualTo("Diagnostic suppressor failed"); + await Assert + .That(ComponentFailure.For().Title.ToString(CultureInfo.InvariantCulture)) + .IsEqualTo("Code fix failed"); + } + + [Test] + public async Task For_UsesTheRootNamespaceAsCategory() + { + await Assert.That(ComponentFailure.For().Category).IsEqualTo("Purview"); + } + + [Test] + public async Task For_DefaultsToWarning() + { + await Assert + .That(ComponentFailure.For().DefaultSeverity) + .IsEqualTo(DiagnosticSeverity.Warning); + } + + /// + /// An analyzer declares this descriptor in SupportedDiagnostics, and Roslyn throws if what is + /// reported is not the same rule. The instance must therefore be stable. + /// + [Test] + public async Task For_GivenTheSameComponentAndOptions_ReturnsTheSameInstance() + { + await Assert + .That(ReferenceEquals(ComponentFailure.For(), ComponentFailure.For())) + .IsTrue(); + } + + [Test] + [Arguments("Acme.Widgets.Generators", "AWG")] + [Arguments("Acme.Widgets.Generators.Internal.Deep", "AWGI")] + [Arguments("Contoso", "CON")] + [Arguments("A", "GEN")] + public async Task DeriveIdPrefix_ProducesAStablePrefix(string @namespace, string expected) + { + await Assert.That(ComponentFailure.DeriveIdPrefix(FakeType.InNamespace(@namespace))).IsEqualTo(expected); + } + + [Test] + public async Task DeriveIdPrefix_GivenNoNamespace_FallsBackToGen() + { + await Assert.That(ComponentFailure.DeriveIdPrefix(FakeType.InNamespace(null))).IsEqualTo("GEN"); + } + + // --------------------------------------------------------------------------------------------- + // Overrides + // --------------------------------------------------------------------------------------------- + + [Test] + public async Task Options_OverrideEveryPartOfTheDescriptor() + { + ComponentFailureOptions options = new() + { + Id = "ACME9999", + Title = "Widget generator fell over", + MessageFormat = "{0} / {1} / {2}", + Category = "Acme.Widgets", + Severity = DiagnosticSeverity.Error, + Description = "A description.", + HelpLinkUri = "https://example.invalid/acme9999", + }; + + var descriptor = ComponentFailure.For(options); + + await Assert.That(descriptor.Id).IsEqualTo("ACME9999"); + await Assert + .That(descriptor.Title.ToString(CultureInfo.InvariantCulture)) + .IsEqualTo("Widget generator fell over"); + await Assert.That(descriptor.Category).IsEqualTo("Acme.Widgets"); + await Assert.That(descriptor.DefaultSeverity).IsEqualTo(DiagnosticSeverity.Error); + await Assert.That(descriptor.Description.ToString(CultureInfo.InvariantCulture)).IsEqualTo("A description."); + await Assert.That(descriptor.HelpLinkUri).IsEqualTo("https://example.invalid/acme9999"); + } + + [Test] + public async Task Options_GivenIncludeStackTrace_PutsTheStackTraceInTheMessage() + { + ComponentGuard.TryRun( + static () => throw new InvalidOperationException("boom"), + out var withoutTrace + ); + + ComponentGuard.TryRun( + static () => throw new InvalidOperationException("boom"), + out var withTrace, + new ComponentFailureOptions { Id = "ACME9998", IncludeStackTrace = true } + ); + + await Assert + .That(withoutTrace!.GetMessage(CultureInfo.InvariantCulture)) + .DoesNotContain(nameof(ComponentGuardTests)); + await Assert.That(withTrace!.GetMessage(CultureInfo.InvariantCulture)).Contains(nameof(ComponentGuardTests)); + } + + /// + /// A diagnostic message spanning lines breaks the single-line shape every consumer of the build log + /// expects, so the newlines a stack trace carries are flattened. + /// + [Test] + public async Task Describe_FlattensNewlinesInTheMessage() + { + var diagnostic = ComponentGuard.Describe( + typeof(SampleGenerator), + new InvalidOperationException("first\r\nsecond") + ); + + await Assert.That(diagnostic.GetMessage(CultureInfo.InvariantCulture)).DoesNotContain("\n"); + await Assert.That(diagnostic.GetMessage(CultureInfo.InvariantCulture)).Contains("first"); + await Assert.That(diagnostic.GetMessage(CultureInfo.InvariantCulture)).Contains("second"); + } + + [Test] + public async Task ResolveComponentType_GivenALambda_WalksOutToTheDeclaringComponent() + { + Action callback = static () => { }; + + await Assert + .That(ComponentGuard.ResolveComponentType(callback, typeof(string))) + .IsEqualTo(typeof(ComponentGuardTests)); + } + + // --------------------------------------------------------------------------------------------- + // Fixtures + // --------------------------------------------------------------------------------------------- + + sealed class SampleGenerator : IIncrementalGenerator + { + public void Initialize(IncrementalGeneratorInitializationContext context) { } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "MicrosoftCodeAnalysisCorrectness", + "RS1001:Missing diagnostic analyzer attribute" + )] + sealed class SampleAnalyzer : DiagnosticAnalyzer + { + public override System.Collections.Immutable.ImmutableArray SupportedDiagnostics => []; + + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "MicrosoftCodeAnalysisCorrectness", + "RS1025:Configure generated code analysis" + )] + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "MicrosoftCodeAnalysisCorrectness", + "RS1026:Enable concurrent execution" + )] + public override void Initialize(AnalysisContext context) { } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "MicrosoftCodeAnalysisCorrectness", + "RS1001:Missing diagnostic analyzer attribute" + )] + sealed class SampleSuppressor : DiagnosticSuppressor + { + public override System.Collections.Immutable.ImmutableArray SupportedSuppressions => []; + + public override void ReportSuppressions(SuppressionAnalysisContext context) { } + } + + sealed class SampleCodeFix : CodeFixProvider + { + public override System.Collections.Immutable.ImmutableArray FixableDiagnosticIds => []; + + public override Task RegisterCodeFixesAsync(CodeFixContext context) => Task.CompletedTask; + } + + /// + /// A stand-in for prefix derivation, so a test does not need a real type per namespace. + /// + sealed class FakeType : TypeDelegator + { + readonly string? _namespace; + + FakeType(string? @namespace) + : base(typeof(object)) => _namespace = @namespace; + + public override string? Namespace => _namespace; + + public static FakeType InNamespace(string? @namespace) => new(@namespace); + } +} diff --git a/src/tests/SourceGeneratorShared.UnitTests/GeneratorLoggingTests.cs b/src/tests/SourceGeneratorShared.UnitTests/GeneratorLoggingTests.cs index 388c993..4858941 100644 --- a/src/tests/SourceGeneratorShared.UnitTests/GeneratorLoggingTests.cs +++ b/src/tests/SourceGeneratorShared.UnitTests/GeneratorLoggingTests.cs @@ -59,8 +59,14 @@ public async Task RunIncrementalAsync_ScopesEntriesToTheRunThatWroteThem(Cancell // registered for the whole sequence and drained between runs. SourceGeneratorTestRunner runner = new(); - // Act - var result = await runner.RunIncrementalAsync([Source], cancellationToken: cancellationToken); + // Act - this test is about draining, not about incrementality, and it needs a fully cached second + // run to have anything to prove. Reusing the compilation for the identical rerun is what makes the + // output step cacheable here; the default of a fresh compilation per run would re-execute it. + var result = await runner.RunIncrementalAsync( + [Source], + new SourceGeneratorTestOptions { ReuseCompilationForIdenticalSources = true }, + cancellationToken + ); // Assert - the first run executes the output step and writes its entries in order. await AssertWrittenInOrderAsync(result.Runs[0].LogEntries); diff --git a/src/tests/SourceGeneratorShared.UnitTests/GuardedSourceOutputTests.cs b/src/tests/SourceGeneratorShared.UnitTests/GuardedSourceOutputTests.cs new file mode 100644 index 0000000..6cb03d2 --- /dev/null +++ b/src/tests/SourceGeneratorShared.UnitTests/GuardedSourceOutputTests.cs @@ -0,0 +1,83 @@ +using System.Globalization; +using Purview.SourceGeneratorFramework.TestGenerators; + +namespace Purview.SourceGeneratorFramework; + +/// +/// Proves the framework's RegisterSourceOutput turns an exception from the emit callback into a +/// diagnostic, rather than letting it escape as CS8785. +/// +/// +/// The distinction matters to a consumer: CS8785 means the generator emitted nothing at all for that +/// compilation, so every type it was supposed to produce is also reported missing. A diagnostic leaves the +/// rest of the compilation intact and names the generator that is actually at fault. +/// +public class GuardedSourceOutputTests +{ + const string Source = """ + [TestAttribute] + public partial class MyClass { } + """; + + const string TestAttributeSource = """ + [System.AttributeUsage(System.AttributeTargets.Class)] + public sealed class TestAttribute : System.Attribute { } + """; + + static SourceGeneratorTestOptions CreateOptions() => + new SourceGeneratorTestOptions { ThrowOnGenerationException = false } + .WithAdditionalSources(TestAttributeSource) + .WithExcludeGeneratedSourceHintNames("TestAttribute"); + + [Test] + public async Task RegisterSourceOutput_GivenAThrowingCallback_ReportsADiagnosticInsteadOfThrowing( + CancellationToken cancellationToken + ) + { + SourceGeneratorTestRunner runner = new(); + + using var result = await runner.RunAsync(Source, CreateOptions(), cancellationToken); + + var failure = result.DriverResult.Diagnostics.SingleOrDefault(diagnostic => + diagnostic.Id == ComponentFailure.For().Id + ); + + await Assert.That(failure).IsNotNull(); + + var message = failure!.GetMessage(CultureInfo.InvariantCulture); + await Assert.That(message).Contains(nameof(ThrowingTestGenerator)); + await Assert.That(message).Contains(nameof(InvalidOperationException)); + await Assert.That(message).Contains(ThrowingTestGenerator.FailureMessage); + } + + /// + /// The exception must not reach the driver, which is what would otherwise become CS8785. + /// + [Test] + public async Task RegisterSourceOutput_GivenAThrowingCallback_ProducesNoGeneratorException( + CancellationToken cancellationToken + ) + { + SourceGeneratorTestRunner runner = new(); + + using var result = await runner.RunAsync(Source, CreateOptions(), cancellationToken); + + await Assert.That(result.DriverResult.Results.Any(run => run.Exception is not null)).IsFalse(); + await Assert.That(result.DriverResult.Diagnostics.Any(diagnostic => diagnostic.Id == "CS8785")).IsFalse(); + } + + /// + /// The id is derived from the generator's own namespace, so a consumer suppressing it is suppressing + /// something that reads as theirs rather than as the framework's. + /// + [Test] + public async Task FailureDiagnostic_UsesAnIdDerivedFromTheGeneratorsNamespace() + { + var descriptor = ComponentFailure.For(); + + await Assert + .That(descriptor.Id) + .IsEqualTo(ComponentFailure.DeriveIdPrefix(typeof(ThrowingTestGenerator)) + "0001"); + await Assert.That(descriptor.Title.ToString(CultureInfo.InvariantCulture)).IsEqualTo("Source generator failed"); + } +} diff --git a/src/tests/SourceGeneratorShared.UnitTests/IncrementalPipelineCacheTests.cs b/src/tests/SourceGeneratorShared.UnitTests/IncrementalPipelineCacheTests.cs index 84cb3b6..54f8a8c 100644 --- a/src/tests/SourceGeneratorShared.UnitTests/IncrementalPipelineCacheTests.cs +++ b/src/tests/SourceGeneratorShared.UnitTests/IncrementalPipelineCacheTests.cs @@ -46,7 +46,11 @@ public async Task IdenticalRerun_AllStagesCached(CancellationToken cancellationT var result = await runner.RunIncrementalAsync([AttributedSource], CreateOptions(), cancellationToken); - await Assert.That(result.Runs[1]).AllStepsCachedOrUnchanged(); + // Each run builds a fresh Compilation, as an IDE does on every keystroke, so Roslyn's own + // compilation and syntax-tree steps necessarily report Modified. Assert the generator's own + // tracked stages instead: those are the ones value equality is supposed to keep cached. + await Assert.That(result.Runs[1]).StepIsCached("ForAttribute_TestAttribute"); + await Assert.That(result.Runs[1]).StepIsCached("GetMSBuildPropertyValue_DisableTestGenerator"); await Assert.That(result.Runs[0]).HasStepReason("ForAttribute_TestAttribute", StepReason.New); } diff --git a/src/tests/SourceGeneratorShared.UnitTests/TestGenerators/ThrowingTestGenerator.cs b/src/tests/SourceGeneratorShared.UnitTests/TestGenerators/ThrowingTestGenerator.cs new file mode 100644 index 0000000..a392336 --- /dev/null +++ b/src/tests/SourceGeneratorShared.UnitTests/TestGenerators/ThrowingTestGenerator.cs @@ -0,0 +1,33 @@ +using Microsoft.CodeAnalysis; +using Purview.SourceGeneratorFramework.Helpers; + +namespace Purview.SourceGeneratorFramework.TestGenerators; + +/// +/// A generator whose emit callback always throws, for covering the guard the framework's +/// RegisterSourceOutput wraps that callback in. +/// +public sealed class ThrowingTestGenerator : IIncrementalGenerator +{ + public const string FailureMessage = "the emit step fell over"; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var targets = IncrementalPipeline.ForAttributeWithMetadataName( + context, + new TypeIdentity("TestAttribute", null), + static (ctx, _) => GeneratorResult.Create(new TargetInfo(ctx.TargetSymbol.Name)) + ); + + var generationContext = IncrementalPipeline.DefaultGenerationContextValueProvider( + context, + new GenerationSettings("ThrowingTestGenerator", "1.0.0") + ); + + context.RegisterSourceOutput( + targets, + generationContext, + static (spc, target, ctx) => throw new InvalidOperationException(FailureMessage) + ); + } +} diff --git a/src/tests/SourceGeneratorShared.UnitTests/TypeIdentityTests.cs b/src/tests/SourceGeneratorShared.UnitTests/TypeIdentityTests.cs index 51d9eb5..6a989c5 100644 --- a/src/tests/SourceGeneratorShared.UnitTests/TypeIdentityTests.cs +++ b/src/tests/SourceGeneratorShared.UnitTests/TypeIdentityTests.cs @@ -626,18 +626,16 @@ public async Task Create_GivenGenericArgumentsDifferingOnlyByNullability_DoesNot [Test] public async Task Create_GivenConstructedGenericOverSourceType_DoesNotRetainTheCompilation() { - // Arrange - an anchor compilation stands in for the rest of the run: because the harness shares its - // MetadataReference instances, every test compilation binds the *same* corlib assembly symbol, and - // that symbol stays alive as long as any compilation holds it. + // Arrange - an anchor compilation stands in for the rest of the run, so a corlib assembly symbol is + // held for longer than the compilation under test. var anchor = TestCompilation.Create(); var anchorCorlib = anchor.GetSpecialType(SpecialType.System_Int32).ContainingAssembly; // List reports its definition's assembly - corlib - as the containing assembly. Keying the // cached identity off that assembly, rather than off the symbol, pinned the source compilation for as - // long as the anchor lived. + // long as any corlib symbol lived. var (reference, name, corlib) = BuildIdentityAndDiscardCompilation(); await Assert.That(name).IsEqualTo("List"); - await Assert.That(ReferenceEquals(corlib, anchorCorlib)).IsTrue(); // Act for (var attempt = 0; attempt < 3; attempt++) @@ -649,6 +647,16 @@ public async Task Create_GivenConstructedGenericOverSourceType_DoesNotRetainTheC // Assert await Assert.That(reference.IsAlive).IsFalse(); + // Both corlib symbols are held across the collection, which is what makes the assertion mean + // something: a cache keyed off the containing assembly would still be reachable, and would still be + // holding the discarded compilation. + // + // Whether these are the *same* instance is deliberately not asserted. Roslyn caches an assembly + // symbol per AssemblyMetadata behind a weak reference, so two independently created compilations + // share one only if the cache entry survived in between - and this test, along with every other test + // forcing a collection, is exactly what drops it. Asserting it made this test fail intermittently + // under parallel execution while passing in isolation. + GC.KeepAlive(corlib); GC.KeepAlive(anchorCorlib); GC.KeepAlive(anchor); }