From 416c8a3209c34cd0315575d637da34d4fb54bb24 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Tue, 31 Mar 2026 16:37:23 -0400 Subject: [PATCH 1/4] feat: add [OptimizedEnumIndex] attribute for generated property lookups Introduces OptimizedEnumIndexAttribute, which can be placed on properties in intermediate OptimizedEnum base classes to generate pre-built dictionary lookups (From{PropertyName} / TryFrom{PropertyName}) on every concrete subclass. Supports any IEquatable property type; string properties respect a configurable StringComparison (default: Ordinal). Also bumps VersionPrefix to 1.2.0 and updates local pack scripts to use a -local. pre-release suffix instead of a fourth version segment. Co-Authored-By: Claude Sonnet 4.6 --- Directory.Build.props | 2 +- .../AnalyzerReleases.Unshipped.md | 1 + .../Diagnostics/DiagnosticDescriptors.cs | 8 + .../Emitters/EnumEmitter.cs | 1 + .../Models/EnumInfo.cs | 6 +- .../Models/IndexedPropertyInfo.cs | 19 +++ .../Providers/EnumSyntaxProvider.cs | 99 +++++++++++++ .../Templates/OptimizedEnum.scriban | 26 ++++ .../OptimizedEnumIndexAttribute.cs | 24 +++ .../GeneratorVerifyTests.cs | 112 ++++++++++++++ ...#MyApp.Domain.ForceAlignment.g.verified.cs | 140 ++++++++++++++++++ ...#MyApp.Domain.ForceAlignment.g.verified.cs | 125 ++++++++++++++++ ...sEmitted#MyApp.Domain.Status.g.verified.cs | 93 ++++++++++++ ...ty_NonEquatableType_IsEmitted.verified.txt | 18 +++ 14 files changed, 671 insertions(+), 3 deletions(-) create mode 100644 src/LayeredCraft.OptimizedEnums.Generator/Models/IndexedPropertyInfo.cs create mode 100644 src/LayeredCraft.OptimizedEnums/OptimizedEnumIndexAttribute.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.IndexedProperty_MultipleIndexes_GeneratesAllLookupMethods#MyApp.Domain.ForceAlignment.g.verified.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.IndexedProperty_StringIndex_GeneratesLookupMethods#MyApp.Domain.ForceAlignment.g.verified.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0202_IndexProperty_NonEquatableType_IsEmitted#MyApp.Domain.Status.g.verified.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0202_IndexProperty_NonEquatableType_IsEmitted.verified.txt diff --git a/Directory.Build.props b/Directory.Build.props index cf82bab..7dfec9e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 1.1.1 + 1.2.0 MIT https://github.com/layeredcraft/optimized-enums git diff --git a/src/LayeredCraft.OptimizedEnums.Generator/AnalyzerReleases.Unshipped.md b/src/LayeredCraft.OptimizedEnums.Generator/AnalyzerReleases.Unshipped.md index 491d7fc..1244adb 100644 --- a/src/LayeredCraft.OptimizedEnums.Generator/AnalyzerReleases.Unshipped.md +++ b/src/LayeredCraft.OptimizedEnums.Generator/AnalyzerReleases.Unshipped.md @@ -11,4 +11,5 @@ OE0006 | OptimizedEnums.Usage | Error | DiagnosticDescriptors OE0101 | OptimizedEnums.Usage | Warning | DiagnosticDescriptors OE0102 | OptimizedEnums.Usage | Warning | DiagnosticDescriptors + OE0202 | OptimizedEnums.Usage | Warning | DiagnosticDescriptors OE9001 | OptimizedEnums.Usage | Error | DiagnosticDescriptors diff --git a/src/LayeredCraft.OptimizedEnums.Generator/Diagnostics/DiagnosticDescriptors.cs b/src/LayeredCraft.OptimizedEnums.Generator/Diagnostics/DiagnosticDescriptors.cs index 81fc72a..8a0b21f 100644 --- a/src/LayeredCraft.OptimizedEnums.Generator/Diagnostics/DiagnosticDescriptors.cs +++ b/src/LayeredCraft.OptimizedEnums.Generator/Diagnostics/DiagnosticDescriptors.cs @@ -54,6 +54,14 @@ internal static class DiagnosticDescriptors DiagnosticSeverity.Warning, isEnabledByDefault: true); + internal static readonly DiagnosticDescriptor IndexPropertyNotEquatable = new( + "OE0202", + "OptimizedEnumIndex property type does not implement IEquatable", + "The property '{0}' of type '{1}' is marked [OptimizedEnumIndex] but its type does not implement IEquatable; the generated dictionary will use reference equality, which is almost certainly incorrect. Implement IEquatable on the property type or remove the attribute.", + UsageCategory, + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + internal static readonly DiagnosticDescriptor GeneratorInternalError = new( "OE9001", "OptimizedEnum generator internal error", diff --git a/src/LayeredCraft.OptimizedEnums.Generator/Emitters/EnumEmitter.cs b/src/LayeredCraft.OptimizedEnums.Generator/Emitters/EnumEmitter.cs index 6974cea..a1eb296 100644 --- a/src/LayeredCraft.OptimizedEnums.Generator/Emitters/EnumEmitter.cs +++ b/src/LayeredCraft.OptimizedEnums.Generator/Emitters/EnumEmitter.cs @@ -24,6 +24,7 @@ internal static void Generate(SourceProductionContext context, EnumInfo info) info.FullyQualifiedClassName, info.ValueTypeFullyQualified, MemberNames = info.MemberNames.ToArray(), + IndexedProperties = info.IndexedProperties.ToArray(), Preamble = BuildPreamble(info), Suffix = BuildSuffix(info), }; diff --git a/src/LayeredCraft.OptimizedEnums.Generator/Models/EnumInfo.cs b/src/LayeredCraft.OptimizedEnums.Generator/Models/EnumInfo.cs index 433b5c2..5ea6f6b 100644 --- a/src/LayeredCraft.OptimizedEnums.Generator/Models/EnumInfo.cs +++ b/src/LayeredCraft.OptimizedEnums.Generator/Models/EnumInfo.cs @@ -14,6 +14,7 @@ internal sealed record EnumInfo( EquatableArray MemberNames, EquatableArray ContainingTypeNames, EquatableArray Diagnostics, + EquatableArray IndexedProperties, LocationInfo? Location ) { @@ -28,9 +29,10 @@ other is not null && ValueTypeFullyQualified == other.ValueTypeFullyQualified && MemberNames == other.MemberNames && ContainingTypeNames == other.ContainingTypeNames - && Diagnostics == other.Diagnostics; + && Diagnostics == other.Diagnostics + && IndexedProperties == other.IndexedProperties; public override int GetHashCode() => HashCode.Combine(Namespace, ClassName, FullyQualifiedClassName, ValueTypeFullyQualified, - MemberNames, ContainingTypeNames, Diagnostics); + MemberNames, ContainingTypeNames, Diagnostics, IndexedProperties); } diff --git a/src/LayeredCraft.OptimizedEnums.Generator/Models/IndexedPropertyInfo.cs b/src/LayeredCraft.OptimizedEnums.Generator/Models/IndexedPropertyInfo.cs new file mode 100644 index 0000000..7c67dc0 --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.Generator/Models/IndexedPropertyInfo.cs @@ -0,0 +1,19 @@ +namespace LayeredCraft.OptimizedEnums.Generator.Models; + +/// +/// Represents a property decorated with [OptimizedEnumIndex] that the generator +/// should emit a pre-built dictionary lookup for on the concrete enum class. +/// +/// The property name as declared (e.g. "SlotValue"). +/// The fully-qualified type of the property. +/// True when the property type is . +/// +/// The StringComparer expression to pass to the dictionary constructor +/// (e.g. "global::System.StringComparer.Ordinal"). Empty for non-string types. +/// +internal sealed record IndexedPropertyInfo( + string PropertyName, + string PropertyTypeFullyQualified, + bool IsStringType, + string StringComparerExpression +) : IEquatable; \ No newline at end of file diff --git a/src/LayeredCraft.OptimizedEnums.Generator/Providers/EnumSyntaxProvider.cs b/src/LayeredCraft.OptimizedEnums.Generator/Providers/EnumSyntaxProvider.cs index 4a73813..d8119e6 100644 --- a/src/LayeredCraft.OptimizedEnums.Generator/Providers/EnumSyntaxProvider.cs +++ b/src/LayeredCraft.OptimizedEnums.Generator/Providers/EnumSyntaxProvider.cs @@ -53,6 +53,7 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) => MemberNames: EquatableArray.Empty, ContainingTypeNames: EquatableArray.Empty, Diagnostics: diagnostics.ToEquatableArray(), + IndexedProperties: EquatableArray.Empty, Location: location); } @@ -142,6 +143,9 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) => className)); } + var indexedProperties = CollectIndexedProperties( + classSymbol, context.SemanticModel.Compilation, diagnostics); + return new EnumInfo( Namespace: GetNamespace(classSymbol), ClassName: className, @@ -150,6 +154,7 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) => MemberNames: validMembers.ToEquatableArray(), ContainingTypeNames: GetContainingTypeDeclarations(classSymbol), Diagnostics: diagnostics.ToEquatableArray(), + IndexedProperties: indexedProperties, Location: location); } @@ -239,6 +244,100 @@ ObjectCreationExpressionSyntax or } } + private static EquatableArray CollectIndexedProperties( + INamedTypeSymbol classSymbol, + Compilation compilation, + List diagnostics) + { + const string attrMetadataName = "LayeredCraft.OptimizedEnums.OptimizedEnumIndexAttribute"; + const string iEquatableMetadataName = "System.IEquatable`1"; + + var attrSymbol = compilation.GetTypeByMetadataName(attrMetadataName); + if (attrSymbol is null) + return EquatableArray.Empty; + + var iEquatableSymbol = compilation.GetTypeByMetadataName(iEquatableMetadataName); + var optimizedEnumBase = compilation.GetTypeByMetadataName(OptimizedEnumBaseMetadataName); + + var result = new List(); + var seenNames = new HashSet(StringComparer.Ordinal); + + // Walk base chain (skip the concrete class itself), stop at OptimizedEnum<,> + var current = classSymbol.BaseType; + while (current is not null) + { + if (optimizedEnumBase is not null && + SymbolEqualityComparer.Default.Equals(current.OriginalDefinition, optimizedEnumBase)) + break; + + foreach (var member in current.GetMembers().OfType()) + { + var attr = member.GetAttributes() + .FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attrSymbol)); + + if (attr is null || !seenNames.Add(member.Name)) + continue; + + var propType = member.Type; + + // OE0202: warn if property type doesn't implement IEquatable + if (iEquatableSymbol is not null) + { + var implementsIEquatable = propType.AllInterfaces.Any(i => + SymbolEqualityComparer.Default.Equals(i.OriginalDefinition, iEquatableSymbol) && + i.TypeArguments.Length == 1 && + SymbolEqualityComparer.Default.Equals(i.TypeArguments[0], propType)); + + if (!implementsIEquatable) + { + diagnostics.Add(new DiagnosticInfo( + DiagnosticDescriptors.IndexPropertyNotEquatable, + member.CreateLocationInfo(), + member.Name, + propType.ToDisplayString())); + continue; + } + } + + var isString = propType.SpecialType == SpecialType.System_String; + var comparerExpr = string.Empty; + + if (isString) + { + var comparisonValue = 4; // StringComparison.Ordinal default + foreach (var namedArg in attr.NamedArguments) + { + if (namedArg.Key == "StringComparison" && namedArg.Value.Value is int cv) + { + comparisonValue = cv; + break; + } + } + + comparerExpr = comparisonValue switch + { + 0 => "global::System.StringComparer.CurrentCulture", + 1 => "global::System.StringComparer.CurrentCultureIgnoreCase", + 2 => "global::System.StringComparer.InvariantCulture", + 3 => "global::System.StringComparer.InvariantCultureIgnoreCase", + 5 => "global::System.StringComparer.OrdinalIgnoreCase", + _ => "global::System.StringComparer.Ordinal" + }; + } + + result.Add(new IndexedPropertyInfo( + PropertyName: member.Name, + PropertyTypeFullyQualified: propType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + IsStringType: isString, + StringComparerExpression: comparerExpr)); + } + + current = current.BaseType; + } + + return result.ToEquatableArray(); + } + private static EquatableArray GetContainingTypeDeclarations(INamedTypeSymbol symbol) { var result = new List(); diff --git a/src/LayeredCraft.OptimizedEnums.Generator/Templates/OptimizedEnum.scriban b/src/LayeredCraft.OptimizedEnums.Generator/Templates/OptimizedEnum.scriban index 2d70536..c9641c5 100644 --- a/src/LayeredCraft.OptimizedEnums.Generator/Templates/OptimizedEnum.scriban +++ b/src/LayeredCraft.OptimizedEnums.Generator/Templates/OptimizedEnum.scriban @@ -52,6 +52,16 @@ partial class {{ class_name }} [{{ name }}.Value] = {{ name }}{{ if !for.last }},{{ end }} {{~ end ~}} }; +{{~ for prop in indexed_properties ~}} + + private static readonly global::System.Collections.Generic.Dictionary<{{ prop.property_type_fully_qualified }}, {{ fully_qualified_class_name }}> s_by{{ prop.property_name }} = + new global::System.Collections.Generic.Dictionary<{{ prop.property_type_fully_qualified }}, {{ fully_qualified_class_name }}>({{ member_names.size }}{{ if prop.is_string_type }}, {{ prop.string_comparer_expression }}{{ end }}) + { + {{~ for name in member_names ~}} + [{{ name }}.{{ prop.property_name }}] = {{ name }}{{ if !for.last }},{{ end }} + {{~ end ~}} + }; +{{~ end ~}} {{ generated_code_attribute }} public static global::System.Collections.Generic.IReadOnlyList<{{ fully_qualified_class_name }}> All => s_all; @@ -98,4 +108,20 @@ partial class {{ class_name }} {{ generated_code_attribute }} public static bool ContainsValue({{ value_type_fully_qualified }} value) => s_byValue.ContainsKey(value); +{{~ for prop in indexed_properties ~}} + + {{ generated_code_attribute }} + public static {{ fully_qualified_class_name }} From{{ prop.property_name }}({{ prop.property_type_fully_qualified }} value) + { + if (!s_by{{ prop.property_name }}.TryGetValue(value, out var result)) + throw new global::System.Collections.Generic.KeyNotFoundException( + $"'{value}' is not a valid {{ prop.property_name }} for {{ class_name }}"); + + return result; + } + + {{ generated_code_attribute }} + public static bool TryFrom{{ prop.property_name }}({{ prop.property_type_fully_qualified }} value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out {{ fully_qualified_class_name }}? result) => + s_by{{ prop.property_name }}.TryGetValue(value, out result); +{{~ end ~}} }{{ suffix }} diff --git a/src/LayeredCraft.OptimizedEnums/OptimizedEnumIndexAttribute.cs b/src/LayeredCraft.OptimizedEnums/OptimizedEnumIndexAttribute.cs new file mode 100644 index 0000000..5dab642 --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums/OptimizedEnumIndexAttribute.cs @@ -0,0 +1,24 @@ +#nullable enable + +namespace LayeredCraft.OptimizedEnums; + +/// +/// Marks a property on an intermediate OptimizedEnum base class as a lookup index. +/// The source generator will emit From{PropertyName} and TryFrom{PropertyName} +/// lookup methods backed by a pre-built dictionary on every concrete subclass. +/// +/// +/// The property type must implement ; otherwise diagnostic OE0202 +/// is emitted and the index is skipped. For string properties, use +/// to control key comparison. +/// +[AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)] +public sealed class OptimizedEnumIndexAttribute : Attribute +{ + /// + /// For properties, specifies the comparison used when building + /// the lookup dictionary. Defaults to . + /// Ignored for non-string property types. + /// + public StringComparison StringComparison { get; set; } = StringComparison.Ordinal; +} \ No newline at end of file diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/GeneratorVerifyTests.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/GeneratorVerifyTests.cs index 7aeb762..90df811 100644 --- a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/GeneratorVerifyTests.cs +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/GeneratorVerifyTests.cs @@ -394,4 +394,116 @@ private OrderStatus(int value, string name) : base(value, name) { } ExpectedDiagnosticId = "OE0005", }, TestContext.Current.CancellationToken); + + [Fact] + public async Task IndexedProperty_StringIndex_GeneratesLookupMethods() => + await GeneratorTestHelpers.Verify( + new VerifyTestOptions + { + SourceCode = """ + using LayeredCraft.OptimizedEnums; + + namespace MyApp.Domain; + + public abstract partial class SlotEnumeration : OptimizedEnum + where TSelf : SlotEnumeration + { + [OptimizedEnumIndex] + public string SlotValue { get; } + + protected SlotEnumeration(int id, string name, string slotValue) : base(id, name) + { + SlotValue = slotValue; + } + } + + public sealed partial class ForceAlignment : SlotEnumeration + { + public static readonly ForceAlignment Jedi = new(1, nameof(Jedi), "jedi"); + public static readonly ForceAlignment Sith = new(2, nameof(Sith), "sith"); + public static readonly ForceAlignment Gray = new(3, nameof(Gray), "gray"); + + private ForceAlignment(int id, string name, string slotValue) + : base(id, name, slotValue) { } + } + """, + ExpectedTrees = 1, + }, + TestContext.Current.CancellationToken); + + [Fact] + public async Task IndexedProperty_MultipleIndexes_GeneratesAllLookupMethods() => + await GeneratorTestHelpers.Verify( + new VerifyTestOptions + { + SourceCode = """ + using System; + using LayeredCraft.OptimizedEnums; + + namespace MyApp.Domain; + + public abstract partial class SlotEnumeration : OptimizedEnum + where TSelf : SlotEnumeration + { + [OptimizedEnumIndex] + public string SlotValue { get; } + + [OptimizedEnumIndex(StringComparison = StringComparison.OrdinalIgnoreCase)] + public string ImageName { get; } + + protected SlotEnumeration(int id, string name, string slotValue, string imageName) + : base(id, name) + { + SlotValue = slotValue; + ImageName = imageName; + } + } + + public sealed partial class ForceAlignment : SlotEnumeration + { + public static readonly ForceAlignment Jedi = new(1, nameof(Jedi), "jedi", "jedi.png"); + public static readonly ForceAlignment Sith = new(2, nameof(Sith), "sith", "sith.png"); + + private ForceAlignment(int id, string name, string slotValue, string imageName) + : base(id, name, slotValue, imageName) { } + } + """, + ExpectedTrees = 1, + }, + TestContext.Current.CancellationToken); + + [Fact] + public async Task Warning_OE0202_IndexProperty_NonEquatableType_IsEmitted() => + await GeneratorTestHelpers.VerifyFailure( + new VerifyTestOptions + { + SourceCode = """ + using LayeredCraft.OptimizedEnums; + + namespace MyApp.Domain; + + public class NonEquatable { } + + public abstract partial class BaseEnum : OptimizedEnum + where TSelf : BaseEnum + { + [OptimizedEnumIndex] + public NonEquatable Tag { get; } + + protected BaseEnum(int id, string name, NonEquatable tag) : base(id, name) + { + Tag = tag; + } + } + + public sealed partial class Status : BaseEnum + { + public static readonly Status Active = new(1, nameof(Active), new NonEquatable()); + + private Status(int id, string name, NonEquatable tag) : base(id, name, tag) { } + } + """, + ExpectedDiagnosticId = "OE0202", + }, + TestContext.Current.CancellationToken); } diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.IndexedProperty_MultipleIndexes_GeneratesAllLookupMethods#MyApp.Domain.ForceAlignment.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.IndexedProperty_MultipleIndexes_GeneratesAllLookupMethods#MyApp.Domain.ForceAlignment.g.verified.cs new file mode 100644 index 0000000..f48b2ac --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.IndexedProperty_MultipleIndexes_GeneratesAllLookupMethods#MyApp.Domain.ForceAlignment.g.verified.cs @@ -0,0 +1,140 @@ +//HintName: MyApp.Domain.ForceAlignment.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +namespace MyApp.Domain; + +[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] +partial class ForceAlignment +{ + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_all = + global::System.Array.AsReadOnly(new global::MyApp.Domain.ForceAlignment[] + { + Jedi, + Sith + }); + + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_names = + global::System.Array.AsReadOnly(new string[] + { + Jedi.Name, + Sith.Name + }); + + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_values = + global::System.Array.AsReadOnly(new int[] + { + Jedi.Value, + Sith.Value + }); + + private static readonly global::System.Collections.Generic.Dictionary s_byName = + new global::System.Collections.Generic.Dictionary(2, global::System.StringComparer.Ordinal) + { + [Jedi.Name] = Jedi, + [Sith.Name] = Sith + }; + + private static readonly global::System.Collections.Generic.Dictionary s_byValue = + new global::System.Collections.Generic.Dictionary(2) + { + [Jedi.Value] = Jedi, + [Sith.Value] = Sith + }; + + private static readonly global::System.Collections.Generic.Dictionary s_bySlotValue = + new global::System.Collections.Generic.Dictionary(2, global::System.StringComparer.Ordinal) + { + [Jedi.SlotValue] = Jedi, + [Sith.SlotValue] = Sith + }; + + private static readonly global::System.Collections.Generic.Dictionary s_byImageName = + new global::System.Collections.Generic.Dictionary(2, global::System.StringComparer.OrdinalIgnoreCase) + { + [Jedi.ImageName] = Jedi, + [Sith.ImageName] = Sith + }; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList All => s_all; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList Names => s_names; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList Values => s_values; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public const int Count = 2; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.ForceAlignment FromName(string name) + { + if (!s_byName.TryGetValue(name, out var result)) + throw new global::System.Collections.Generic.KeyNotFoundException( + $"'{name}' is not a valid name for ForceAlignment"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromName(string name, out global::MyApp.Domain.ForceAlignment? result) => + s_byName.TryGetValue(name, out result); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.ForceAlignment FromValue(int value) + { + if (!s_byValue.TryGetValue(value, out var result)) + throw new global::System.Collections.Generic.KeyNotFoundException( + $"'{value}' is not a valid value for ForceAlignment"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromValue(int value, out global::MyApp.Domain.ForceAlignment? result) => + s_byValue.TryGetValue(value, out result); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool ContainsName(string name) => s_byName.ContainsKey(name); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool ContainsValue(int value) => s_byValue.ContainsKey(value); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.ForceAlignment FromSlotValue(string value) + { + if (!s_bySlotValue.TryGetValue(value, out var result)) + throw new global::System.Collections.Generic.KeyNotFoundException( + $"'{value}' is not a valid SlotValue for ForceAlignment"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromSlotValue(string value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.ForceAlignment? result) => + s_bySlotValue.TryGetValue(value, out result); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.ForceAlignment FromImageName(string value) + { + if (!s_byImageName.TryGetValue(value, out var result)) + throw new global::System.Collections.Generic.KeyNotFoundException( + $"'{value}' is not a valid ImageName for ForceAlignment"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromImageName(string value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.ForceAlignment? result) => + s_byImageName.TryGetValue(value, out result); +} diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.IndexedProperty_StringIndex_GeneratesLookupMethods#MyApp.Domain.ForceAlignment.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.IndexedProperty_StringIndex_GeneratesLookupMethods#MyApp.Domain.ForceAlignment.g.verified.cs new file mode 100644 index 0000000..b047346 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.IndexedProperty_StringIndex_GeneratesLookupMethods#MyApp.Domain.ForceAlignment.g.verified.cs @@ -0,0 +1,125 @@ +//HintName: MyApp.Domain.ForceAlignment.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +namespace MyApp.Domain; + +[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] +partial class ForceAlignment +{ + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_all = + global::System.Array.AsReadOnly(new global::MyApp.Domain.ForceAlignment[] + { + Jedi, + Sith, + Gray + }); + + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_names = + global::System.Array.AsReadOnly(new string[] + { + Jedi.Name, + Sith.Name, + Gray.Name + }); + + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_values = + global::System.Array.AsReadOnly(new int[] + { + Jedi.Value, + Sith.Value, + Gray.Value + }); + + private static readonly global::System.Collections.Generic.Dictionary s_byName = + new global::System.Collections.Generic.Dictionary(3, global::System.StringComparer.Ordinal) + { + [Jedi.Name] = Jedi, + [Sith.Name] = Sith, + [Gray.Name] = Gray + }; + + private static readonly global::System.Collections.Generic.Dictionary s_byValue = + new global::System.Collections.Generic.Dictionary(3) + { + [Jedi.Value] = Jedi, + [Sith.Value] = Sith, + [Gray.Value] = Gray + }; + + private static readonly global::System.Collections.Generic.Dictionary s_bySlotValue = + new global::System.Collections.Generic.Dictionary(3, global::System.StringComparer.Ordinal) + { + [Jedi.SlotValue] = Jedi, + [Sith.SlotValue] = Sith, + [Gray.SlotValue] = Gray + }; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList All => s_all; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList Names => s_names; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList Values => s_values; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public const int Count = 3; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.ForceAlignment FromName(string name) + { + if (!s_byName.TryGetValue(name, out var result)) + throw new global::System.Collections.Generic.KeyNotFoundException( + $"'{name}' is not a valid name for ForceAlignment"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromName(string name, out global::MyApp.Domain.ForceAlignment? result) => + s_byName.TryGetValue(name, out result); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.ForceAlignment FromValue(int value) + { + if (!s_byValue.TryGetValue(value, out var result)) + throw new global::System.Collections.Generic.KeyNotFoundException( + $"'{value}' is not a valid value for ForceAlignment"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromValue(int value, out global::MyApp.Domain.ForceAlignment? result) => + s_byValue.TryGetValue(value, out result); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool ContainsName(string name) => s_byName.ContainsKey(name); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool ContainsValue(int value) => s_byValue.ContainsKey(value); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.ForceAlignment FromSlotValue(string value) + { + if (!s_bySlotValue.TryGetValue(value, out var result)) + throw new global::System.Collections.Generic.KeyNotFoundException( + $"'{value}' is not a valid SlotValue for ForceAlignment"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromSlotValue(string value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.ForceAlignment? result) => + s_bySlotValue.TryGetValue(value, out result); +} diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0202_IndexProperty_NonEquatableType_IsEmitted#MyApp.Domain.Status.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0202_IndexProperty_NonEquatableType_IsEmitted#MyApp.Domain.Status.g.verified.cs new file mode 100644 index 0000000..62d8cf0 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0202_IndexProperty_NonEquatableType_IsEmitted#MyApp.Domain.Status.g.verified.cs @@ -0,0 +1,93 @@ +//HintName: MyApp.Domain.Status.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +namespace MyApp.Domain; + +[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] +partial class Status +{ + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_all = + global::System.Array.AsReadOnly(new global::MyApp.Domain.Status[] + { + Active + }); + + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_names = + global::System.Array.AsReadOnly(new string[] + { + Active.Name + }); + + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_values = + global::System.Array.AsReadOnly(new int[] + { + Active.Value + }); + + private static readonly global::System.Collections.Generic.Dictionary s_byName = + new global::System.Collections.Generic.Dictionary(1, global::System.StringComparer.Ordinal) + { + [Active.Name] = Active + }; + + private static readonly global::System.Collections.Generic.Dictionary s_byValue = + new global::System.Collections.Generic.Dictionary(1) + { + [Active.Value] = Active + }; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList All => s_all; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList Names => s_names; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList Values => s_values; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public const int Count = 1; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.Status FromName(string name) + { + if (!s_byName.TryGetValue(name, out var result)) + throw new global::System.Collections.Generic.KeyNotFoundException( + $"'{name}' is not a valid name for Status"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromName(string name, out global::MyApp.Domain.Status? result) => + s_byName.TryGetValue(name, out result); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.Status FromValue(int value) + { + if (!s_byValue.TryGetValue(value, out var result)) + throw new global::System.Collections.Generic.KeyNotFoundException( + $"'{value}' is not a valid value for Status"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromValue(int value, out global::MyApp.Domain.Status? result) => + s_byValue.TryGetValue(value, out result); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool ContainsName(string name) => s_byName.ContainsKey(name); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool ContainsValue(int value) => s_byValue.ContainsKey(value); +} diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0202_IndexProperty_NonEquatableType_IsEmitted.verified.txt b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0202_IndexProperty_NonEquatableType_IsEmitted.verified.txt new file mode 100644 index 0000000..dc7fa80 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0202_IndexProperty_NonEquatableType_IsEmitted.verified.txt @@ -0,0 +1,18 @@ +{ + Diagnostics: [ + { + Location: Program.cs: (10,24)-(10,27), + Message: The property 'Tag' of type 'MyApp.Domain.NonEquatable' is marked [OptimizedEnumIndex] but its type does not implement IEquatable; the generated dictionary will use reference equality, which is almost certainly incorrect. Implement IEquatable on the property type or remove the attribute., + Severity: Warning, + WarningLevel: 1, + Descriptor: { + Id: OE0202, + Title: OptimizedEnumIndex property type does not implement IEquatable, + MessageFormat: The property '{0}' of type '{1}' is marked [OptimizedEnumIndex] but its type does not implement IEquatable; the generated dictionary will use reference equality, which is almost certainly incorrect. Implement IEquatable on the property type or remove the attribute., + Category: OptimizedEnums.Usage, + DefaultSeverity: Warning, + IsEnabledByDefault: true + } + } + ] +} \ No newline at end of file From cc2e6e602e0d725f569b577550f3fd4c1b191ef2 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Tue, 31 Mar 2026 21:46:33 -0400 Subject: [PATCH 2/4] fix: address Copilot PR feedback on [OptimizedEnumIndex] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OE0203: emit warning and skip index for static, private, indexer, or write-only properties marked [OptimizedEnumIndex] - OE0204: emit warning and skip index when property name conflicts with reserved generated members (Name → s_byName/FromName/TryFromName, Value → s_byValue/FromValue/TryFromValue) - Fix OE0202 message to say "the index will not be generated" rather than implying fallback to reference equality - Add [NotNullWhen(true)] to TryFromName and TryFromValue for consistency with TryFrom{PropertyName} methods - Add tests for OE0203 (private property, static property) and OE0204 (reserved name collision) - Update all affected snapshots Co-Authored-By: Claude Sonnet 4.6 --- .../AnalyzerReleases.Unshipped.md | 2 + .../Diagnostics/DiagnosticDescriptors.cs | 18 +++- .../Providers/EnumSyntaxProvider.cs | 35 +++++++ .../Templates/OptimizedEnum.scriban | 4 +- .../GeneratorVerifyTests.cs | 96 +++++++++++++++++++ ...RTP#MyApp.Domain.OrderStatus.g.verified.cs | 4 +- ...enerates#MyApp.Domain.Status.g.verified.cs | 4 +- ...#MyApp.Domain.ForceAlignment.g.verified.cs | 4 +- ...#MyApp.Domain.ForceAlignment.g.verified.cs | 4 +- ...mbers#MyApp.Domain.DayOfWeek.g.verified.cs | 4 +- ...pe#MyApp.Domain.Outer.Status.g.verified.cs | 4 +- ...espaces#MyApp.Domain1.Status.g.verified.cs | 4 +- ...espaces#MyApp.Domain2.Status.g.verified.cs | 4 +- ...num_GlobalNamespace#Priority.g.verified.cs | 4 +- ...ace#MyApp.Domain.OrderStatus.g.verified.cs | 4 +- ...ValueType#MyApp.Domain.Color.g.verified.cs | 4 +- ...tor#MyApp.Domain.OrderStatus.g.verified.cs | 4 +- ...ted#MyApp.Domain.OrderStatus.g.verified.cs | 4 +- ...ted#MyApp.Domain.OrderStatus.g.verified.cs | 4 +- ...sEmitted#MyApp.Domain.Status.g.verified.cs | 4 +- ...ty_NonEquatableType_IsEmitted.verified.txt | 4 +- ...sEmitted#MyApp.Domain.Status.g.verified.cs | 93 ++++++++++++++++++ ...rty_PrivateProperty_IsEmitted.verified.txt | 18 ++++ ...sEmitted#MyApp.Domain.Status.g.verified.cs | 93 ++++++++++++++++++ ...erty_StaticProperty_IsEmitted.verified.txt | 18 ++++ ...sEmitted#MyApp.Domain.Status.g.verified.cs | 93 ++++++++++++++++++ ...operty_ReservedName_IsEmitted.verified.txt | 18 ++++ 27 files changed, 517 insertions(+), 35 deletions(-) create mode 100644 tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0203_IndexProperty_PrivateProperty_IsEmitted#MyApp.Domain.Status.g.verified.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0203_IndexProperty_PrivateProperty_IsEmitted.verified.txt create mode 100644 tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0203_IndexProperty_StaticProperty_IsEmitted#MyApp.Domain.Status.g.verified.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0203_IndexProperty_StaticProperty_IsEmitted.verified.txt create mode 100644 tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0204_IndexProperty_ReservedName_IsEmitted#MyApp.Domain.Status.g.verified.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0204_IndexProperty_ReservedName_IsEmitted.verified.txt diff --git a/src/LayeredCraft.OptimizedEnums.Generator/AnalyzerReleases.Unshipped.md b/src/LayeredCraft.OptimizedEnums.Generator/AnalyzerReleases.Unshipped.md index 1244adb..cb83ee6 100644 --- a/src/LayeredCraft.OptimizedEnums.Generator/AnalyzerReleases.Unshipped.md +++ b/src/LayeredCraft.OptimizedEnums.Generator/AnalyzerReleases.Unshipped.md @@ -12,4 +12,6 @@ OE0101 | OptimizedEnums.Usage | Warning | DiagnosticDescriptors OE0102 | OptimizedEnums.Usage | Warning | DiagnosticDescriptors OE0202 | OptimizedEnums.Usage | Warning | DiagnosticDescriptors + OE0203 | OptimizedEnums.Usage | Warning | DiagnosticDescriptors + OE0204 | OptimizedEnums.Usage | Warning | DiagnosticDescriptors OE9001 | OptimizedEnums.Usage | Error | DiagnosticDescriptors diff --git a/src/LayeredCraft.OptimizedEnums.Generator/Diagnostics/DiagnosticDescriptors.cs b/src/LayeredCraft.OptimizedEnums.Generator/Diagnostics/DiagnosticDescriptors.cs index 8a0b21f..faa18ec 100644 --- a/src/LayeredCraft.OptimizedEnums.Generator/Diagnostics/DiagnosticDescriptors.cs +++ b/src/LayeredCraft.OptimizedEnums.Generator/Diagnostics/DiagnosticDescriptors.cs @@ -57,7 +57,23 @@ internal static class DiagnosticDescriptors internal static readonly DiagnosticDescriptor IndexPropertyNotEquatable = new( "OE0202", "OptimizedEnumIndex property type does not implement IEquatable", - "The property '{0}' of type '{1}' is marked [OptimizedEnumIndex] but its type does not implement IEquatable; the generated dictionary will use reference equality, which is almost certainly incorrect. Implement IEquatable on the property type or remove the attribute.", + "The property '{0}' of type '{1}' is marked [OptimizedEnumIndex] but its type does not implement IEquatable; the index will not be generated. Implement IEquatable on the property type or remove the attribute.", + UsageCategory, + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + internal static readonly DiagnosticDescriptor IndexPropertyNotAccessible = new( + "OE0203", + "OptimizedEnumIndex property must be an accessible instance property with a readable getter", + "The property '{0}' on '{1}' is marked [OptimizedEnumIndex] but cannot be used as an index because it must be a non-static, accessible instance property with a readable getter. The index will not be generated.", + UsageCategory, + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + internal static readonly DiagnosticDescriptor IndexPropertyNameConflict = new( + "OE0204", + "OptimizedEnumIndex property name conflicts with a reserved generated member", + "The property '{0}' is marked [OptimizedEnumIndex] but its name conflicts with a reserved generated member name ('{1}'). The index will not be generated. Rename the property or remove the attribute.", UsageCategory, DiagnosticSeverity.Warning, isEnabledByDefault: true); diff --git a/src/LayeredCraft.OptimizedEnums.Generator/Providers/EnumSyntaxProvider.cs b/src/LayeredCraft.OptimizedEnums.Generator/Providers/EnumSyntaxProvider.cs index d8119e6..c22f8f3 100644 --- a/src/LayeredCraft.OptimizedEnums.Generator/Providers/EnumSyntaxProvider.cs +++ b/src/LayeredCraft.OptimizedEnums.Generator/Providers/EnumSyntaxProvider.cs @@ -259,6 +259,11 @@ private static EquatableArray CollectIndexedProperties( var iEquatableSymbol = compilation.GetTypeByMetadataName(iEquatableMetadataName); var optimizedEnumBase = compilation.GetTypeByMetadataName(OptimizedEnumBaseMetadataName); + // Property names that would collide with existing generated members. + // "Name" → s_byName / FromName / TryFromName + // "Value" → s_byValue / FromValue / TryFromValue + var reservedNames = new HashSet(StringComparer.Ordinal) { "Name", "Value" }; + var result = new List(); var seenNames = new HashSet(StringComparer.Ordinal); @@ -278,6 +283,36 @@ private static EquatableArray CollectIndexedProperties( if (attr is null || !seenNames.Add(member.Name)) continue; + // OE0203: property must be a non-static, accessible instance property with a readable getter + if (member.IsStatic || + member.IsIndexer || + member.Parameters.Length > 0 || + member.GetMethod is null || + member.DeclaredAccessibility == Accessibility.Private || + member.GetMethod.DeclaredAccessibility == Accessibility.Private) + { + diagnostics.Add(new DiagnosticInfo( + DiagnosticDescriptors.IndexPropertyNotAccessible, + member.CreateLocationInfo(), + member.Name, + current.Name)); + continue; + } + + // OE0204: property name must not conflict with reserved generated member names + if (reservedNames.Contains(member.Name)) + { + var conflicting = member.Name == "Name" + ? "s_byName, FromName, TryFromName" + : "s_byValue, FromValue, TryFromValue"; + diagnostics.Add(new DiagnosticInfo( + DiagnosticDescriptors.IndexPropertyNameConflict, + member.CreateLocationInfo(), + member.Name, + conflicting)); + continue; + } + var propType = member.Type; // OE0202: warn if property type doesn't implement IEquatable diff --git a/src/LayeredCraft.OptimizedEnums.Generator/Templates/OptimizedEnum.scriban b/src/LayeredCraft.OptimizedEnums.Generator/Templates/OptimizedEnum.scriban index c9641c5..12be5fc 100644 --- a/src/LayeredCraft.OptimizedEnums.Generator/Templates/OptimizedEnum.scriban +++ b/src/LayeredCraft.OptimizedEnums.Generator/Templates/OptimizedEnum.scriban @@ -86,7 +86,7 @@ partial class {{ class_name }} } {{ generated_code_attribute }} - public static bool TryFromName(string name, out {{ fully_qualified_class_name }}? result) => + public static bool TryFromName(string name, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out {{ fully_qualified_class_name }}? result) => s_byName.TryGetValue(name, out result); {{ generated_code_attribute }} @@ -100,7 +100,7 @@ partial class {{ class_name }} } {{ generated_code_attribute }} - public static bool TryFromValue({{ value_type_fully_qualified }} value, out {{ fully_qualified_class_name }}? result) => + public static bool TryFromValue({{ value_type_fully_qualified }} value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out {{ fully_qualified_class_name }}? result) => s_byValue.TryGetValue(value, out result); {{ generated_code_attribute }} diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/GeneratorVerifyTests.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/GeneratorVerifyTests.cs index 90df811..d5d7f8e 100644 --- a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/GeneratorVerifyTests.cs +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/GeneratorVerifyTests.cs @@ -506,4 +506,100 @@ private Status(int id, string name, NonEquatable tag) : base(id, name, tag) { } ExpectedDiagnosticId = "OE0202", }, TestContext.Current.CancellationToken); + + [Fact] + public async Task Warning_OE0203_IndexProperty_PrivateProperty_IsEmitted() => + await GeneratorTestHelpers.VerifyFailure( + new VerifyTestOptions + { + SourceCode = """ + using LayeredCraft.OptimizedEnums; + + namespace MyApp.Domain; + + public abstract partial class BaseEnum : OptimizedEnum + where TSelf : BaseEnum + { + [OptimizedEnumIndex] + private string Tag { get; } + + protected BaseEnum(int id, string name, string tag) : base(id, name) + { + Tag = tag; + } + } + + public sealed partial class Status : BaseEnum + { + public static readonly Status Active = new(1, nameof(Active), "active"); + + private Status(int id, string name, string tag) : base(id, name, tag) { } + } + """, + ExpectedDiagnosticId = "OE0203", + }, + TestContext.Current.CancellationToken); + + [Fact] + public async Task Warning_OE0203_IndexProperty_StaticProperty_IsEmitted() => + await GeneratorTestHelpers.VerifyFailure( + new VerifyTestOptions + { + SourceCode = """ + using LayeredCraft.OptimizedEnums; + + namespace MyApp.Domain; + + public abstract partial class BaseEnum : OptimizedEnum + where TSelf : BaseEnum + { + [OptimizedEnumIndex] + public static string Tag { get; } = "shared"; + + protected BaseEnum(int id, string name) : base(id, name) { } + } + + public sealed partial class Status : BaseEnum + { + public static readonly Status Active = new(1, nameof(Active)); + + private Status(int id, string name) : base(id, name) { } + } + """, + ExpectedDiagnosticId = "OE0203", + }, + TestContext.Current.CancellationToken); + + [Fact] + public async Task Warning_OE0204_IndexProperty_ReservedName_IsEmitted() => + await GeneratorTestHelpers.VerifyFailure( + new VerifyTestOptions + { + SourceCode = """ + using LayeredCraft.OptimizedEnums; + + namespace MyApp.Domain; + + public abstract partial class BaseEnum : OptimizedEnum + where TSelf : BaseEnum + { + [OptimizedEnumIndex] + public string Name { get; } + + protected BaseEnum(int id, string name, string displayName) : base(id, name) + { + Name = displayName; + } + } + + public sealed partial class Status : BaseEnum + { + public static readonly Status Active = new(1, nameof(Active), "Active Status"); + + private Status(int id, string name, string displayName) : base(id, name, displayName) { } + } + """, + ExpectedDiagnosticId = "OE0204", + }, + TestContext.Current.CancellationToken); } diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.AbstractBase_WithCRTP#MyApp.Domain.OrderStatus.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.AbstractBase_WithCRTP#MyApp.Domain.OrderStatus.g.verified.cs index 8c3d17f..25d0d8c 100644 --- a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.AbstractBase_WithCRTP#MyApp.Domain.OrderStatus.g.verified.cs +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.AbstractBase_WithCRTP#MyApp.Domain.OrderStatus.g.verified.cs @@ -73,7 +73,7 @@ partial class OrderStatus } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromName(string name, out global::MyApp.Domain.OrderStatus? result) => + public static bool TryFromName(string name, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.OrderStatus? result) => s_byName.TryGetValue(name, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] @@ -87,7 +87,7 @@ public static bool TryFromName(string name, out global::MyApp.Domain.OrderStatus } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromValue(int value, out global::MyApp.Domain.OrderStatus? result) => + public static bool TryFromValue(int value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.OrderStatus? result) => s_byValue.TryGetValue(value, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.AbstractEnum_WithMembers_StillGenerates#MyApp.Domain.Status.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.AbstractEnum_WithMembers_StillGenerates#MyApp.Domain.Status.g.verified.cs index 82baaeb..32c69fb 100644 --- a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.AbstractEnum_WithMembers_StillGenerates#MyApp.Domain.Status.g.verified.cs +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.AbstractEnum_WithMembers_StillGenerates#MyApp.Domain.Status.g.verified.cs @@ -73,7 +73,7 @@ partial class Status } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromName(string name, out global::MyApp.Domain.Status? result) => + public static bool TryFromName(string name, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.Status? result) => s_byName.TryGetValue(name, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] @@ -87,7 +87,7 @@ public static bool TryFromName(string name, out global::MyApp.Domain.Status? res } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromValue(int value, out global::MyApp.Domain.Status? result) => + public static bool TryFromValue(int value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.Status? result) => s_byValue.TryGetValue(value, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.IndexedProperty_MultipleIndexes_GeneratesAllLookupMethods#MyApp.Domain.ForceAlignment.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.IndexedProperty_MultipleIndexes_GeneratesAllLookupMethods#MyApp.Domain.ForceAlignment.g.verified.cs index f48b2ac..0624f74 100644 --- a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.IndexedProperty_MultipleIndexes_GeneratesAllLookupMethods#MyApp.Domain.ForceAlignment.g.verified.cs +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.IndexedProperty_MultipleIndexes_GeneratesAllLookupMethods#MyApp.Domain.ForceAlignment.g.verified.cs @@ -87,7 +87,7 @@ partial class ForceAlignment } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromName(string name, out global::MyApp.Domain.ForceAlignment? result) => + public static bool TryFromName(string name, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.ForceAlignment? result) => s_byName.TryGetValue(name, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] @@ -101,7 +101,7 @@ public static bool TryFromName(string name, out global::MyApp.Domain.ForceAlignm } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromValue(int value, out global::MyApp.Domain.ForceAlignment? result) => + public static bool TryFromValue(int value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.ForceAlignment? result) => s_byValue.TryGetValue(value, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.IndexedProperty_StringIndex_GeneratesLookupMethods#MyApp.Domain.ForceAlignment.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.IndexedProperty_StringIndex_GeneratesLookupMethods#MyApp.Domain.ForceAlignment.g.verified.cs index b047346..795089c 100644 --- a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.IndexedProperty_StringIndex_GeneratesLookupMethods#MyApp.Domain.ForceAlignment.g.verified.cs +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.IndexedProperty_StringIndex_GeneratesLookupMethods#MyApp.Domain.ForceAlignment.g.verified.cs @@ -86,7 +86,7 @@ partial class ForceAlignment } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromName(string name, out global::MyApp.Domain.ForceAlignment? result) => + public static bool TryFromName(string name, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.ForceAlignment? result) => s_byName.TryGetValue(name, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] @@ -100,7 +100,7 @@ public static bool TryFromName(string name, out global::MyApp.Domain.ForceAlignm } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromValue(int value, out global::MyApp.Domain.ForceAlignment? result) => + public static bool TryFromValue(int value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.ForceAlignment? result) => s_byValue.TryGetValue(value, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.MultipleMembers#MyApp.Domain.DayOfWeek.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.MultipleMembers#MyApp.Domain.DayOfWeek.g.verified.cs index 161b62c..2519ae8 100644 --- a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.MultipleMembers#MyApp.Domain.DayOfWeek.g.verified.cs +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.MultipleMembers#MyApp.Domain.DayOfWeek.g.verified.cs @@ -98,7 +98,7 @@ partial class DayOfWeek } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromName(string name, out global::MyApp.Domain.DayOfWeek? result) => + public static bool TryFromName(string name, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.DayOfWeek? result) => s_byName.TryGetValue(name, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] @@ -112,7 +112,7 @@ public static bool TryFromName(string name, out global::MyApp.Domain.DayOfWeek? } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromValue(int value, out global::MyApp.Domain.DayOfWeek? result) => + public static bool TryFromValue(int value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.DayOfWeek? result) => s_byValue.TryGetValue(value, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.NestedType#MyApp.Domain.Outer.Status.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.NestedType#MyApp.Domain.Outer.Status.g.verified.cs index b518f29..dfd2aea 100644 --- a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.NestedType#MyApp.Domain.Outer.Status.g.verified.cs +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.NestedType#MyApp.Domain.Outer.Status.g.verified.cs @@ -75,7 +75,7 @@ partial class Status } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromName(string name, out global::MyApp.Domain.Outer.Status? result) => + public static bool TryFromName(string name, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.Outer.Status? result) => s_byName.TryGetValue(name, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] @@ -89,7 +89,7 @@ public static bool TryFromName(string name, out global::MyApp.Domain.Outer.Statu } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromValue(int value, out global::MyApp.Domain.Outer.Status? result) => + public static bool TryFromValue(int value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.Outer.Status? result) => s_byValue.TryGetValue(value, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SameClassName_DifferentNamespaces#MyApp.Domain1.Status.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SameClassName_DifferentNamespaces#MyApp.Domain1.Status.g.verified.cs index 196385f..ab56754 100644 --- a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SameClassName_DifferentNamespaces#MyApp.Domain1.Status.g.verified.cs +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SameClassName_DifferentNamespaces#MyApp.Domain1.Status.g.verified.cs @@ -68,7 +68,7 @@ partial class Status } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromName(string name, out global::MyApp.Domain1.Status? result) => + public static bool TryFromName(string name, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain1.Status? result) => s_byName.TryGetValue(name, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] @@ -82,7 +82,7 @@ public static bool TryFromName(string name, out global::MyApp.Domain1.Status? re } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromValue(int value, out global::MyApp.Domain1.Status? result) => + public static bool TryFromValue(int value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain1.Status? result) => s_byValue.TryGetValue(value, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SameClassName_DifferentNamespaces#MyApp.Domain2.Status.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SameClassName_DifferentNamespaces#MyApp.Domain2.Status.g.verified.cs index 94f1a72..8a30346 100644 --- a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SameClassName_DifferentNamespaces#MyApp.Domain2.Status.g.verified.cs +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SameClassName_DifferentNamespaces#MyApp.Domain2.Status.g.verified.cs @@ -68,7 +68,7 @@ partial class Status } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromName(string name, out global::MyApp.Domain2.Status? result) => + public static bool TryFromName(string name, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain2.Status? result) => s_byName.TryGetValue(name, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] @@ -82,7 +82,7 @@ public static bool TryFromName(string name, out global::MyApp.Domain2.Status? re } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromValue(int value, out global::MyApp.Domain2.Status? result) => + public static bool TryFromValue(int value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain2.Status? result) => s_byValue.TryGetValue(value, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SimpleEnum_GlobalNamespace#Priority.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SimpleEnum_GlobalNamespace#Priority.g.verified.cs index 591ee43..8d45832 100644 --- a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SimpleEnum_GlobalNamespace#Priority.g.verified.cs +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SimpleEnum_GlobalNamespace#Priority.g.verified.cs @@ -76,7 +76,7 @@ partial class Priority } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromName(string name, out global::Priority? result) => + public static bool TryFromName(string name, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Priority? result) => s_byName.TryGetValue(name, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] @@ -90,7 +90,7 @@ public static bool TryFromName(string name, out global::Priority? result) => } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromValue(int value, out global::Priority? result) => + public static bool TryFromValue(int value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Priority? result) => s_byValue.TryGetValue(value, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SimpleEnum_WithNamespace#MyApp.Domain.OrderStatus.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SimpleEnum_WithNamespace#MyApp.Domain.OrderStatus.g.verified.cs index b85c817..9d69f1d 100644 --- a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SimpleEnum_WithNamespace#MyApp.Domain.OrderStatus.g.verified.cs +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SimpleEnum_WithNamespace#MyApp.Domain.OrderStatus.g.verified.cs @@ -78,7 +78,7 @@ partial class OrderStatus } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromName(string name, out global::MyApp.Domain.OrderStatus? result) => + public static bool TryFromName(string name, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.OrderStatus? result) => s_byName.TryGetValue(name, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] @@ -92,7 +92,7 @@ public static bool TryFromName(string name, out global::MyApp.Domain.OrderStatus } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromValue(int value, out global::MyApp.Domain.OrderStatus? result) => + public static bool TryFromValue(int value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.OrderStatus? result) => s_byValue.TryGetValue(value, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.StringValueType#MyApp.Domain.Color.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.StringValueType#MyApp.Domain.Color.g.verified.cs index f48c64b..ac41740 100644 --- a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.StringValueType#MyApp.Domain.Color.g.verified.cs +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.StringValueType#MyApp.Domain.Color.g.verified.cs @@ -78,7 +78,7 @@ partial class Color } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromName(string name, out global::MyApp.Domain.Color? result) => + public static bool TryFromName(string name, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.Color? result) => s_byName.TryGetValue(name, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] @@ -92,7 +92,7 @@ public static bool TryFromName(string name, out global::MyApp.Domain.Color? resu } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromValue(string value, out global::MyApp.Domain.Color? result) => + public static bool TryFromValue(string value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.Color? result) => s_byValue.TryGetValue(value, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_NonPrivateConstructor#MyApp.Domain.OrderStatus.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_NonPrivateConstructor#MyApp.Domain.OrderStatus.g.verified.cs index a64d62c..fb76f6d 100644 --- a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_NonPrivateConstructor#MyApp.Domain.OrderStatus.g.verified.cs +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_NonPrivateConstructor#MyApp.Domain.OrderStatus.g.verified.cs @@ -68,7 +68,7 @@ partial class OrderStatus } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromName(string name, out global::MyApp.Domain.OrderStatus? result) => + public static bool TryFromName(string name, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.OrderStatus? result) => s_byName.TryGetValue(name, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] @@ -82,7 +82,7 @@ public static bool TryFromName(string name, out global::MyApp.Domain.OrderStatus } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromValue(int value, out global::MyApp.Domain.OrderStatus? result) => + public static bool TryFromValue(int value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.OrderStatus? result) => s_byValue.TryGetValue(value, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0101_NonPrivateConstructor_IsEmitted#MyApp.Domain.OrderStatus.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0101_NonPrivateConstructor_IsEmitted#MyApp.Domain.OrderStatus.g.verified.cs index a64d62c..fb76f6d 100644 --- a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0101_NonPrivateConstructor_IsEmitted#MyApp.Domain.OrderStatus.g.verified.cs +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0101_NonPrivateConstructor_IsEmitted#MyApp.Domain.OrderStatus.g.verified.cs @@ -68,7 +68,7 @@ partial class OrderStatus } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromName(string name, out global::MyApp.Domain.OrderStatus? result) => + public static bool TryFromName(string name, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.OrderStatus? result) => s_byName.TryGetValue(name, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] @@ -82,7 +82,7 @@ public static bool TryFromName(string name, out global::MyApp.Domain.OrderStatus } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromValue(int value, out global::MyApp.Domain.OrderStatus? result) => + public static bool TryFromValue(int value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.OrderStatus? result) => s_byValue.TryGetValue(value, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0102_NonReadonlyField_IsEmitted#MyApp.Domain.OrderStatus.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0102_NonReadonlyField_IsEmitted#MyApp.Domain.OrderStatus.g.verified.cs index a64d62c..fb76f6d 100644 --- a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0102_NonReadonlyField_IsEmitted#MyApp.Domain.OrderStatus.g.verified.cs +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0102_NonReadonlyField_IsEmitted#MyApp.Domain.OrderStatus.g.verified.cs @@ -68,7 +68,7 @@ partial class OrderStatus } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromName(string name, out global::MyApp.Domain.OrderStatus? result) => + public static bool TryFromName(string name, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.OrderStatus? result) => s_byName.TryGetValue(name, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] @@ -82,7 +82,7 @@ public static bool TryFromName(string name, out global::MyApp.Domain.OrderStatus } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromValue(int value, out global::MyApp.Domain.OrderStatus? result) => + public static bool TryFromValue(int value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.OrderStatus? result) => s_byValue.TryGetValue(value, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0202_IndexProperty_NonEquatableType_IsEmitted#MyApp.Domain.Status.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0202_IndexProperty_NonEquatableType_IsEmitted#MyApp.Domain.Status.g.verified.cs index 62d8cf0..f9294b9 100644 --- a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0202_IndexProperty_NonEquatableType_IsEmitted#MyApp.Domain.Status.g.verified.cs +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0202_IndexProperty_NonEquatableType_IsEmitted#MyApp.Domain.Status.g.verified.cs @@ -68,7 +68,7 @@ partial class Status } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromName(string name, out global::MyApp.Domain.Status? result) => + public static bool TryFromName(string name, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.Status? result) => s_byName.TryGetValue(name, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] @@ -82,7 +82,7 @@ public static bool TryFromName(string name, out global::MyApp.Domain.Status? res } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromValue(int value, out global::MyApp.Domain.Status? result) => + public static bool TryFromValue(int value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.Status? result) => s_byValue.TryGetValue(value, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0202_IndexProperty_NonEquatableType_IsEmitted.verified.txt b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0202_IndexProperty_NonEquatableType_IsEmitted.verified.txt index dc7fa80..b0db229 100644 --- a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0202_IndexProperty_NonEquatableType_IsEmitted.verified.txt +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0202_IndexProperty_NonEquatableType_IsEmitted.verified.txt @@ -2,13 +2,13 @@ Diagnostics: [ { Location: Program.cs: (10,24)-(10,27), - Message: The property 'Tag' of type 'MyApp.Domain.NonEquatable' is marked [OptimizedEnumIndex] but its type does not implement IEquatable; the generated dictionary will use reference equality, which is almost certainly incorrect. Implement IEquatable on the property type or remove the attribute., + Message: The property 'Tag' of type 'MyApp.Domain.NonEquatable' is marked [OptimizedEnumIndex] but its type does not implement IEquatable; the index will not be generated. Implement IEquatable on the property type or remove the attribute., Severity: Warning, WarningLevel: 1, Descriptor: { Id: OE0202, Title: OptimizedEnumIndex property type does not implement IEquatable, - MessageFormat: The property '{0}' of type '{1}' is marked [OptimizedEnumIndex] but its type does not implement IEquatable; the generated dictionary will use reference equality, which is almost certainly incorrect. Implement IEquatable on the property type or remove the attribute., + MessageFormat: The property '{0}' of type '{1}' is marked [OptimizedEnumIndex] but its type does not implement IEquatable; the index will not be generated. Implement IEquatable on the property type or remove the attribute., Category: OptimizedEnums.Usage, DefaultSeverity: Warning, IsEnabledByDefault: true diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0203_IndexProperty_PrivateProperty_IsEmitted#MyApp.Domain.Status.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0203_IndexProperty_PrivateProperty_IsEmitted#MyApp.Domain.Status.g.verified.cs new file mode 100644 index 0000000..f9294b9 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0203_IndexProperty_PrivateProperty_IsEmitted#MyApp.Domain.Status.g.verified.cs @@ -0,0 +1,93 @@ +//HintName: MyApp.Domain.Status.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +namespace MyApp.Domain; + +[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] +partial class Status +{ + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_all = + global::System.Array.AsReadOnly(new global::MyApp.Domain.Status[] + { + Active + }); + + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_names = + global::System.Array.AsReadOnly(new string[] + { + Active.Name + }); + + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_values = + global::System.Array.AsReadOnly(new int[] + { + Active.Value + }); + + private static readonly global::System.Collections.Generic.Dictionary s_byName = + new global::System.Collections.Generic.Dictionary(1, global::System.StringComparer.Ordinal) + { + [Active.Name] = Active + }; + + private static readonly global::System.Collections.Generic.Dictionary s_byValue = + new global::System.Collections.Generic.Dictionary(1) + { + [Active.Value] = Active + }; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList All => s_all; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList Names => s_names; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList Values => s_values; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public const int Count = 1; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.Status FromName(string name) + { + if (!s_byName.TryGetValue(name, out var result)) + throw new global::System.Collections.Generic.KeyNotFoundException( + $"'{name}' is not a valid name for Status"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromName(string name, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.Status? result) => + s_byName.TryGetValue(name, out result); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.Status FromValue(int value) + { + if (!s_byValue.TryGetValue(value, out var result)) + throw new global::System.Collections.Generic.KeyNotFoundException( + $"'{value}' is not a valid value for Status"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromValue(int value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.Status? result) => + s_byValue.TryGetValue(value, out result); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool ContainsName(string name) => s_byName.ContainsKey(name); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool ContainsValue(int value) => s_byValue.ContainsKey(value); +} diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0203_IndexProperty_PrivateProperty_IsEmitted.verified.txt b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0203_IndexProperty_PrivateProperty_IsEmitted.verified.txt new file mode 100644 index 0000000..9ca9560 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0203_IndexProperty_PrivateProperty_IsEmitted.verified.txt @@ -0,0 +1,18 @@ +{ + Diagnostics: [ + { + Location: Program.cs: (8,19)-(8,22), + Message: The property 'Tag' on 'BaseEnum' is marked [OptimizedEnumIndex] but cannot be used as an index because it must be a non-static, accessible instance property with a readable getter. The index will not be generated., + Severity: Warning, + WarningLevel: 1, + Descriptor: { + Id: OE0203, + Title: OptimizedEnumIndex property must be an accessible instance property with a readable getter, + MessageFormat: The property '{0}' on '{1}' is marked [OptimizedEnumIndex] but cannot be used as an index because it must be a non-static, accessible instance property with a readable getter. The index will not be generated., + Category: OptimizedEnums.Usage, + DefaultSeverity: Warning, + IsEnabledByDefault: true + } + } + ] +} \ No newline at end of file diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0203_IndexProperty_StaticProperty_IsEmitted#MyApp.Domain.Status.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0203_IndexProperty_StaticProperty_IsEmitted#MyApp.Domain.Status.g.verified.cs new file mode 100644 index 0000000..f9294b9 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0203_IndexProperty_StaticProperty_IsEmitted#MyApp.Domain.Status.g.verified.cs @@ -0,0 +1,93 @@ +//HintName: MyApp.Domain.Status.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +namespace MyApp.Domain; + +[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] +partial class Status +{ + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_all = + global::System.Array.AsReadOnly(new global::MyApp.Domain.Status[] + { + Active + }); + + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_names = + global::System.Array.AsReadOnly(new string[] + { + Active.Name + }); + + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_values = + global::System.Array.AsReadOnly(new int[] + { + Active.Value + }); + + private static readonly global::System.Collections.Generic.Dictionary s_byName = + new global::System.Collections.Generic.Dictionary(1, global::System.StringComparer.Ordinal) + { + [Active.Name] = Active + }; + + private static readonly global::System.Collections.Generic.Dictionary s_byValue = + new global::System.Collections.Generic.Dictionary(1) + { + [Active.Value] = Active + }; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList All => s_all; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList Names => s_names; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList Values => s_values; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public const int Count = 1; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.Status FromName(string name) + { + if (!s_byName.TryGetValue(name, out var result)) + throw new global::System.Collections.Generic.KeyNotFoundException( + $"'{name}' is not a valid name for Status"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromName(string name, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.Status? result) => + s_byName.TryGetValue(name, out result); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.Status FromValue(int value) + { + if (!s_byValue.TryGetValue(value, out var result)) + throw new global::System.Collections.Generic.KeyNotFoundException( + $"'{value}' is not a valid value for Status"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromValue(int value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.Status? result) => + s_byValue.TryGetValue(value, out result); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool ContainsName(string name) => s_byName.ContainsKey(name); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool ContainsValue(int value) => s_byValue.ContainsKey(value); +} diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0203_IndexProperty_StaticProperty_IsEmitted.verified.txt b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0203_IndexProperty_StaticProperty_IsEmitted.verified.txt new file mode 100644 index 0000000..8246a33 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0203_IndexProperty_StaticProperty_IsEmitted.verified.txt @@ -0,0 +1,18 @@ +{ + Diagnostics: [ + { + Location: Program.cs: (8,25)-(8,28), + Message: The property 'Tag' on 'BaseEnum' is marked [OptimizedEnumIndex] but cannot be used as an index because it must be a non-static, accessible instance property with a readable getter. The index will not be generated., + Severity: Warning, + WarningLevel: 1, + Descriptor: { + Id: OE0203, + Title: OptimizedEnumIndex property must be an accessible instance property with a readable getter, + MessageFormat: The property '{0}' on '{1}' is marked [OptimizedEnumIndex] but cannot be used as an index because it must be a non-static, accessible instance property with a readable getter. The index will not be generated., + Category: OptimizedEnums.Usage, + DefaultSeverity: Warning, + IsEnabledByDefault: true + } + } + ] +} \ No newline at end of file diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0204_IndexProperty_ReservedName_IsEmitted#MyApp.Domain.Status.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0204_IndexProperty_ReservedName_IsEmitted#MyApp.Domain.Status.g.verified.cs new file mode 100644 index 0000000..f9294b9 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0204_IndexProperty_ReservedName_IsEmitted#MyApp.Domain.Status.g.verified.cs @@ -0,0 +1,93 @@ +//HintName: MyApp.Domain.Status.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +namespace MyApp.Domain; + +[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] +partial class Status +{ + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_all = + global::System.Array.AsReadOnly(new global::MyApp.Domain.Status[] + { + Active + }); + + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_names = + global::System.Array.AsReadOnly(new string[] + { + Active.Name + }); + + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_values = + global::System.Array.AsReadOnly(new int[] + { + Active.Value + }); + + private static readonly global::System.Collections.Generic.Dictionary s_byName = + new global::System.Collections.Generic.Dictionary(1, global::System.StringComparer.Ordinal) + { + [Active.Name] = Active + }; + + private static readonly global::System.Collections.Generic.Dictionary s_byValue = + new global::System.Collections.Generic.Dictionary(1) + { + [Active.Value] = Active + }; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList All => s_all; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList Names => s_names; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList Values => s_values; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public const int Count = 1; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.Status FromName(string name) + { + if (!s_byName.TryGetValue(name, out var result)) + throw new global::System.Collections.Generic.KeyNotFoundException( + $"'{name}' is not a valid name for Status"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromName(string name, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.Status? result) => + s_byName.TryGetValue(name, out result); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.Status FromValue(int value) + { + if (!s_byValue.TryGetValue(value, out var result)) + throw new global::System.Collections.Generic.KeyNotFoundException( + $"'{value}' is not a valid value for Status"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromValue(int value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.Status? result) => + s_byValue.TryGetValue(value, out result); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool ContainsName(string name) => s_byName.ContainsKey(name); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool ContainsValue(int value) => s_byValue.ContainsKey(value); +} diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0204_IndexProperty_ReservedName_IsEmitted.verified.txt b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0204_IndexProperty_ReservedName_IsEmitted.verified.txt new file mode 100644 index 0000000..0f83cf9 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0204_IndexProperty_ReservedName_IsEmitted.verified.txt @@ -0,0 +1,18 @@ +{ + Diagnostics: [ + { + Location: Program.cs: (8,18)-(8,22), + Message: The property 'Name' is marked [OptimizedEnumIndex] but its name conflicts with a reserved generated member name ('s_byName, FromName, TryFromName'). The index will not be generated. Rename the property or remove the attribute., + Severity: Warning, + WarningLevel: 1, + Descriptor: { + Id: OE0204, + Title: OptimizedEnumIndex property name conflicts with a reserved generated member, + MessageFormat: The property '{0}' is marked [OptimizedEnumIndex] but its name conflicts with a reserved generated member name ('{1}'). The index will not be generated. Rename the property or remove the attribute., + Category: OptimizedEnums.Usage, + DefaultSeverity: Warning, + IsEnabledByDefault: true + } + } + ] +} \ No newline at end of file From c405c87a26c81f6410ceb9f41c8dd4950948571b Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Tue, 31 Mar 2026 21:49:26 -0400 Subject: [PATCH 3/4] test: update SystemTextJson snapshots for TryFromName/TryFromValue NotNullWhen Co-Authored-By: Claude Sonnet 4.6 --- ...erifyTests.ByName_GlobalNamespace#Priority.g.verified.cs | 4 ++-- ...yName_NestedType#MyApp.Domain.Outer.Status.g.verified.cs | 6 +++--- ....ByName_StringValueType#MyApp.Domain.Color.g.verified.cs | 4 ++-- ...ame_WithNamespace#MyApp.Domain.OrderStatus.g.verified.cs | 4 ++-- ...ByValue_StringValueType#MyApp.Domain.Color.g.verified.cs | 4 ++-- ...lue_WithNamespace#MyApp.Domain.OrderStatus.g.verified.cs | 4 ++-- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_GlobalNamespace#Priority.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_GlobalNamespace#Priority.g.verified.cs index 591ee43..8d45832 100644 --- a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_GlobalNamespace#Priority.g.verified.cs +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_GlobalNamespace#Priority.g.verified.cs @@ -76,7 +76,7 @@ partial class Priority } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromName(string name, out global::Priority? result) => + public static bool TryFromName(string name, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Priority? result) => s_byName.TryGetValue(name, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] @@ -90,7 +90,7 @@ public static bool TryFromName(string name, out global::Priority? result) => } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromValue(int value, out global::Priority? result) => + public static bool TryFromValue(int value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Priority? result) => s_byValue.TryGetValue(value, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] diff --git a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_NestedType#MyApp.Domain.Outer.Status.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_NestedType#MyApp.Domain.Outer.Status.g.verified.cs index b25eccc..dfd2aea 100644 --- a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_NestedType#MyApp.Domain.Outer.Status.g.verified.cs +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_NestedType#MyApp.Domain.Outer.Status.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: MyApp.Domain.Outer.Status.g.cs +//HintName: MyApp.Domain.Outer.Status.g.cs //------------------------------------------------------------------------------ // // This code was generated by a tool. @@ -75,7 +75,7 @@ partial class Status } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromName(string name, out global::MyApp.Domain.Outer.Status? result) => + public static bool TryFromName(string name, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.Outer.Status? result) => s_byName.TryGetValue(name, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] @@ -89,7 +89,7 @@ public static bool TryFromName(string name, out global::MyApp.Domain.Outer.Statu } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromValue(int value, out global::MyApp.Domain.Outer.Status? result) => + public static bool TryFromValue(int value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.Outer.Status? result) => s_byValue.TryGetValue(value, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] diff --git a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_StringValueType#MyApp.Domain.Color.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_StringValueType#MyApp.Domain.Color.g.verified.cs index f48c64b..ac41740 100644 --- a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_StringValueType#MyApp.Domain.Color.g.verified.cs +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_StringValueType#MyApp.Domain.Color.g.verified.cs @@ -78,7 +78,7 @@ partial class Color } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromName(string name, out global::MyApp.Domain.Color? result) => + public static bool TryFromName(string name, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.Color? result) => s_byName.TryGetValue(name, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] @@ -92,7 +92,7 @@ public static bool TryFromName(string name, out global::MyApp.Domain.Color? resu } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromValue(string value, out global::MyApp.Domain.Color? result) => + public static bool TryFromValue(string value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.Color? result) => s_byValue.TryGetValue(value, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] diff --git a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_WithNamespace#MyApp.Domain.OrderStatus.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_WithNamespace#MyApp.Domain.OrderStatus.g.verified.cs index b85c817..9d69f1d 100644 --- a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_WithNamespace#MyApp.Domain.OrderStatus.g.verified.cs +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_WithNamespace#MyApp.Domain.OrderStatus.g.verified.cs @@ -78,7 +78,7 @@ partial class OrderStatus } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromName(string name, out global::MyApp.Domain.OrderStatus? result) => + public static bool TryFromName(string name, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.OrderStatus? result) => s_byName.TryGetValue(name, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] @@ -92,7 +92,7 @@ public static bool TryFromName(string name, out global::MyApp.Domain.OrderStatus } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromValue(int value, out global::MyApp.Domain.OrderStatus? result) => + public static bool TryFromValue(int value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.OrderStatus? result) => s_byValue.TryGetValue(value, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] diff --git a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByValue_StringValueType#MyApp.Domain.Color.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByValue_StringValueType#MyApp.Domain.Color.g.verified.cs index f48c64b..ac41740 100644 --- a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByValue_StringValueType#MyApp.Domain.Color.g.verified.cs +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByValue_StringValueType#MyApp.Domain.Color.g.verified.cs @@ -78,7 +78,7 @@ partial class Color } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromName(string name, out global::MyApp.Domain.Color? result) => + public static bool TryFromName(string name, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.Color? result) => s_byName.TryGetValue(name, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] @@ -92,7 +92,7 @@ public static bool TryFromName(string name, out global::MyApp.Domain.Color? resu } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromValue(string value, out global::MyApp.Domain.Color? result) => + public static bool TryFromValue(string value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.Color? result) => s_byValue.TryGetValue(value, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] diff --git a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByValue_WithNamespace#MyApp.Domain.OrderStatus.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByValue_WithNamespace#MyApp.Domain.OrderStatus.g.verified.cs index b85c817..9d69f1d 100644 --- a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByValue_WithNamespace#MyApp.Domain.OrderStatus.g.verified.cs +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByValue_WithNamespace#MyApp.Domain.OrderStatus.g.verified.cs @@ -78,7 +78,7 @@ partial class OrderStatus } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromName(string name, out global::MyApp.Domain.OrderStatus? result) => + public static bool TryFromName(string name, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.OrderStatus? result) => s_byName.TryGetValue(name, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] @@ -92,7 +92,7 @@ public static bool TryFromName(string name, out global::MyApp.Domain.OrderStatus } [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static bool TryFromValue(int value, out global::MyApp.Domain.OrderStatus? result) => + public static bool TryFromValue(int value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.OrderStatus? result) => s_byValue.TryGetValue(value, out result); [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] From 18712e6b18821ed8541a30946eb156d4d5303cb6 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Tue, 31 Mar 2026 22:26:16 -0400 Subject: [PATCH 4/4] fix: address Copilot PR feedback on seenNames ordering and NotNullWhen compatibility - Move seenNames.Add() in CollectIndexedProperties to after all validation checks so an invalid nearer-base [OptimizedEnumIndex] property cannot shadow a valid one higher in the inheritance chain - Add HasNotNullWhenAttribute to EnumInfo, detected via compilation.GetTypeByMetadataName at transform time; template now conditionally emits [NotNullWhen(true)] on TryFrom* parameters so generated code compiles on netstandard2.0 targets where the attribute is absent Co-Authored-By: Claude Sonnet 4.6 --- .../Emitters/EnumEmitter.cs | 1 + .../Models/EnumInfo.cs | 13 +++++++++---- .../Providers/EnumSyntaxProvider.cs | 17 ++++++++++++++--- .../Templates/OptimizedEnum.scriban | 6 +++--- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/LayeredCraft.OptimizedEnums.Generator/Emitters/EnumEmitter.cs b/src/LayeredCraft.OptimizedEnums.Generator/Emitters/EnumEmitter.cs index a1eb296..a9180fb 100644 --- a/src/LayeredCraft.OptimizedEnums.Generator/Emitters/EnumEmitter.cs +++ b/src/LayeredCraft.OptimizedEnums.Generator/Emitters/EnumEmitter.cs @@ -27,6 +27,7 @@ internal static void Generate(SourceProductionContext context, EnumInfo info) IndexedProperties = info.IndexedProperties.ToArray(), Preamble = BuildPreamble(info), Suffix = BuildSuffix(info), + info.HasNotNullWhenAttribute, }; // Use the fully-qualified name (minus "global::") as the hint name to avoid diff --git a/src/LayeredCraft.OptimizedEnums.Generator/Models/EnumInfo.cs b/src/LayeredCraft.OptimizedEnums.Generator/Models/EnumInfo.cs index 5ea6f6b..9e88222 100644 --- a/src/LayeredCraft.OptimizedEnums.Generator/Models/EnumInfo.cs +++ b/src/LayeredCraft.OptimizedEnums.Generator/Models/EnumInfo.cs @@ -15,7 +15,8 @@ internal sealed record EnumInfo( EquatableArray ContainingTypeNames, EquatableArray Diagnostics, EquatableArray IndexedProperties, - LocationInfo? Location + LocationInfo? Location, + bool HasNotNullWhenAttribute ) { // Location is intentionally excluded from equality so that a position-only change @@ -30,9 +31,13 @@ other is not null && MemberNames == other.MemberNames && ContainingTypeNames == other.ContainingTypeNames && Diagnostics == other.Diagnostics - && IndexedProperties == other.IndexedProperties; + && IndexedProperties == other.IndexedProperties + && HasNotNullWhenAttribute == other.HasNotNullWhenAttribute; - public override int GetHashCode() => - HashCode.Combine(Namespace, ClassName, FullyQualifiedClassName, ValueTypeFullyQualified, + public override int GetHashCode() + { + var h = HashCode.Combine(Namespace, ClassName, FullyQualifiedClassName, ValueTypeFullyQualified, MemberNames, ContainingTypeNames, Diagnostics, IndexedProperties); + return HashCode.Combine(h, HasNotNullWhenAttribute); + } } diff --git a/src/LayeredCraft.OptimizedEnums.Generator/Providers/EnumSyntaxProvider.cs b/src/LayeredCraft.OptimizedEnums.Generator/Providers/EnumSyntaxProvider.cs index c22f8f3..0d48c42 100644 --- a/src/LayeredCraft.OptimizedEnums.Generator/Providers/EnumSyntaxProvider.cs +++ b/src/LayeredCraft.OptimizedEnums.Generator/Providers/EnumSyntaxProvider.cs @@ -35,6 +35,10 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) => var diagnostics = new List(); var location = classDecl.CreateLocationInfo(); var className = classSymbol.Name; + var compilation = context.SemanticModel.Compilation; + + var hasNotNullWhen = compilation.GetTypeByMetadataName( + "System.Diagnostics.CodeAnalysis.NotNullWhenAttribute") is not null; // OE0001: Must be partial var isPartial = classDecl.Modifiers.Any(static m => m.IsKind(SyntaxKind.PartialKeyword)); @@ -54,7 +58,8 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) => ContainingTypeNames: EquatableArray.Empty, Diagnostics: diagnostics.ToEquatableArray(), IndexedProperties: EquatableArray.Empty, - Location: location); + Location: location, + HasNotNullWhenAttribute: hasNotNullWhen); } // Extract TValue (second type argument of OptimizedEnum) @@ -155,7 +160,8 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) => ContainingTypeNames: GetContainingTypeDeclarations(classSymbol), Diagnostics: diagnostics.ToEquatableArray(), IndexedProperties: indexedProperties, - Location: location); + Location: location, + HasNotNullWhenAttribute: hasNotNullWhen); } private static INamedTypeSymbol? FindOptimizedEnumBase( @@ -280,7 +286,7 @@ private static EquatableArray CollectIndexedProperties( var attr = member.GetAttributes() .FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attrSymbol)); - if (attr is null || !seenNames.Add(member.Name)) + if (attr is null) continue; // OE0203: property must be a non-static, accessible instance property with a readable getter @@ -334,6 +340,11 @@ member.GetMethod is null || } } + // Only mark name as seen after it has passed all validation checks, so that an + // invalid nearer-base property does not shadow a valid one higher in the chain. + if (!seenNames.Add(member.Name)) + continue; + var isString = propType.SpecialType == SpecialType.System_String; var comparerExpr = string.Empty; diff --git a/src/LayeredCraft.OptimizedEnums.Generator/Templates/OptimizedEnum.scriban b/src/LayeredCraft.OptimizedEnums.Generator/Templates/OptimizedEnum.scriban index 12be5fc..75cd0f0 100644 --- a/src/LayeredCraft.OptimizedEnums.Generator/Templates/OptimizedEnum.scriban +++ b/src/LayeredCraft.OptimizedEnums.Generator/Templates/OptimizedEnum.scriban @@ -86,7 +86,7 @@ partial class {{ class_name }} } {{ generated_code_attribute }} - public static bool TryFromName(string name, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out {{ fully_qualified_class_name }}? result) => + public static bool TryFromName(string name, {{ if has_not_null_when_attribute }}[global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] {{ end }}out {{ fully_qualified_class_name }}? result) => s_byName.TryGetValue(name, out result); {{ generated_code_attribute }} @@ -100,7 +100,7 @@ partial class {{ class_name }} } {{ generated_code_attribute }} - public static bool TryFromValue({{ value_type_fully_qualified }} value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out {{ fully_qualified_class_name }}? result) => + public static bool TryFromValue({{ value_type_fully_qualified }} value, {{ if has_not_null_when_attribute }}[global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] {{ end }}out {{ fully_qualified_class_name }}? result) => s_byValue.TryGetValue(value, out result); {{ generated_code_attribute }} @@ -121,7 +121,7 @@ partial class {{ class_name }} } {{ generated_code_attribute }} - public static bool TryFrom{{ prop.property_name }}({{ prop.property_type_fully_qualified }} value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out {{ fully_qualified_class_name }}? result) => + public static bool TryFrom{{ prop.property_name }}({{ prop.property_type_fully_qualified }} value, {{ if has_not_null_when_attribute }}[global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] {{ end }}out {{ fully_qualified_class_name }}? result) => s_by{{ prop.property_name }}.TryGetValue(value, out result); {{~ end ~}} }{{ suffix }}