Skip to content

Commit 2844012

Browse files
committed
add support for generic ad hoc unions
1 parent 3c1061e commit 2844012

File tree

69 files changed

+6958
-192
lines changed

Some content is hidden

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

69 files changed

+6958
-192
lines changed

.claude/CLAUDE.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ A .NET library providing Smart Enums, Value Objects, and Discriminated Unions vi
5050
| Keyless Smart Enum | `[SmartEnum]` | Type-safe enum without underlying value. Identified by field reference only. |
5151
| Simple Value Object | `[ValueObject<TKey>]` | Single-value immutable type wrapping one underlying value. |
5252
| Complex Value Object | `[ComplexValueObject]` | Multi-property immutable type. |
53-
| Ad-hoc Union | `[Union<T1, T2, ...>]` or `[AdHocUnion(...)]` | "One of" several types. Up to 5 type parameters. Cannot be generic. |
53+
| Ad-hoc Union | `[Union<T1, T2, ...>]` or `[AdHocUnion(...)]` | "One of" several types. Up to 5 type parameters. Can be generic via `TypeParamRef1``TypeParamRef5` placeholders. |
5454
| Regular Union | `[Union]` | Inheritance-based union. Abstract base with sealed derived types. |
5555

5656
All types must be declared as `partial`. Source generators produce: factory methods (`Create`, `TryCreate`, `Validate`), equality members, conversion operators, `Switch`/`Map` pattern matching, `IParsable<T>`/`ISpanParsable<T>` (NET9+), and serialization integration.
@@ -94,6 +94,8 @@ All types must be declared as `partial`. Source generators produce: factory meth
9494

9595
**Ad-hoc Unions** (`[Union<T1, T2>]` or `[AdHocUnion(...)]`):
9696

97+
- Generic unions: use `TypeParamRef1``TypeParamRef5` placeholders (in namespace `Thinktecture`) to reference the union's own type parameters. E.g., `[Union<TypeParamRef1, string>] public partial struct Result<T>`. Nested usage supported (e.g., `List<TypeParamRef1>`). For type parameter members: no implicit/explicit conversion operators (C# limitation), factory methods generated instead.
98+
- `allows ref struct` is **not supported** on ad-hoc union type parameters (TTRESG073). Ref structs cannot be boxed, which conflicts with equality, `Value` property, and Switch/Map delegate patterns.
9799
- Stateless types (`TXIsStateless = true`): store only discriminator, not instance. Prefer structs.
98100
- Backing fields: with 2+ distinct non-stateless reference types, reference types auto-share a single `object? _obj` field (value types keep typed fields). `UseSingleBackingField = true` forces all types (including value types, with boxing) into `_obj`.
99101

@@ -156,7 +158,7 @@ SyntaxProvider (filter by attribute via ForAttributeWithMetadataName)
156158

157159
### Analyzers & Refactorings
158160

159-
1. `ThinktectureRuntimeExtensionsAnalyzer` -- 57 diagnostic rules (`TTRESG` prefix) for correct usage
161+
1. `ThinktectureRuntimeExtensionsAnalyzer` -- 59 diagnostic rules (`TTRESG` prefix) for correct usage
160162
2. `ThinktectureRuntimeExtensionsInternalUsageAnalyzer` -- Prevents external use of internal APIs
161163
3. `SwitchMapCompletionRefactoringProvider` -- IDE refactoring (light bulb action) that auto-generates arguments for `Switch`/`Map`/`SwitchPartially`/`MapPartially` method calls on Smart Enums and Unions
162164

.claude/guides/IMPLEMENTATION.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ public partial class PriorityLevel<TContext> where TContext : notnull
154154

155155
Note: Generated partial declarations do not need to repeat generic constraints -- C# inherits them from the original declaration.
156156

157-
**Important**: Ad-hoc unions cannot be generic (the analyzer enforces this via a diagnostic).
157+
**Important**: Ad-hoc unions can be generic via `TypeParamRef1``TypeParamRef5` placeholders. The source generator resolves these placeholders to the union's own type parameters. For type parameter members, factory methods are generated instead of conversion operators (C# limitation). See diagnostics TTRESG071, TTRESG072, TTRESG107 for validation rules.
158158

159159
## Nullability Awareness
160160

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<Copyright>(c) $([System.DateTime]::Now.Year), Pawel Gerr. All rights reserved.</Copyright>
5-
<VersionPrefix>10.1.2</VersionPrefix>
5+
<VersionPrefix>10.2.0</VersionPrefix>
66
<Authors>Pawel Gerr</Authors>
77
<GenerateDocumentationFile>true</GenerateDocumentationFile>
88
<PackageProjectUrl>https://github.com/PawelGerr/Thinktecture.Runtime.Extensions</PackageProjectUrl>

docs

Submodule docs updated from 2294ccd to 5c98f92

src/Thinktecture.Runtime.Extensions.Analyzers/CodeAnalysis/Diagnostics/ThinktectureRuntimeExtensionsAnalyzer.cs

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ public sealed class ThinktectureRuntimeExtensionsAnalyzer : DiagnosticAnalyzer
2323
DiagnosticsDescriptors.InnerSmartEnumOnNonFirstLevelMustBePublic,
2424
DiagnosticsDescriptors.KeyMemberShouldNotBeNullable,
2525
DiagnosticsDescriptors.StaticPropertiesAreNotConsideredItems,
26-
DiagnosticsDescriptors.AdHocUnionsMustNotBeGeneric,
2726
DiagnosticsDescriptors.BaseClassFieldMustBeReadOnly,
2827
DiagnosticsDescriptors.BaseClassPropertyMustBeReadOnly,
2928
DiagnosticsDescriptors.SmartEnumKeyShouldNotBeNullable,
@@ -558,7 +557,6 @@ private static void ValidateAdHocUnion(
558557

559558
CheckConstructors(context, type, mustBePrivate: false, canHavePrimaryConstructor: false);
560559
TypeMustBePartial(context, type);
561-
TypeMustNotBeGeneric(context, type, type.GetTypeIdentifierLocation(context.CancellationToken));
562560
}
563561

564562
private static void ValidateRegularUnion(
@@ -1102,18 +1100,6 @@ private static void ValidateKeyedSmartEnum(
11021100
ValidateKeyMemberComparers(context, enumType, keyType, smartEnumAttribute, factory, tdsLocation, false);
11031101
}
11041102

1105-
private static void TypeMustNotBeGeneric(SymbolAnalysisContext context, INamedTypeSymbol type, Location tdsLocation)
1106-
{
1107-
if (!type.TypeParameters.IsDefaultOrEmpty)
1108-
{
1109-
ReportDiagnostic(
1110-
context,
1111-
DiagnosticsDescriptors.AdHocUnionsMustNotBeGeneric,
1112-
tdsLocation,
1113-
BuildTypeName(type));
1114-
}
1115-
}
1116-
11171103
private static void Check_ItemLike_StaticProperties(
11181104
SymbolAnalysisContext context,
11191105
INamedTypeSymbol enumType)

src/Thinktecture.Runtime.Extensions.Roslyn.Sources/AnalyzerReleases.Shipped.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
TTRESG049 | ThinktectureRuntimeExtensionsAnalyzer | Warning | Complex Value Object with string members needs equality comparer
3434
TTRESG050 | ThinktectureRuntimeExtensionsAnalyzer | Error | Method with UseDelegateFromConstructor must be partial
3535
TTRESG051 | ThinktectureRuntimeExtensionsAnalyzer | Error | Method with UseDelegateFromConstructor must not have generics
36-
TTRESG052 | ThinktectureRuntimeExtensionsAnalyzer | Error | The type must not be inside generic type
3736
TTRESG053 | ThinktectureRuntimeExtensionsAnalyzer | Error | Derived type of a union must not be generic
3837
TTRESG054 | ThinktectureRuntimeExtensionsAnalyzer | Error | Discriminated union must be sealed or have private constructors only
3938
TTRESG055 | ThinktectureRuntimeExtensionsAnalyzer | Error | Discriminated union implemented using a record must be sealed
Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
### New Rules
22

3-
Rule ID | Category | Severity | Notes
4-
---------|----------|----------|-------
3+
Rule ID | Category | Severity | Notes
4+
-----------|---------------------------------------|----------|-----------------------------------------------------------------------------
5+
TTRESG071 | ThinktectureRuntimeExtensionsAnalyzer | Error | TypeParamRef index exceeds the number of type parameters
6+
TTRESG072 | ThinktectureRuntimeExtensionsAnalyzer | Error | TypeParamRef cannot be used on non-generic ad-hoc union
7+
TTRESG073 | ThinktectureRuntimeExtensionsAnalyzer | Error | Ad-hoc unions do not support 'allows ref struct' type parameters
8+
TTRESG107 | ThinktectureRuntimeExtensionsAnalyzer | Warning | Generic ad-hoc union does not reference any type parameter via TypeParamRef
59

610
### Removed Rules
711

8-
Rule ID | Category | Severity | Notes
9-
---------|----------|----------|-------
10-
TTRESG052 | Thinktecture.Runtime.Extensions | Error | Types are now allowed inside generic types
12+
Rule ID | Category | Severity | Notes
13+
-----------|---------------------------------------|----------|---------------------------------------------
14+
TTRESG033 | ThinktectureRuntimeExtensionsAnalyzer | Error | Ad hoc unions are now allowed to be generic

src/Thinktecture.Runtime.Extensions.Roslyn.Sources/CodeAnalysis/Constants.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,23 @@ public static class Convertible
9494
}
9595
}
9696

97+
public static class TypeParamRef
98+
{
99+
public const string NAMESPACE = "Thinktecture";
100+
101+
public const string NAME_1 = "TypeParamRef1";
102+
public const string NAME_2 = "TypeParamRef2";
103+
public const string NAME_3 = "TypeParamRef3";
104+
public const string NAME_4 = "TypeParamRef4";
105+
public const string NAME_5 = "TypeParamRef5";
106+
107+
public const string FULL_NAME_1 = $"{NAMESPACE}.{NAME_1}";
108+
public const string FULL_NAME_2 = $"{NAMESPACE}.{NAME_2}";
109+
public const string FULL_NAME_3 = $"{NAMESPACE}.{NAME_3}";
110+
public const string FULL_NAME_4 = $"{NAMESPACE}.{NAME_4}";
111+
public const string FULL_NAME_5 = $"{NAMESPACE}.{NAME_5}";
112+
}
113+
97114
public static class Attributes
98115
{
99116
public const string NAMESPACE = "Thinktecture";

src/Thinktecture.Runtime.Extensions.Roslyn.Sources/CodeAnalysis/DiagnosticsDescriptors.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ internal static class DiagnosticsDescriptors
1212
public static readonly DiagnosticDescriptor InnerSmartEnumOnFirstLevelMustBePrivate = new("TTRESG014", "Inner Smart Enum on first level must be private", "Derived inner Smart Enum '{0}' on first-level must be private", Constants.ANALYZER_CATEGORY, DiagnosticSeverity.Error, true);
1313
public static readonly DiagnosticDescriptor InnerSmartEnumOnNonFirstLevelMustBePublic = new("TTRESG015", "Inner Smart Enum on non-first level must be public", "Derived inner Smart Enum '{0}' on non-first-level must be public", Constants.ANALYZER_CATEGORY, DiagnosticSeverity.Error, true);
1414
public static readonly DiagnosticDescriptor KeyMemberShouldNotBeNullable = new("TTRESG017", "The key member must not be nullable", "A key member must not be nullable", Constants.ANALYZER_CATEGORY, DiagnosticSeverity.Error, true);
15-
public static readonly DiagnosticDescriptor AdHocUnionsMustNotBeGeneric = new("TTRESG033", "Ad hoc unions must not be generic", "Type '{0}' must not be generic", Constants.ANALYZER_CATEGORY, DiagnosticSeverity.Error, true);
1615
public static readonly DiagnosticDescriptor BaseClassFieldMustBeReadOnly = new("TTRESG034", "Field of the base class must be read-only", "The field '{0}' of the base class '{1}' must be read-only", Constants.ANALYZER_CATEGORY, DiagnosticSeverity.Error, true);
1716
public static readonly DiagnosticDescriptor BaseClassPropertyMustBeReadOnly = new("TTRESG035", "Property of the base class must be read-only", "The property '{0}' of the base class '{1}' must be read-only", Constants.ANALYZER_CATEGORY, DiagnosticSeverity.Error, true);
1817
public static readonly DiagnosticDescriptor SmartEnumKeyShouldNotBeNullable = new("TTRESG036", "The key type must not be nullable", "The generic type T of SmartEnumAttribute<T> must not be nullable", Constants.ANALYZER_CATEGORY, DiagnosticSeverity.Error, true);
@@ -46,6 +45,9 @@ internal static class DiagnosticsDescriptors
4645
public static readonly DiagnosticDescriptor MultipleObjectFactoryAttributesWithUseWithEntityFramework = new("TTRESG068", "Multiple ObjectFactoryAttribute instances cannot have UseWithEntityFramework = true", "Type '{0}' has multiple ObjectFactoryAttribute instances with 'UseWithEntityFramework = true'. Only one ObjectFactoryAttribute can have 'UseWithEntityFramework = true'.", Constants.ANALYZER_CATEGORY, DiagnosticSeverity.Error, true);
4746
public static readonly DiagnosticDescriptor MultipleObjectFactoryAttributesWithUseForModelBinding = new("TTRESG069", "Multiple ObjectFactoryAttribute instances cannot have UseForModelBinding = true", "Type '{0}' has multiple ObjectFactoryAttribute instances with 'UseForModelBinding = true'. Only one ObjectFactoryAttribute can have 'UseForModelBinding = true'.", Constants.ANALYZER_CATEGORY, DiagnosticSeverity.Error, true);
4847
public static readonly DiagnosticDescriptor MultipleObjectFactoryAttributesWithOverlappingSerializationFrameworks = new("TTRESG070", "Multiple ObjectFactoryAttribute instances cannot specify overlapping serialization frameworks", "Type '{0}' has multiple ObjectFactoryAttribute instances with overlapping serialization frameworks '{1}'. Each serialization framework can only be used by one ObjectFactoryAttribute.", Constants.ANALYZER_CATEGORY, DiagnosticSeverity.Error, true);
48+
public static readonly DiagnosticDescriptor TypeParamRefIndexOutOfRange = new("TTRESG071", "TypeParamRef index exceeds the number of type parameters", "TypeParamRef{0} references type parameter at index {0}, but the union type '{1}' has only {2} type parameter(s)", Constants.ANALYZER_CATEGORY, DiagnosticSeverity.Error, true);
49+
public static readonly DiagnosticDescriptor TypeParamRefOnNonGenericUnion = new("TTRESG072", "TypeParamRef cannot be used on non-generic ad-hoc union", "TypeParamRef{0} cannot be used on non-generic ad-hoc union '{1}'", Constants.ANALYZER_CATEGORY, DiagnosticSeverity.Error, true);
50+
public static readonly DiagnosticDescriptor AllowsRefStructNotSupportedOnAdHocUnion = new("TTRESG073", "Ad-hoc unions do not support 'allows ref struct' type parameters", "Ad-hoc union '{0}' has type parameter '{1}' with 'allows ref struct' which is not supported. Remove the 'allows ref struct' anti-constraint from the type parameter.", Constants.ANALYZER_CATEGORY, DiagnosticSeverity.Error, true);
4951

5052
public static readonly DiagnosticDescriptor ErrorDuringModulesAnalysis = new("TTRESG097", "Error during analysis of referenced modules", "Error during analysis of referenced modules: '{0}'", Constants.ANALYZER_CATEGORY, DiagnosticSeverity.Error, true);
5153
public static readonly DiagnosticDescriptor ErrorDuringCodeAnalysis = new("TTRESG098", "Error during code analysis", "Error during code analysis of '{0}': '{1}'", Constants.ANALYZER_CATEGORY, DiagnosticSeverity.Warning, true);
@@ -58,6 +60,7 @@ internal static class DiagnosticsDescriptors
5860
public static readonly DiagnosticDescriptor MembersDisallowingDefaultValuesMustBeRequired = new("TTRESG104", "The member must be marked as 'required' to ensure proper initialization", "The {0} '{1}' of type '{2}' must be marked as 'required' to ensure proper initialization", Constants.ANALYZER_CATEGORY, DiagnosticSeverity.Warning, true);
5961
public static readonly DiagnosticDescriptor ComparisonAndEqualityOperatorsMismatch = new("TTRESG105", "Comparison and equality operators settings mismatch", "The type '{0}' has 'ComparisonOperators = {1}' and 'EqualityComparisonOperators = {2}' which differ. Set them to the same value.", Constants.ANALYZER_CATEGORY, DiagnosticSeverity.Warning, true);
6062
public static readonly DiagnosticDescriptor InnerTypeDoesNotDeriveFromUnion = new("TTRESG106", "Inner type should derive from union type", "The inner type '{0}' should derive from union type '{1}'", Constants.ANALYZER_CATEGORY, DiagnosticSeverity.Warning, true);
63+
public static readonly DiagnosticDescriptor GenericUnionWithoutTypeParamRef = new("TTRESG107", "Generic ad-hoc union does not reference any type parameter via TypeParamRef", "Generic ad-hoc union '{0}' does not reference any type parameter via TypeParamRef. All type parameters are unused.", Constants.ANALYZER_CATEGORY, DiagnosticSeverity.Warning, true);
6164

6265
public static readonly DiagnosticDescriptor InternalApiUsage = new("TTRESG1000", "Internal Thinktecture.Runtime.Extensions API usage", "'{0}' is an internal API that supports the Thinktecture.Runtime.Extensions infrastructure and not subject to the same compatibility standards as public APIs. It may be changed or removed without notice in any release.", Constants.INTERNAL_USAGE_ANALYZER_CATEGORY, DiagnosticSeverity.Warning, true);
6366

0 commit comments

Comments
 (0)