Skip to content

Commit b947a24

Browse files
ANcpLuaclaude
andcommitted
feat: structural refactoring and validation support (v3.6.0)
10-task batch refactoring improving strictness, readability, and extensibility: - HttpVerb enum replacing string-based HTTP method comparisons - Generic CombineAll replacing hand-written CombineSix/CombineNine (163→47 lines) - EmitContext record struct flattening nested pipeline tuples - MapCallEmitter extracting shared map call helpers - Per-concern middleware emission methods - Pure CollectSerializableTypes extraction - Exhaustive switch expressions with ArgumentOutOfRangeException - `in` parameter optimization for readonly record structs - ValidationResolverEmitter for DataAnnotations integration - Validation showcase sample project 457 tests passing, 0 warnings, 0 errors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6e4b62e commit b947a24

45 files changed

Lines changed: 1667 additions & 593 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.editorconfig

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,22 @@ dotnet_diagnostic.AL0025.severity = warning # Prefer static lambda
1515
dotnet_diagnostic.AL0026.severity = warning # DateTime.Now/UtcNow
1616
dotnet_diagnostic.AL0027.severity = warning # Newtonsoft.Json
1717

18+
# CA1062: Validate arguments of public methods — Roslyn callbacks guarantee non-null
19+
dotnet_diagnostic.CA1062.severity = none
20+
# CA1308: Normalize strings to uppercase — lowercase is standard for URL routes
21+
dotnet_diagnostic.CA1308.severity = none
22+
23+
# Meziantou — suppress opinionated style rules
24+
dotnet_diagnostic.MA0002.severity = none # StringBuilder generator code uses IndentedTextWriter
25+
dotnet_diagnostic.MA0006.severity = none # string.Equals string == is ordinal, explicit enough
26+
dotnet_diagnostic.MA0008.severity = none # StructLayoutAttribute unnecessary for managed types
27+
dotnet_diagnostic.MA0011.severity = none # IFormatProvider on TryParse netstandard2.0 is fine
28+
dotnet_diagnostic.MA0048.severity = none # File name must match type partial classes + DTOs are intentional
29+
dotnet_diagnostic.MA0049.severity = none # Type name namespace ErrorOr in ErrorOrX is by design
30+
dotnet_diagnostic.MA0004.severity = none # ConfigureAwait ASP.NET Core has no SynchronizationContext
31+
dotnet_diagnostic.MA0016.severity = none # Collection abstraction contradicts CA1859 (use concrete types)
32+
dotnet_diagnostic.MA0051.severity = none # Method too long source generators have legitimate long methods
33+
1834
# ============================================================================
1935
# IDE Suggestion-Level Rules Rationale
2036
# ============================================================================
@@ -51,21 +67,17 @@ csharp_style_prefer_extended_property_pattern = true:suggestion
5167
# Test code intentionally uses patterns that trigger warnings in production code.
5268
# These are suppressed for test files only.
5369

54-
[**/tests/**/*.cs]
55-
# Nullable suppression (null!) is intentional for testing null argument validation
70+
[tests/**/*.cs]
5671
resharper_nullable_warning_suppression_is_used_highlighting = none
57-
# Record properties in test data structures may not be directly accessed
5872
resharper_not_accessed_positional_property_local_highlighting = none
59-
# Multiple enumeration is sometimes acceptable in tests for clarity
6073
resharper_possible_multiple_enumeration_highlighting = none
61-
# Pure method return values may be intentionally discarded in tests
6274
resharper_return_value_of_pure_method_is_not_used_highlighting = none
63-
# Redundant nullable suppressions after assertions
6475
resharper_redundant_nullable_warning_suppression_highlighting = none
6576

6677
[**/src/**/*.cs]
67-
# CA1859 requires concrete types for performance; Rider's suggestion to widen conflicts
78+
# CA1859 requires concrete types for private methods; ReSharper suggests the opposite
6879
resharper_parameter_type_can_be_enumerable_local_highlighting = none
80+
resharper_suggest_base_type_for_parameter_highlighting = none
6981

7082
# ============================================================================
7183
# Generator-Specific Suppressions
@@ -76,6 +88,9 @@ resharper_parameter_type_can_be_enumerable_local_highlighting = none
7688
# The namespace intentionally does not match the folder structure.
7789
[**/ErrorOrX.Generators/Polyfills.cs]
7890
dotnet_diagnostic.IDE0130.severity = none
91+
dotnet_diagnostic.CA1019.severity = none # Polyfill attribute constructor intentional design
92+
dotnet_diagnostic.CA1305.severity = none # uint.ToString() no locale dependence
93+
dotnet_diagnostic.CA1812.severity = none # IsExternalInit compiler-reserved type
7994

8095
# Extractor.cs: AL0029 flags foreach-over-GetAttributes() loops, suggesting
8196
# HasAttribute() instead. These loops legitimately need to extract constructor

CHANGELOG.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,60 @@ All notable changes to this project are documented in this file.
44

55
## [Unreleased]
66

7+
## [3.6.0] - 2026-02-13
8+
9+
### Added
10+
11+
- **HttpVerb enum**: Replaced string-based HTTP method comparisons throughout the core generator pipeline with a
12+
strongly-typed `HttpVerb` enum, providing compile-time exhaustiveness checks and eliminating string comparison bugs.
13+
14+
- **EmitContext record struct**: Flattened deeply nested Roslyn pipeline tuples into a named `EmitContext` record struct,
15+
improving readability of `RegisterSourceOutput` callbacks.
16+
17+
- **MapCallEmitter**: Extracted shared map call emission helpers (`EmitMapCallStart`/`EmitMapCallEnd`) used by both
18+
grouped and ungrouped endpoint emission, eliminating code duplication.
19+
20+
- **ValidationResolverEmitter**: New emitter for validation resolver support with DataAnnotations integration.
21+
22+
- **Validation showcase sample**: Added `ErrorOrX.Validation.Showcase` sample project demonstrating validation patterns.
23+
24+
### Changed
25+
26+
- **Generic CombineAll**: Replaced hand-written `CombineSix`/`CombineNine` provider combiners with a generic
27+
`CombineAll<T>(params IncrementalValuesProvider<T>[])` using pairwise loop (163 lines → 47 lines).
28+
29+
- **Per-concern middleware emission**: Split monolithic `EmitMiddlewareCalls` into 4 focused per-concern methods
30+
(`EmitAuthorizationMiddleware`, `EmitRateLimitingMiddleware`, `EmitOutputCacheMiddleware`, `EmitCorsMiddleware`).
31+
32+
- **Pure `CollectSerializableTypes`**: Extracted serializable type collection as a pure method from the analyzer.
33+
34+
- **Exhaustive switch expressions**: Added `ArgumentOutOfRangeException` discard arms to all `ParameterSource` and
35+
`HttpVerb` switch expressions for compile-time safety.
36+
37+
- **`in` parameter optimization**: Added `in` modifier to 5 `MiddlewareInfo` and `VersioningInfo` readonly record struct
38+
parameters to avoid unnecessary copies.
39+
40+
- **Updated dependencies**: ANcpLua.Roslyn.Utilities 1.31.0 → 1.33.0.
41+
42+
## [3.5.0] - 2026-02-08
43+
44+
### Changed
45+
46+
- **Emitter cohesion refactoring**: Improved expressiveness and removed incoherent patterns in
47+
`ErrorOrEndpointGenerator.Emitter.cs`:
48+
- Removed passthrough wrappers (`EmitParameterBinding`, `BuildArgumentExpression`) that added indirection without
49+
logic — callers now use `BindingCodeEmitter` directly
50+
- Unified `WrapReturn` — eliminated duplicate local function in `EmitUnionTypeErrorHandling` by threading
51+
`InvokerContext` through `EmitValidationHandling` and `EmitErrorTypeSwitch`, also removing `Func<string, string>`
52+
delegate allocations
53+
- Collapsed `GetSuccessFactoryWithLocation` from 4 sequential guard clauses into a single positive condition
54+
- Simplified `HasValidatableTypes` from manual nested loop to `Any()`/`Any()` LINQ expression
55+
- Simplified `SortEndpoints` from manual array copy + `Array.Sort` to idiomatic `OrderBy`/`ThenBy` chain
56+
57+
- **Nullable suppression fix**: Replaced `constant.Value.ToString()!` in `ErrorOrContext.TypedConstantToLiteral` with
58+
`Convert.ToString(constant.Value, CultureInfo.InvariantCulture) ?? "null"` — removes hidden nullable assumption and
59+
uses `InvariantCulture` consistently with adjacent numeric arms.
60+
761
## [3.4.0] - 2026-02-07
862

963
### Changed

CLAUDE.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -226,8 +226,10 @@ tests/
226226

227227
**Search for existing implementations first.** Common duplication areas:
228228

229-
| Concept | Symbol-based API | String-based API |
230-
|------------------|----------------------------------------------|----------------------------------------------|
231-
| Unwrap nullable | `ErrorOrContext.UnwrapNullable(ITypeSymbol)` | `TypeNameHelper.UnwrapNullable(string)` |
232-
| Type comparison | Roslyn `ITypeSymbol.Equals` | `TypeNameHelper.TypeNamesMatch()` |
233-
| Route parameters | - | `RouteValidator.BuildRouteParameterLookup()` |
229+
| Concept | Owner | Do NOT duplicate in |
230+
|--------------------|----------------------------------------------|----------------------------------------------|
231+
| Unwrap nullable | `ErrorOrContext.UnwrapNullable(ITypeSymbol)` | `TypeNameHelper.UnwrapNullable(string)` |
232+
| Type comparison | Roslyn `ITypeSymbol.Equals` | `TypeNameHelper.TypeNamesMatch()` |
233+
| Route parameters | - | `RouteValidator.BuildRouteParameterLookup()` |
234+
| Param binding emit | `BindingCodeEmitter` | Emitter.cs (call directly, no wrappers) |
235+
| Wrap return exprs | `InvokerContext.WrapReturn()` | Local functions in emit methods |

Directory.Build.props

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,14 @@
55

66
<Import Project="$(MSBuildThisFileDirectory)version.props"/>
77

8-
<PropertyGroup>
9-
<ANcpLuaRoslynUtilitiesVersion>1.31.0</ANcpLuaRoslynUtilitiesVersion>
10-
</PropertyGroup>
11-
128
<PropertyGroup>
139
<LangVersion>preview</LangVersion>
1410
<Nullable>enable</Nullable>
1511
<ImplicitUsings>enable</ImplicitUsings>
1612
<Deterministic>true</Deterministic>
1713
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
14+
<AnalysisLevel>latest-all</AnalysisLevel>
15+
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
1816
<Authors>ANcpLua</Authors>
1917
<Copyright>Copyright (c) 2025 Alexander Nachtmann</Copyright>
2018
<PackageLicenseExpression>MIT</PackageLicenseExpression>

Directory.Build.targets

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@
1010
<PackageReference Include="Roslynator.Analyzers" PrivateAssets="all"/>
1111
<PackageReference Include="Roslynator.Formatting.Analyzers" PrivateAssets="all"/>
1212
<PackageReference Include="Roslynator.CodeAnalysis.Analyzers" PrivateAssets="all"/>
13+
<PackageReference Include="Meziantou.Analyzer" PrivateAssets="all"/>
1314
<PackageReference Include="JonSkeet.RoslynAnalyzers" PrivateAssets="all" Condition="'@(PackageReference->WithMetadataValue('Identity', 'JonSkeet.RoslynAnalyzers'))' == ''"/>
1415
</ItemGroup>
1516

1617
<ItemGroup>
1718
<!-- Ensure PackageVersion is defined for CPM -->
1819
<PackageVersion Update="ANcpLua.Analyzers" Version="1.13.0"/>
1920
<PackageVersion Include="ANcpLua.Analyzers" Version="1.13.0" Condition="'@(PackageVersion->WithMetadataValue('Identity', 'ANcpLua.Analyzers'))' == ''"/>
21+
<PackageVersion Include="Meziantou.Analyzer" Version="$(MeziantouAnalyzerVersion)"/>
2022
<!-- JonSkeet: SDK adds it for some projects, so use Update + conditional Include -->
2123
<PackageVersion Update="JonSkeet.RoslynAnalyzers" Version="1.0.0-beta.6"/>
2224
<PackageVersion Include="JonSkeet.RoslynAnalyzers" Version="1.0.0-beta.6" Condition="'@(PackageVersion->WithMetadataValue('Identity', 'JonSkeet.RoslynAnalyzers'))' == ''"/>

Directory.Packages.props

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0"/>
88
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0"/>
99
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="5.0.0"/>
10-
<PackageVersion Include="ANcpLua.Roslyn.Utilities" Version="1.31.0"/>
11-
<PackageVersion Include="ANcpLua.Roslyn.Utilities.Testing" Version="1.31.0"/>
10+
<PackageVersion Include="ANcpLua.Roslyn.Utilities" Version="$(ANcpLuaRoslynUtilitiesVersion)"/>
11+
<PackageVersion Include="ANcpLua.Roslyn.Utilities.Testing" Version="$(ANcpLuaRoslynUtilitiesVersion)"/>
1212
<PackageVersion Update="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4"/>
1313
<PackageVersion Update="Microsoft.Sbom.Targets" Version="4.1.5"/>
1414
<PackageVersion Include="ErrorProne.NET.CoreAnalyzers" Version="0.8.2-beta.1"/>
@@ -27,8 +27,8 @@
2727
<PackageVersion Include="xunit.v3.mtp-v2" Version="3.2.2"/>
2828
<PackageVersion Include="AwesomeAssertions" Version="9.3.0"/>
2929
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.3"/>
30-
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="10.0.2"/>
31-
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.2"/>
30+
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="$(MicrosoftAspNetCoreTestHostVersion)"/>
31+
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="$(MicrosoftAspNetCoreMvcTestingVersion)"/>
3232
<PackageVersion Include="Asp.Versioning.Http" Version="8.1.1"/>
3333
</ItemGroup>
3434
</Project>

ErrorOrX.slnx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
<Folder Name="/samples/">
2424
<Project Path="samples/DiagnosticsDemos/DiagnosticsDemos.csproj"/>
2525
<Project Path="samples/ErrorOrX.Sample/ErrorOrX.Sample.csproj"/>
26+
<Project Path="samples/ErrorOrX.Validation.Showcase.Models/ErrorOrX.Validation.Showcase.Models.csproj"/>
27+
<Project Path="samples/ErrorOrX.Validation.Showcase/ErrorOrX.Validation.Showcase.csproj"/>
2628
</Folder>
2729

2830
<!-- Tests -->

nuget.config

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<configuration>
33
<packageSources>
4-
<clear/>
5-
<add key="nuget.org" value="https://api.nuget.org/v3/index.json"/>
4+
<clear />
5+
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
66
</packageSources>
77

88
<!-- Package Source Mapping: ensures all packages come from nuget.org (fixes NU1507) -->
99
<packageSourceMapping>
1010
<packageSource key="nuget.org">
11-
<package pattern="*"/>
11+
<package pattern="*" />
1212
</packageSource>
1313
</packageSourceMapping>
1414
</configuration>

samples/Directory.Build.props

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<Project>
2+
<!-- Import root build settings -->
3+
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" Condition="'$(DirectoryBuildPropsImported)' != 'true'"/>
4+
<PropertyGroup>
5+
<DirectoryBuildPropsImported>true</DirectoryBuildPropsImported>
6+
</PropertyGroup>
7+
8+
<!-- Sample/demo code doesn't need production-level analysis -->
9+
<PropertyGroup>
10+
<AnalysisLevel>none</AnalysisLevel>
11+
</PropertyGroup>
12+
</Project>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System.ComponentModel.DataAnnotations;
2+
3+
namespace ErrorOrX.Validation.Showcase.Models;
4+
5+
public sealed record CreateOrderRequest(
6+
[Required] [StringLength(200, MinimumLength = 1)]
7+
string CustomerName,
8+
[Required] [EmailAddress] string Email,
9+
[Required] [MinLength(1)] IReadOnlyList<OrderItem> Items);
10+
11+
public sealed record OrderItem(
12+
[Required] [StringLength(100)] string ProductName,
13+
[Range(1, 10000)] int Quantity,
14+
[Range(0.01, 999999.99)] decimal UnitPrice);
15+
16+
public sealed record UpdateOrderRequest(
17+
18+
[StringLength(200)] string? CustomerName,
19+
[EmailAddress] string? Email);
20+
21+
public sealed record Order(
22+
Guid Id,
23+
string CustomerName,
24+
string Email,
25+
IReadOnlyList<OrderItem> Items,
26+
decimal TotalAmount,
27+
DateTimeOffset CreatedAt);

0 commit comments

Comments
 (0)