From 461cf905c3677ebcba477c602e8b8a03ffe6ad44 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Mon, 30 Mar 2026 14:28:59 -0400 Subject: [PATCH 1/7] feat: initial implementation of LayeredCraft.OptimizedEnums Introduces the full library: AOT-safe smart enum base classes, an incremental source generator (inheritance-based triggering, no attribute required), snapshot-tested generator suite, BenchmarkDotNet benchmarks, MkDocs documentation site, and GitHub Actions CI/CD workflows. Co-Authored-By: Claude Sonnet 4.6 --- .github/dependabot.yml | 12 + .github/workflows/build.yaml | 26 ++ .github/workflows/docs.yml | 78 +++++ .github/workflows/pr-build.yaml | 26 ++ .gitignore | 22 ++ .../.idea/.gitignore | 15 + .../.idea/encodings.xml | 4 + Directory.Build.props | 37 +++ Directory.Packages.props | 29 ++ LayeredCraft.OptimizedEnums.slnx | 60 ++++ README.md | 73 +++++ docs/advanced/aot-trimming.md | 50 +++ docs/advanced/diagnostics.md | 94 ++++++ docs/advanced/performance.md | 58 ++++ docs/api-reference/base-class.md | 68 +++++ docs/api-reference/generated-members.md | 102 +++++++ docs/assets/css/extra.css | 136 +++++++++ docs/assets/icon.png | Bin 0 -> 85222 bytes docs/changelog.md | 22 ++ docs/contributing.md | 54 ++++ docs/core-concepts/how-it-works.md | 105 +++++++ docs/core-concepts/inheritance-model.md | 74 +++++ docs/core-concepts/source-generators.md | 46 +++ docs/getting-started/installation.md | 57 ++++ docs/getting-started/quick-start.md | 87 ++++++ docs/index.md | 67 ++++ docs/usage/defining-enums.md | 88 ++++++ docs/usage/lookups.md | 117 +++++++ docs/usage/string-values.md | 60 ++++ global.json | 9 + icon.png | Bin 0 -> 85222 bytes layeredcraft-optimized-enums-spec.md | 289 ++++++++++++++++++ mkdocs.yml | 137 +++++++++ requirements.txt | 2 + .../AnalyzerReleases.Shipped.md | 3 + .../AnalyzerReleases.Unshipped.md | 13 + .../Diagnostics/DiagnosticDescriptors.cs | 56 ++++ .../Diagnostics/DiagnosticInfo.cs | 34 +++ .../Emitters/EnumEmitter.cs | 41 +++ .../Emitters/TemplateHelper.cs | 70 +++++ ...yeredCraft.OptimizedEnums.Generator.csproj | 89 ++++++ .../Models/EnumInfo.cs | 31 ++ .../Models/EquatableArray.cs | 58 ++++ .../Models/LocationInfo.cs | 38 +++ .../OptimizedEnumGenerator.cs | 44 +++ .../Providers/EnumSyntaxProvider.cs | 223 ++++++++++++++ .../Templates/OptimizedEnum.scriban | 101 ++++++ .../TrackingNames.cs | 9 + .../LayeredCraft.OptimizedEnums.csproj | 15 + .../OptimizedEnum.cs | 97 ++++++ tests/Directory.Build.props | 6 + .../Directory.Build.props | 4 + .../EnumLookupBenchmarks.cs | 50 +++ ...eredCraft.OptimizedEnums.Benchmarks.csproj | 22 ++ .../Program.cs | 3 + .../GeneratorTestHelpers.cs | 179 +++++++++++ .../GeneratorVerifyTests.cs | 165 ++++++++++ ...raft.OptimizedEnums.Generator.Tests.csproj | 42 +++ .../ModuleInitializer.cs | 9 + ...orVerifyTests.Error_NoMembers.verified.txt | 17 ++ ...rVerifyTests.Error_NotPartial.verified.txt | 17 ++ ...ts.MultipleMembers#DayOfWeek.g.verified.cs | 120 ++++++++ ...num_GlobalNamespace#Priority.g.verified.cs | 98 ++++++ ...um_WithNamespace#OrderStatus.g.verified.cs | 100 ++++++ ...yTests.StringValueType#Color.g.verified.cs | 100 ++++++ ...ivateConstructor#OrderStatus.g.verified.cs | 90 ++++++ .../xunit.runner.json | 3 + .../LayeredCraft.OptimizedEnums.Tests.csproj | 35 +++ .../OptimizedEnumTests.cs | 167 ++++++++++ .../OrderStatusFixture.cs | 12 + .../xunit.runner.json | 3 + 71 files changed, 4268 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/build.yaml create mode 100644 .github/workflows/docs.yml create mode 100644 .github/workflows/pr-build.yaml create mode 100644 .idea/.idea.LayeredCraft.OptimizedEnums/.idea/.gitignore create mode 100644 .idea/.idea.LayeredCraft.OptimizedEnums/.idea/encodings.xml create mode 100644 Directory.Build.props create mode 100644 Directory.Packages.props create mode 100644 LayeredCraft.OptimizedEnums.slnx create mode 100644 README.md create mode 100644 docs/advanced/aot-trimming.md create mode 100644 docs/advanced/diagnostics.md create mode 100644 docs/advanced/performance.md create mode 100644 docs/api-reference/base-class.md create mode 100644 docs/api-reference/generated-members.md create mode 100644 docs/assets/css/extra.css create mode 100644 docs/assets/icon.png create mode 100644 docs/changelog.md create mode 100644 docs/contributing.md create mode 100644 docs/core-concepts/how-it-works.md create mode 100644 docs/core-concepts/inheritance-model.md create mode 100644 docs/core-concepts/source-generators.md create mode 100644 docs/getting-started/installation.md create mode 100644 docs/getting-started/quick-start.md create mode 100644 docs/index.md create mode 100644 docs/usage/defining-enums.md create mode 100644 docs/usage/lookups.md create mode 100644 docs/usage/string-values.md create mode 100644 global.json create mode 100644 icon.png create mode 100644 layeredcraft-optimized-enums-spec.md create mode 100644 mkdocs.yml create mode 100644 requirements.txt create mode 100644 src/LayeredCraft.OptimizedEnums.Generator/AnalyzerReleases.Shipped.md create mode 100644 src/LayeredCraft.OptimizedEnums.Generator/AnalyzerReleases.Unshipped.md create mode 100644 src/LayeredCraft.OptimizedEnums.Generator/Diagnostics/DiagnosticDescriptors.cs create mode 100644 src/LayeredCraft.OptimizedEnums.Generator/Diagnostics/DiagnosticInfo.cs create mode 100644 src/LayeredCraft.OptimizedEnums.Generator/Emitters/EnumEmitter.cs create mode 100644 src/LayeredCraft.OptimizedEnums.Generator/Emitters/TemplateHelper.cs create mode 100644 src/LayeredCraft.OptimizedEnums.Generator/LayeredCraft.OptimizedEnums.Generator.csproj create mode 100644 src/LayeredCraft.OptimizedEnums.Generator/Models/EnumInfo.cs create mode 100644 src/LayeredCraft.OptimizedEnums.Generator/Models/EquatableArray.cs create mode 100644 src/LayeredCraft.OptimizedEnums.Generator/Models/LocationInfo.cs create mode 100644 src/LayeredCraft.OptimizedEnums.Generator/OptimizedEnumGenerator.cs create mode 100644 src/LayeredCraft.OptimizedEnums.Generator/Providers/EnumSyntaxProvider.cs create mode 100644 src/LayeredCraft.OptimizedEnums.Generator/Templates/OptimizedEnum.scriban create mode 100644 src/LayeredCraft.OptimizedEnums.Generator/TrackingNames.cs create mode 100644 src/LayeredCraft.OptimizedEnums/LayeredCraft.OptimizedEnums.csproj create mode 100644 src/LayeredCraft.OptimizedEnums/OptimizedEnum.cs create mode 100644 tests/Directory.Build.props create mode 100644 tests/LayeredCraft.OptimizedEnums.Benchmarks/Directory.Build.props create mode 100644 tests/LayeredCraft.OptimizedEnums.Benchmarks/EnumLookupBenchmarks.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.Benchmarks/LayeredCraft.OptimizedEnums.Benchmarks.csproj create mode 100644 tests/LayeredCraft.OptimizedEnums.Benchmarks/Program.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.Generator.Tests/GeneratorTestHelpers.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.Generator.Tests/GeneratorVerifyTests.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.Generator.Tests/LayeredCraft.OptimizedEnums.Generator.Tests.csproj create mode 100644 tests/LayeredCraft.OptimizedEnums.Generator.Tests/ModuleInitializer.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Error_NoMembers.verified.txt create mode 100644 tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Error_NotPartial.verified.txt create mode 100644 tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.MultipleMembers#DayOfWeek.g.verified.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SimpleEnum_GlobalNamespace#Priority.g.verified.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SimpleEnum_WithNamespace#OrderStatus.g.verified.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.StringValueType#Color.g.verified.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_NonPrivateConstructor#OrderStatus.g.verified.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.Generator.Tests/xunit.runner.json create mode 100644 tests/LayeredCraft.OptimizedEnums.Tests/LayeredCraft.OptimizedEnums.Tests.csproj create mode 100644 tests/LayeredCraft.OptimizedEnums.Tests/OptimizedEnumTests.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.Tests/OrderStatusFixture.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.Tests/xunit.runner.json diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..5e7d1cc --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + - package-ecosystem: "nuget" + directory: "/" + schedule: + interval: "weekly" + day: "wednesday" + open-pull-requests-limit: 25 + groups: + dotnet: + patterns: + - "*" diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..d75c2eb --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,26 @@ +name: Build +on: + workflow_dispatch: + push: + branches: + - main + - beta + - release/* + tags: + - v* + paths-ignore: + - 'docs/**' + - 'README.md' + - 'mkdocs.yml' + - 'requirements.txt' +permissions: write-all +jobs: + build: + uses: LayeredCraft/devops-templates/.github/workflows/package-build.yaml@v7.5 + with: + hasTests: true + dotnet-version: | + 8.0.x + 9.0.x + 10.0.x + secrets: inherit diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..d6f1f37 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,78 @@ +name: Deploy Documentation + +on: + push: + branches: [ main ] + paths: + - 'docs/**' + - 'mkdocs.yml' + - '.github/workflows/docs.yml' + pull_request: + branches: [ main ] + types: [ opened, synchronize, reopened, ready_for_review ] + paths: + - 'docs/**' + - 'mkdocs.yml' + - '.github/workflows/docs.yml' + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Cache dependencies + uses: actions/cache@v4 + with: + key: mkdocs-material-${{ hashFiles('requirements.txt') }} + path: ~/.cache/pip + restore-keys: | + mkdocs-material- + + - name: Install dependencies + run: | + pip install mkdocs-material + pip install mkdocs-minify-plugin + + - name: Setup Pages + id: pages + uses: actions/configure-pages@v4 + + - name: Build documentation + run: | + mkdocs build --clean + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: site + + deploy: + if: github.ref == 'refs/heads/main' + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/pr-build.yaml b/.github/workflows/pr-build.yaml new file mode 100644 index 0000000..898e9dd --- /dev/null +++ b/.github/workflows/pr-build.yaml @@ -0,0 +1,26 @@ +name: PR Build + +on: + pull_request: + types: [ opened, synchronize, reopened, ready_for_review ] + branches: [ "main" ] + +concurrency: + group: pr-${{ github.event.pull_request.number }} + cancel-in-progress: true + +permissions: write-all +jobs: + build: + if: github.event.pull_request.draft == false + uses: LayeredCraft/devops-templates/.github/workflows/pr-build.yaml@v7.5 + with: + solution: LayeredCraft.OptimizedEnums.slnx + hasTests: true + useMtpRunner: true + dotnetVersion: | + 8.0.x + 9.0.x + 10.0.x + runCdk: false + secrets: inherit diff --git a/.gitignore b/.gitignore index ce89292..0cdd3e3 100644 --- a/.gitignore +++ b/.gitignore @@ -416,3 +416,25 @@ FodyWeavers.xsd *.msix *.msm *.msp + +.idea + +.config + +# Local scripts (not for source control) +scripts/ + +# macOS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +*.received.* + +**/*.DotSettings.user +/.claude/do_not_commit/ +/nupkg/ diff --git a/.idea/.idea.LayeredCraft.OptimizedEnums/.idea/.gitignore b/.idea/.idea.LayeredCraft.OptimizedEnums/.idea/.gitignore new file mode 100644 index 0000000..71c5671 --- /dev/null +++ b/.idea/.idea.LayeredCraft.OptimizedEnums/.idea/.gitignore @@ -0,0 +1,15 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/projectSettingsUpdater.xml +/contentModel.xml +/modules.xml +/.idea.LayeredCraft.OptimizedEnums.iml +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/.idea.LayeredCraft.OptimizedEnums/.idea/encodings.xml b/.idea/.idea.LayeredCraft.OptimizedEnums/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/.idea/.idea.LayeredCraft.OptimizedEnums/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..d7df32f --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,37 @@ + + + 0.1.0-alpha + MIT + https://github.com/layeredcraft/optimized-enums + git + Nick Cipollina + https://github.com/layeredcraft/optimized-enums + icon.png + false + true + true + false + true + + + embedded + true + latest + False + true + true + + + README.md + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..3375eba --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,29 @@ + + + true + true + $(NoWarn);NU1507 + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LayeredCraft.OptimizedEnums.slnx b/LayeredCraft.OptimizedEnums.slnx new file mode 100644 index 0000000..0b1dabe --- /dev/null +++ b/LayeredCraft.OptimizedEnums.slnx @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0d76fcb --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +# LayeredCraft.OptimizedEnums + +A high-performance, AOT-safe alternative to SmartEnum patterns using source generation. + +## Features + +- **Zero reflection** — all lookup tables are source-generated at compile time +- **AOT / trimming friendly** — no runtime type discovery +- **O(1) lookups** — `FromName`, `FromValue`, `ContainsName`, `ContainsValue` +- **Compile-time validation** — errors for missing `partial`, duplicate values/names +- **No allocations per call** — all collections are statically cached + +## Usage + +```csharp +public sealed partial class OrderStatus : OptimizedEnum +{ + public static readonly OrderStatus Pending = new(1, nameof(Pending)); + public static readonly OrderStatus Paid = new(2, nameof(Paid)); + public static readonly OrderStatus Shipped = new(3, nameof(Shipped)); + + private OrderStatus(int value, string name) : base(value, name) { } +} +``` + +The source generator produces: + +```csharp +// Lookup +var status = OrderStatus.FromName("Paid"); // OrderStatus.Paid +var status = OrderStatus.FromValue(3); // OrderStatus.Shipped + +// Try-style +OrderStatus.TryFromName("Paid", out var result); +OrderStatus.TryFromValue(3, out var result); + +// Membership +OrderStatus.ContainsName("Paid"); // true +OrderStatus.ContainsValue(99); // false + +// Enumeration +IReadOnlyList all = OrderStatus.All; +IReadOnlyList names = OrderStatus.Names; +IReadOnlyList values = OrderStatus.Values; +int count = OrderStatus.Count; +``` + +## Performance + +Benchmarks run on Apple M3 Max, .NET 9.0.8, BenchmarkDotNet v0.14.0. + +| Method | Mean | Allocated | +|-------------- |---------:|----------:| +| FromName | 5.48 ns | 0 B | +| TryFromName | 4.53 ns | 0 B | +| FromValue | 2.18 ns | 0 B | +| TryFromValue | 1.21 ns | 0 B | +| ContainsName | 4.54 ns | 0 B | +| ContainsValue | 1.18 ns | 0 B | +| GetAll | 0.76 ns | 0 B | +| GetCount | ~0 ns | 0 B | + +All lookups are O(1) via statically-cached dictionaries. `Count` is a compile-time constant. + +## Installation + +``` +dotnet add package LayeredCraft.OptimizedEnums +``` + +## License + +MIT diff --git a/docs/advanced/aot-trimming.md b/docs/advanced/aot-trimming.md new file mode 100644 index 0000000..589a0ef --- /dev/null +++ b/docs/advanced/aot-trimming.md @@ -0,0 +1,50 @@ +# AOT & Trimming + +## Overview + +`LayeredCraft.OptimizedEnums` is designed from the ground up for AOT compilation and aggressive trimming. There is no reflection at runtime — all lookup tables are generated at compile time. + +## AOT Compatibility + +The library is compatible with: + +- **NativeAOT** (`PublishAot=true`) +- **ReadyToRun** (`PublishReadyToRun=true`) +- **.NET trimming** (`PublishTrimmed=true`) +- **Blazor WebAssembly** (AOT mode) +- **AWS Lambda self-contained deployments** + +## Why It Works + +Traditional SmartEnum implementations use `FieldInfo.GetValue()` or `Activator.CreateInstance()` to discover members — both of which are either blocked or stripped by the trimmer. This library generates explicit code that directly references the field values by name, which the trimmer can analyze statically. + +Generated code example: + +```csharp +// The trimmer can see exactly which fields are referenced +private static readonly global::System.Collections.Generic.Dictionary s_byName = + new(global::System.StringComparer.Ordinal) + { + ["Pending"] = global::MyApp.OrderStatus.Pending, + ["Paid"] = global::MyApp.OrderStatus.Paid, + ["Shipped"] = global::MyApp.OrderStatus.Shipped, + }; +``` + +Every reference is a direct field access. The trimmer retains exactly what is needed. + +## Lambda / Serverless + +This library was designed with Lambda cold starts in mind. Because `Count` is a compile-time constant and all lookup dictionaries are lazily initialized on first type access (not per-call), the initialization cost is paid once and is minimal. + +For AOT Lambda deployments (`PublishAot=true`), the library works without any additional trimmer annotations or `DynamicDependency` attributes. + +## Testing AOT Compatibility + +You can verify your enum works under trimming by publishing with: + +```bash +dotnet publish -c Release -r linux-x64 /p:PublishAot=true +``` + +If any reflection-based code sneaks in via a dependency, the trimmer will warn. `LayeredCraft.OptimizedEnums` itself produces no trimmer warnings. diff --git a/docs/advanced/diagnostics.md b/docs/advanced/diagnostics.md new file mode 100644 index 0000000..1fd86c3 --- /dev/null +++ b/docs/advanced/diagnostics.md @@ -0,0 +1,94 @@ +# Diagnostics + +The generator emits structured diagnostics with the prefix `OE`. Errors block code generation; warnings allow generation to proceed. + +## Errors + +### OE0001 — Must Be Partial + +**Message:** `The class '{0}' must be declared as partial for OptimizedEnum source generation` + +**Cause:** A class that inherits from `OptimizedEnum` is missing the `partial` keyword. + +**Fix:** +```csharp +// Before +public sealed class OrderStatus : OptimizedEnum { } + +// After +public sealed partial class OrderStatus : OptimizedEnum { } +``` + +### OE0004 — No Members Found + +**Message:** `The class '{0}' has no public static readonly fields of its own type` + +**Cause:** The class contains no `public static readonly` fields of its own type. The generator has nothing to build a lookup table from. + +**Fix:** Add at least one member: +```csharp +public static readonly OrderStatus Pending = new(1, nameof(Pending)); +``` + +### OE0005 — Duplicate Value + +**Message:** `The class '{0}' has duplicate value on fields '{1}' and '{2}'` + +**Cause:** Two members share the same value. Detected best-effort when constructor arguments are compile-time constants. + +**Fix:** Ensure all values are unique. + +### OE0006 — Duplicate Name + +**Message:** `The class '{0}' has a duplicate member name '{1}'` + +**Cause:** Two fields have the same name (unusual but possible via shadowing or copy-paste). + +**Fix:** Rename one of the fields. + +## Warnings + +### OE0101 — Non-Private Constructor + +**Message:** `The class '{0}' has a non-private constructor; OptimizedEnum constructors should be private to prevent direct instantiation` + +**Cause:** A constructor has non-private accessibility. + +**Recommendation:** Make the constructor `private`. Generation still proceeds. + +### OE0102 — Non-Readonly Field + +**Message:** `The field '{0}' in class '{1}' is a public static field of the enum type but is not readonly` + +**Cause:** A `public static` field of the enum type is missing `readonly`. The field is excluded from the generated lookup tables. + +**Fix:** Add `readonly` to the field declaration. + +## Suppressing Warnings + +Warnings can be suppressed via standard MSBuild mechanisms: + +```xml + + + $(NoWarn);OE0101 + +``` + +Or inline: + +```csharp +#pragma warning disable OE0101 +public OrderStatus(int value, string name) : base(value, name) { } +#pragma warning restore OE0101 +``` + +## Generator Not Running? + +If you add the package but see no generated members, check: + +1. **Is the class `partial`?** — OE0001 will be emitted and generation stops. +2. **Does it inherit from `OptimizedEnum`?** — The generator only fires when the base type is found. +3. **Are there any members?** — OE0004 fires if no qualifying fields are found. +4. **Check the build output** — run `dotnet build` and look for any `OE*` diagnostics. +5. **Inspect `obj/` generated files** — if a `.g.cs` file exists but the methods aren't available, check for a build cache issue. diff --git a/docs/advanced/performance.md b/docs/advanced/performance.md new file mode 100644 index 0000000..35ce650 --- /dev/null +++ b/docs/advanced/performance.md @@ -0,0 +1,58 @@ +# Performance + +## Benchmark Results + +All benchmarks run on Apple M3 Max, .NET 9.0.8 (Arm64 RyuJIT AdvSIMD), BenchmarkDotNet v0.14.0. + +| Method | Mean | Error | StdDev | Allocated | +|-------------- |---------:|---------:|---------:|----------:| +| FromName | 5.48 ns | 0.096 ns | 0.089 ns | 0 B | +| TryFromName | 4.53 ns | 0.064 ns | 0.057 ns | 0 B | +| FromValue | 2.18 ns | 0.018 ns | 0.017 ns | 0 B | +| TryFromValue | 1.21 ns | 0.015 ns | 0.011 ns | 0 B | +| ContainsName | 4.54 ns | 0.045 ns | 0.042 ns | 0 B | +| ContainsValue | 1.18 ns | 0.011 ns | 0.010 ns | 0 B | +| GetAll | 0.76 ns | 0.007 ns | 0.006 ns | 0 B | +| GetCount | ~0 ns | — | — | 0 B | + +## Why These Numbers + +**Zero allocations** — every operation reads from statically-initialized, cached collections. There is no per-call heap activity. + +**`FromValue` / `ContainsValue` are faster than name lookups** — `int` dictionary keys hash and compare faster than `string` keys (which use `StringComparer.Ordinal` but still require character scanning). + +**`GetAll` is a property returning a cached list reference** — essentially free after JIT inlining. + +**`GetCount` measures as ~0 ns** — it is a `const int`. The JIT replaces it with the literal value at the call site. BenchmarkDotNet cannot distinguish it from an empty method. + +## How Lookups Are Implemented + +Name lookups use a `Dictionary` with `StringComparer.Ordinal`. Value lookups use a `Dictionary` with the default equality comparer. Both dictionaries are `static readonly` fields initialized once when the type is first accessed. + +The generated code looks roughly like: + +```csharp +private static readonly Dictionary s_byName = + new(StringComparer.Ordinal) + { + ["Pending"] = Pending, + ["Paid"] = Paid, + ["Shipped"] = Shipped, + }; + +public static OrderStatus FromName(string name) => + s_byName.TryGetValue(name, out var result) + ? result + : throw new InvalidOperationException($"..."); +``` + +## Comparison to Reflection-Based Approaches + +A typical reflection-based SmartEnum implementation scans fields via `GetFields()` on first access and may allocate a new list per call if not carefully cached. This library eliminates both concerns entirely: the field list is known at compile time and all collections are allocated once at type initialization. + +## Running the Benchmarks + +```bash +cd tests/LayeredCraft.OptimizedEnums.Benchmarks +dotnet run -c Release -- --filter '*EnumLookupBenchmarks*' +``` diff --git a/docs/api-reference/base-class.md b/docs/api-reference/base-class.md new file mode 100644 index 0000000..d5fa409 --- /dev/null +++ b/docs/api-reference/base-class.md @@ -0,0 +1,68 @@ +# Base Class + +## `OptimizedEnum` + +```csharp +public abstract partial class OptimizedEnum + where TEnum : OptimizedEnum + where TValue : notnull, IComparable +``` + +The primary base class. Implements `IEquatable`, `IComparable`, and `IComparable`. + +### Constructor + +```csharp +protected OptimizedEnum(TValue value, string name) +``` + +| Parameter | Description | +|-----------|-------------| +| `value` | The underlying value for this member. Must be unique across all members of the type. | +| `name` | The display name for this member. Typically `nameof(MemberName)`. Must be unique across all members. | + +Throws `ArgumentNullException` if `name` is `null`. + +### Properties + +| Property | Type | Description | +|----------|------|-------------| +| `Value` | `TValue` | The underlying value passed to the constructor. | +| `Name` | `string` | The name passed to the constructor. | + +### Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `ToString()` | `string` | Returns `Name`. | +| `Equals(object?)` | `bool` | `sealed`. True if `obj` is `TEnum` with the same `Value` and runtime type. | +| `Equals(TEnum?)` | `bool` | True if `other` is non-null, same runtime type, and same `Value`. | +| `GetHashCode()` | `int` | `sealed`. Hash of runtime type and `Value`. | +| `CompareTo(object?)` | `int` | Delegates to `CompareTo(TEnum?)`. Throws `ArgumentException` for wrong type. | +| `CompareTo(TEnum?)` | `int` | Compares by `Value.CompareTo`. Null sorts first. | + +### Operators + +| Operator | Description | +|----------|-------------| +| `==` | Delegates to `Equals`. | +| `!=` | Delegates to `!Equals`. | + +--- + +## `OptimizedEnum` + +```csharp +public abstract class OptimizedEnum : OptimizedEnum + where TEnum : OptimizedEnum +``` + +Convenience base class that fixes `TValue = int`. All members are inherited from `OptimizedEnum`. + +### Constructor + +```csharp +protected OptimizedEnum(int value, string name) +``` + +Identical to `OptimizedEnum(value, name)`. diff --git a/docs/api-reference/generated-members.md b/docs/api-reference/generated-members.md new file mode 100644 index 0000000..878b582 --- /dev/null +++ b/docs/api-reference/generated-members.md @@ -0,0 +1,102 @@ +# Generated Members + +The source generator emits the following members into a second `partial` class declaration for each qualifying type. + +## Static Properties + +### `All` + +```csharp +public static IReadOnlyList All { get; } +``` + +All members in declaration order. Backed by a statically-initialized array. + +### `Names` + +```csharp +public static IReadOnlyList Names { get; } +``` + +All member names in declaration order. + +### `Values` + +```csharp +public static IReadOnlyList Values { get; } +``` + +All member values in declaration order. + +### `Count` + +```csharp +public const int Count = ; +``` + +The number of members. A compile-time constant — replaced with the literal value by the JIT. + +## Factory Methods + +### `FromName` + +```csharp +public static TEnum FromName(string name) +``` + +Returns the member with the given name. Uses `StringComparer.Ordinal` (case-sensitive). + +**Throws:** `InvalidOperationException` if no member has that name. + +### `TryFromName` + +```csharp +public static bool TryFromName(string name, out TEnum? result) +``` + +Returns `true` and sets `result` if found. Returns `false` and sets `result = null` if not found. Never throws. + +### `FromValue` + +```csharp +public static TEnum FromValue(TValue value) +``` + +Returns the member with the given value. + +**Throws:** `InvalidOperationException` if no member has that value. + +### `TryFromValue` + +```csharp +public static bool TryFromValue(TValue value, out TEnum? result) +``` + +Returns `true` and sets `result` if found. Returns `false` and sets `result = null` if not found. Never throws. + +## Membership Methods + +### `ContainsName` + +```csharp +public static bool ContainsName(string name) +``` + +Returns `true` if a member with that name exists. Equivalent to `TryFromName(name, out _)` but marginally faster (no out parameter overhead). + +### `ContainsValue` + +```csharp +public static bool ContainsValue(TValue value) +``` + +Returns `true` if a member with that value exists. + +## Implementation Notes + +All lookup methods are backed by pre-built `Dictionary` instances: + +- `s_byName` — `Dictionary` with `StringComparer.Ordinal` +- `s_byValue` — `Dictionary` with default comparer + +Both are `static readonly` fields initialized at type-load time. Lookups are O(1). diff --git a/docs/assets/css/extra.css b/docs/assets/css/extra.css new file mode 100644 index 0000000..5aa589c --- /dev/null +++ b/docs/assets/css/extra.css @@ -0,0 +1,136 @@ +/* Custom styles for LayeredCraft.OptimizedEnums documentation */ + +/* Improve code block styling */ +.highlight pre { + border-radius: 6px; +} + +/* Better table styling */ +.md-typeset table:not([class]) { + border: 1px solid var(--md-default-fg-color--lightest); + border-radius: 4px; +} + +.md-typeset table:not([class]) th { + background-color: var(--md-code-bg-color); + font-weight: 600; +} + +/* Improve admonition spacing */ +.md-typeset .admonition { + margin: 1.5em 0; +} + +/* Better spacing for documentation sections */ +.md-typeset h2 { + margin-top: 2em; + padding-bottom: 0.3em; + border-bottom: 1px solid var(--md-default-fg-color--lightest); +} + +.md-typeset h3 { + margin-top: 1.5em; +} + +/* Improve blockquote styling */ +.md-typeset blockquote { + border-left: 4px solid var(--md-primary-fg-color); + padding-left: 1em; +} + +/* Badge styling for version, build status, etc. */ +.md-typeset p > img[src*="shields.io"], +.md-typeset p > img[src*="badge"] { + margin: 0 0.25em; + display: inline-block; +} + +/* Improve navigation for home page */ +.md-typeset .grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1rem; + margin: 2rem 0; +} + +/* Feature boxes for home page */ +.feature-box { + padding: 1.5rem; + border: 1px solid var(--md-default-fg-color--lightest); + border-radius: 8px; + background-color: var(--md-code-bg-color); +} + +.feature-box h3 { + margin-top: 0; + color: var(--md-primary-fg-color); +} + +/* Code example improvements */ +.md-typeset code { + word-break: break-word; +} + +/* Improve keyboard key styling */ +.md-typeset kbd { + padding: 0.2em 0.4em; + font-size: 0.85em; + background-color: var(--md-code-bg-color); + border: 1px solid var(--md-default-fg-color--lightest); + border-radius: 3px; + box-shadow: 0 1px 0 var(--md-default-fg-color--lightest); +} + +/* Improve footer styling */ +.md-footer__inner { + padding-top: 1rem; +} + +/* Custom alert boxes using admonitions */ +.md-typeset .admonition.info { + border-left-color: var(--md-primary-fg-color); +} + +.md-typeset .admonition.tip { + border-left-color: var(--md-accent-fg-color); +} + +/* Improve inline code in tables */ +.md-typeset table code { + white-space: nowrap; +} + +/* Better navigation tabs on mobile */ +@media screen and (max-width: 76.1875em) { + .md-tabs { + display: flex; + overflow-x: auto; + } +} + +/* Improve search result highlighting */ +.md-search-result__article--document { + padding: 1rem; +} + +/* Improve ordered and unordered lists */ +.md-typeset ol, +.md-typeset ul { + margin: 1em 0; +} + +.md-typeset li { + margin: 0.5em 0; +} + +/* Better spacing for definition lists */ +.md-typeset dd { + margin-left: 2em; + margin-top: 0.5em; +} + +/* Improve horizontal rules */ +.md-typeset hr { + margin: 2em 0; + border-bottom: 2px solid var(--md-default-fg-color--lightest); +} diff --git a/docs/assets/icon.png b/docs/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..17a036b65af4b2457cd6f8331aa17876d3d22181 GIT binary patch literal 85222 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%0AbgRhc=K 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 literal 0 HcmV?d00001 diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 0000000..1460ea6 --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,22 @@ +# Changelog + +All notable changes to this project will be documented here. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). + +## [Unreleased] + +### Added +- `OptimizedEnum` single-parameter base class for `int`-valued enums +- Inheritance-based generation trigger — `[OptimizedEnum]` attribute no longer required +- `Microsoft.CSharp.dll` bundled in `analyzers/dotnet/cs/` for Scriban dynamic dispatch +- `GetDependencyTargetPaths` MSBuild target for IDE analyzer resolution + +### Changed +- Generator now triggers on `OptimizedEnum` inheritance rather than `[OptimizedEnum]` attribute +- `OE0001` base-type check moved before partial check — unrelated classes no longer receive false diagnostics + +### Removed +- `OptimizedEnumAttribute` — no longer needed +- `OE0002` (must be sealed) — `sealed` is now optional +- `OE0003` (must inherit) — superseded by inheritance-based triggering diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..4d4490b --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,54 @@ +# Contributing + +Contributions are welcome. Please open an issue or pull request on [GitHub](https://github.com/layeredcraft/optimized-enums). + +## Development Setup + +1. Clone the repository +2. Ensure .NET 10 SDK is installed (`global.json` pins the version) +3. Open `LayeredCraft.OptimizedEnums.slnx` in your IDE + +## Running Tests + +```bash +# Runtime tests +dotnet test --project tests/LayeredCraft.OptimizedEnums.Tests/LayeredCraft.OptimizedEnums.Tests.csproj + +# Generator snapshot tests +dotnet test --project tests/LayeredCraft.OptimizedEnums.Generator.Tests/LayeredCraft.OptimizedEnums.Generator.Tests.csproj +``` + +## Running Benchmarks + +```bash +cd tests/LayeredCraft.OptimizedEnums.Benchmarks +dotnet run -c Release -- --filter '*' +``` + +## Snapshot Tests + +The generator tests use [Verify.SourceGenerators](https://github.com/VerifyTests/Verify) for snapshot testing. If you change generated output, update the snapshots: + +```bash +dotnet test ... -- --update-snapshots +``` + +Or delete the relevant `.verified.cs` file and let the test re-create it. + +## Building the Package Locally + +```bash +bash scripts/pack-local.sh +``` + +This increments a local counter and publishes to `/usr/local/share/nuget/local/`. + +## Code Style + +- Follow existing patterns in the codebase +- Generator code uses C# latest features (`field` keyword, `extension()` blocks, pattern matching) +- Runtime code targets `netstandard2.0` — avoid APIs not available there + +## License + +By contributing, you agree that your contributions will be licensed under the MIT license. diff --git a/docs/core-concepts/how-it-works.md b/docs/core-concepts/how-it-works.md new file mode 100644 index 0000000..e6ddad3 --- /dev/null +++ b/docs/core-concepts/how-it-works.md @@ -0,0 +1,105 @@ +# How It Works + +## The Problem with SmartEnum + +The classic SmartEnum pattern provides rich enum-like types with value semantics and named members. However, traditional implementations rely on reflection to discover members at runtime: + +```csharp +// Traditional approach — reflection at runtime +var members = typeof(OrderStatus) + .GetFields(BindingFlags.Public | BindingFlags.Static) + .Where(f => f.FieldType == typeof(OrderStatus)) + .Select(f => (OrderStatus)f.GetValue(null)!) + .ToList(); +``` + +This has several drawbacks: + +- Allocates on every call unless cached manually +- Breaks under AOT compilation and aggressive trimming +- Slow on cold paths (first access per type) + +## The Source Generator Approach + +`LayeredCraft.OptimizedEnums` inverts this model. At compile time, the Roslyn source generator inspects your class declaration and emits a second `partial` class file containing: + +1. **Static lookup dictionaries** — `Dictionary` keyed by name, `Dictionary` keyed by value +2. **Static list properties** — `IReadOnlyList`, `IReadOnlyList`, `IReadOnlyList` +3. **Factory methods** — `FromName`, `FromValue`, `TryFromName`, `TryFromValue` +4. **Membership methods** — `ContainsName`, `ContainsValue` +5. **Count constant** — a compile-time `int` constant + +Because the generator reads your source directly, no reflection is ever needed at runtime. + +## Generator Pipeline + +``` +Your source file + │ + ▼ + Roslyn compiler triggers IIncrementalGenerator + │ + ▼ + Syntax predicate: ClassDeclarationSyntax { BaseList: not null } + │ + ▼ + Semantic transform: inherits OptimizedEnum? + │ + ▼ + EnumInfo model built (members, value type, namespace, diagnostics) + │ + ▼ + Scriban template rendered → partial class source + │ + ▼ + Emitted into compilation +``` + +The pipeline is incremental — Roslyn caches the `EnumInfo` model between builds. If you only change unrelated files, the generator does not re-run. + +## What Gets Generated + +Given this input: + +```csharp +public sealed partial class OrderStatus : OptimizedEnum +{ + public static readonly OrderStatus Pending = new(1, nameof(Pending)); + public static readonly OrderStatus Paid = new(2, nameof(Paid)); + public static readonly OrderStatus Shipped = new(3, nameof(Shipped)); + + private OrderStatus(int value, string name) : base(value, name) { } +} +``` + +The generator emits a file roughly equivalent to: + +```csharp +partial class OrderStatus +{ + private static readonly Dictionary s_byName = new(StringComparer.Ordinal) + { + ["Pending"] = Pending, + ["Paid"] = Paid, + ["Shipped"] = Shipped, + }; + + private static readonly Dictionary s_byValue = new() + { + [1] = Pending, + [2] = Paid, + [3] = Shipped, + }; + + public static IReadOnlyList All { get; } = [Pending, Paid, Shipped]; + public static IReadOnlyList Names { get; } = ["Pending", "Paid", "Shipped"]; + public static IReadOnlyList Values { get; } = [1, 2, 3]; + public const int Count = 3; + + public static OrderStatus FromName(string name) => ...; + public static OrderStatus FromValue(int value) => ...; + // ... +} +``` + +See [Generated Members](../api-reference/generated-members.md) for the full API surface. diff --git a/docs/core-concepts/inheritance-model.md b/docs/core-concepts/inheritance-model.md new file mode 100644 index 0000000..a8e289a --- /dev/null +++ b/docs/core-concepts/inheritance-model.md @@ -0,0 +1,74 @@ +# Inheritance Model + +## Base Classes + +The library provides two base classes: + +### `OptimizedEnum` + +The primary base class. Accepts any value type that implements `IComparable`. + +```csharp +public abstract partial class OptimizedEnum + where TEnum : OptimizedEnum + where TValue : notnull, IComparable +``` + +Use this when your enum uses a non-`int` value type (e.g., `string`, `Guid`, `long`). + +### `OptimizedEnum` + +A convenience subclass that fixes `TValue = int`. + +```csharp +public abstract class OptimizedEnum : OptimizedEnum + where TEnum : OptimizedEnum +``` + +Use this for the common case of integer-valued enums to reduce boilerplate. + +## Multi-Level Inheritance + +The generator walks the full inheritance chain when looking for `OptimizedEnum`. This means you can introduce an intermediate abstract base class: + +```csharp +// Intermediate abstract base — adds domain behavior +public abstract partial class GameStatus : OptimizedEnum + where TEnum : OptimizedEnum +{ + public bool IsTerminal { get; } + + protected GameStatus(int value, string name, bool isTerminal) + : base(value, name) + { + IsTerminal = isTerminal; + } +} + +// Concrete enum — generator fires here +public partial class DuelStatus : GameStatus +{ + public static readonly DuelStatus Active = new(1, nameof(Active), false); + public static readonly DuelStatus Won = new(2, nameof(Won), true); + public static readonly DuelStatus Lost = new(3, nameof(Lost), true); + + private DuelStatus(int value, string name, bool isTerminal) + : base(value, name, isTerminal) { } +} +``` + +The generator will produce the full lookup API for `DuelStatus` because it finds `OptimizedEnum` further up the chain. + +## `sealed` Is Optional + +Unlike some SmartEnum libraries, `sealed` is not required. You may leave a type unsealed if you need further subclassing. The generator does not enforce or require `sealed`. + +## The `partial` Requirement + +`partial` is the only hard requirement. The generator needs to emit a second partial declaration for the same class. Without `partial`, the compiler cannot merge the two and the generator reports OE0001. + +!!! warning "OE0001" + If you forget `partial`, you will see: + ``` + error OE0001: The class 'MyEnum' must be declared as partial for OptimizedEnum source generation + ``` diff --git a/docs/core-concepts/source-generators.md b/docs/core-concepts/source-generators.md new file mode 100644 index 0000000..e00ea56 --- /dev/null +++ b/docs/core-concepts/source-generators.md @@ -0,0 +1,46 @@ +# Source Generators + +## What Is an Incremental Source Generator? + +Roslyn source generators are compiler extensions that participate in the build and emit additional source files. The `IIncrementalGenerator` API (introduced in .NET 6 SDK) provides a pipeline model where intermediate results are cached and only re-executed when their inputs change. + +`LayeredCraft.OptimizedEnums` uses this API to ensure that: + +- The generator only re-runs for classes that actually changed +- No extra allocations or work occur on incremental builds +- The IDE design-time build (IntelliSense) stays fast + +## Trigger Condition + +The generator activates for any class declaration that: + +1. Has a base list (syntax-level filter — very cheap) +2. Inherits from `OptimizedEnum` anywhere in its chain (semantic check) + +No attribute is required. Inheriting the base class is sufficient signal. + +## Incremental Caching + +The generator builds an `EnumInfo` record from the semantic model. This record uses value-equality (`EquatableArray` for collections) so Roslyn can cache it between builds. If nothing in your enum class changed, the emit step is skipped entirely. + +## Template Engine + +The code generation step uses **Scriban** — a fast, lightweight template engine. The template is embedded directly in the generator DLL at build time (`PackageScribanIncludeSource=true`), so no external files are needed at runtime. + +The template lives at `src/LayeredCraft.OptimizedEnums.Generator/Templates/OptimizedEnum.scriban` and produces a single `partial class` file per enum. + +## Build Output + +Generated files appear under your project's `obj/` directory: + +``` +obj/Debug/net9.0/generated/ + LayeredCraft.OptimizedEnums.Generator/ + LayeredCraft.OptimizedEnums.Generator.OptimizedEnumGenerator/ + OrderStatus.g.cs +``` + +You can inspect these files to understand exactly what was generated. They are also visible in your IDE via "Go to definition" on any generated method. + +!!! note "Source generators and NuGet" + The generator DLL is delivered inside the NuGet package under `analyzers/dotnet/cs/`. It is loaded by the compiler host, not referenced as a normal assembly. This is why the package has `PrivateAssets="all"` on its analyzer references internally. diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md new file mode 100644 index 0000000..063dc3b --- /dev/null +++ b/docs/getting-started/installation.md @@ -0,0 +1,57 @@ +# Installation + +## Requirements + +- .NET 8.0, .NET 9.0, or .NET 10.0 (or any target that supports `netstandard2.0` for the runtime) +- C# 9.0 or later (for `partial` class support) + +## NuGet Package + +Add the package to your project: + +=== ".NET CLI" + + ```bash + dotnet add package LayeredCraft.OptimizedEnums + ``` + +=== "Package Manager" + + ```powershell + Install-Package LayeredCraft.OptimizedEnums + ``` + +=== "PackageReference" + + ```xml + + ``` + +## What Gets Installed + +The package bundles two assemblies: + +- **`LayeredCraft.OptimizedEnums.Generator.dll`** — the Roslyn source generator, loaded by the compiler +- **`LayeredCraft.OptimizedEnums.dll`** — the runtime base classes (`OptimizedEnum`, `OptimizedEnum`) + +Both are delivered automatically by the single NuGet package. No separate runtime package reference is needed. + +## Verifying the Installation + +After adding the package, define a type that inherits from `OptimizedEnum` and declare it `partial`. Build the project — the generator runs during compilation and produces the lookup members. You can inspect the generated output under `obj/` or via your IDE's "Go to definition" on any generated method. + +```csharp +using LayeredCraft.OptimizedEnums; + +public sealed partial class OrderStatus : OptimizedEnum +{ + public static readonly OrderStatus Pending = new(1, nameof(Pending)); + private OrderStatus(int value, string name) : base(value, name) { } +} + +// If installed correctly, this compiles: +var s = OrderStatus.FromName("Pending"); +``` + +!!! tip + If `FromName` is not found after adding the package, see [Troubleshooting](../advanced/diagnostics.md). diff --git a/docs/getting-started/quick-start.md b/docs/getting-started/quick-start.md new file mode 100644 index 0000000..10e5520 --- /dev/null +++ b/docs/getting-started/quick-start.md @@ -0,0 +1,87 @@ +# Quick Start + +## 1. Install the Package + +```bash +dotnet add package LayeredCraft.OptimizedEnums +``` + +## 2. Define Your Enum + +Inherit from `OptimizedEnum`, declare the class `partial`, and add your members as `public static readonly` fields of the same type. + +```csharp +using LayeredCraft.OptimizedEnums; + +public sealed partial class OrderStatus : OptimizedEnum +{ + public static readonly OrderStatus Pending = new(1, nameof(Pending)); + public static readonly OrderStatus Paid = new(2, nameof(Paid)); + public static readonly OrderStatus Shipped = new(3, nameof(Shipped)); + + private OrderStatus(int value, string name) : base(value, name) { } +} +``` + +!!! note "Why `partial`?" + The source generator augments your class with a second partial declaration containing the generated lookup members. Without `partial`, the compiler cannot merge the two declarations and the generator emits a build error (OE0001). + +## 3. Use the Generated API + +```csharp +// Lookup by name +OrderStatus status = OrderStatus.FromName("Paid"); + +// Lookup by value +OrderStatus status = OrderStatus.FromValue(2); + +// Try-style (no exception on miss) +if (OrderStatus.TryFromName("Unknown", out var result)) + Console.WriteLine(result.Name); + +// Membership checks +bool exists = OrderStatus.ContainsName("Shipped"); // true +bool exists = OrderStatus.ContainsValue(99); // false + +// Enumeration +IReadOnlyList all = OrderStatus.All; +IReadOnlyList names = OrderStatus.Names; +IReadOnlyList values = OrderStatus.Values; +int count = OrderStatus.Count; +``` + +## 4. Using `int` as the Default Value Type + +For `int`-valued enums you can use the single-parameter convenience base class: + +```csharp +public sealed partial class Priority : OptimizedEnum +{ + public static readonly Priority Low = new(1, nameof(Low)); + public static readonly Priority Medium = new(2, nameof(Medium)); + public static readonly Priority High = new(3, nameof(High)); + + private Priority(int value, string name) : base(value, name) { } +} +``` + +## 5. String Values + +Any value type implementing `IComparable` works, including `string`: + +```csharp +public sealed partial class Color : OptimizedEnum +{ + public static readonly Color Red = new("red", nameof(Red)); + public static readonly Color Green = new("green", nameof(Green)); + public static readonly Color Blue = new("blue", nameof(Blue)); + + private Color(string value, string name) : base(value, name) { } +} +``` + +## Next Steps + +- [Core Concepts — How It Works](../core-concepts/how-it-works.md) +- [Usage — Defining Enums](../usage/defining-enums.md) +- [API Reference — Generated Members](../api-reference/generated-members.md) diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..ee52dd3 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,67 @@ +# LayeredCraft.OptimizedEnums + +A high-performance, AOT-safe alternative to SmartEnum patterns using source generation. + +
+
+### Zero Reflection +All lookup tables are generated at compile time. No runtime type discovery, no `GetType()` scanning. +
+
+### O(1) Lookups +`FromName`, `FromValue`, `ContainsName`, and `ContainsValue` all resolve in constant time via statically-cached dictionaries. +
+
+### AOT & Trimming Safe +No reflection, no dynamic code. Works with `PublishAot`, `PublishTrimmed`, and NativeAOT targets out of the box. +
+
+### No Ceremony +Just inherit from `OptimizedEnum` and declare your members. The generator does the rest. +
+
+ +## Quick Example + +```csharp +public sealed partial class OrderStatus : OptimizedEnum +{ + public static readonly OrderStatus Pending = new(1, nameof(Pending)); + public static readonly OrderStatus Paid = new(2, nameof(Paid)); + public static readonly OrderStatus Shipped = new(3, nameof(Shipped)); + + private OrderStatus(int value, string name) : base(value, name) { } +} +``` + +The source generator produces: + +```csharp +var status = OrderStatus.FromName("Paid"); // OrderStatus.Paid +var status = OrderStatus.FromValue(3); // OrderStatus.Shipped +bool exists = OrderStatus.ContainsName("Paid"); // true +IReadOnlyList all = OrderStatus.All; +``` + +## Performance + +All operations are zero-allocation. Benchmarks run on Apple M3 Max, .NET 9.0.8. + +| Method | Mean | Allocated | +|-------------- |---------:|----------:| +| FromName | 5.48 ns | 0 B | +| TryFromName | 4.53 ns | 0 B | +| FromValue | 2.18 ns | 0 B | +| TryFromValue | 1.21 ns | 0 B | +| ContainsName | 4.54 ns | 0 B | +| ContainsValue | 1.18 ns | 0 B | +| GetAll | 0.76 ns | 0 B | +| GetCount | ~0 ns | 0 B | + +## Installation + +``` +dotnet add package LayeredCraft.OptimizedEnums +``` + +See [Installation](getting-started/installation.md) for full setup instructions. diff --git a/docs/usage/defining-enums.md b/docs/usage/defining-enums.md new file mode 100644 index 0000000..fb6ba40 --- /dev/null +++ b/docs/usage/defining-enums.md @@ -0,0 +1,88 @@ +# Defining Enums + +## Minimal Definition + +The minimum required shape is: + +1. A `partial` class +2. Inheriting from `OptimizedEnum` (or `OptimizedEnum`) +3. At least one `public static readonly` field of the same type + +```csharp +using LayeredCraft.OptimizedEnums; + +public partial class Direction : OptimizedEnum +{ + public static readonly Direction North = new(1, nameof(North)); + public static readonly Direction South = new(2, nameof(South)); + public static readonly Direction East = new(3, nameof(East)); + public static readonly Direction West = new(4, nameof(West)); + + private Direction(int value, string name) : base(value, name) { } +} +``` + +## Using the `int` Shorthand + +For integer-valued enums, inherit from `OptimizedEnum` to drop the second type parameter: + +```csharp +public partial class Priority : OptimizedEnum +{ + public static readonly Priority Low = new(1, nameof(Low)); + public static readonly Priority Medium = new(2, nameof(Medium)); + public static readonly Priority High = new(3, nameof(High)); + + private Priority(int value, string name) : base(value, name) { } +} +``` + +## Constructor Visibility + +The constructor should be `private` to prevent direct instantiation outside the class. The generator emits a warning (OE0101) if any non-private constructor is found, but still generates the lookup code. + +```csharp +// Recommended +private OrderStatus(int value, string name) : base(value, name) { } + +// Allowed but triggers OE0101 warning +public OrderStatus(int value, string name) : base(value, name) { } +``` + +## Member Field Requirements + +The generator only picks up fields that are: + +- `public` +- `static` +- `readonly` +- Of the same type as the enclosing class + +Non-readonly public static fields of the enum type trigger a warning (OE0102) and are excluded from generation. + +```csharp +public static readonly OrderStatus Pending = new(1, nameof(Pending)); // included +public static OrderStatus Draft = new(0, nameof(Draft)); // OE0102, excluded +private static readonly OrderStatus Hidden = new(99, nameof(Hidden)); // excluded (private) +``` + +## Duplicate Detection + +The generator performs best-effort duplicate detection at compile time: + +- **OE0005** — duplicate values (detected when constructor arguments are compile-time constants) +- **OE0006** — duplicate member names + +```csharp +// OE0005: value 1 used twice +public static readonly OrderStatus Pending = new(1, nameof(Pending)); +public static readonly OrderStatus Draft = new(1, nameof(Draft)); // error + +// OE0006: name "Pending" used twice +public static readonly OrderStatus Pending = new(1, nameof(Pending)); +public static readonly OrderStatus Pending2 = new(2, "Pending"); // error +``` + +## Namespaces + +Enums can be in any namespace, including the global namespace. The generator respects the containing namespace when emitting the partial class. diff --git a/docs/usage/lookups.md b/docs/usage/lookups.md new file mode 100644 index 0000000..31a6512 --- /dev/null +++ b/docs/usage/lookups.md @@ -0,0 +1,117 @@ +# Lookups & Queries + +## Lookup by Name + +### `FromName(string name)` + +Returns the enum member with the given name. Throws `InvalidOperationException` if not found. + +```csharp +OrderStatus status = OrderStatus.FromName("Paid"); +// Returns OrderStatus.Paid +``` + +### `TryFromName(string name, out TEnum? result)` + +Returns `true` and populates `result` if found. Returns `false` and sets `result` to `null` if not found. Never throws. + +```csharp +if (OrderStatus.TryFromName("Unknown", out var status)) +{ + Console.WriteLine(status.Name); +} +``` + +Name lookups use `StringComparer.Ordinal` (case-sensitive, culture-insensitive). + +## Lookup by Value + +### `FromValue(TValue value)` + +Returns the enum member with the given value. Throws `InvalidOperationException` if not found. + +```csharp +OrderStatus status = OrderStatus.FromValue(2); +// Returns OrderStatus.Paid (if value 2 is Paid) +``` + +### `TryFromValue(TValue value, out TEnum? result)` + +Returns `true` and populates `result` if found. Never throws. + +```csharp +if (OrderStatus.TryFromValue(99, out var status)) +{ + Console.WriteLine(status.Value); +} +``` + +## Membership Checks + +### `ContainsName(string name)` + +Returns `true` if a member with that name exists. + +```csharp +bool exists = OrderStatus.ContainsName("Shipped"); // true +bool exists = OrderStatus.ContainsName("Cancelled"); // false +``` + +### `ContainsValue(TValue value)` + +Returns `true` if a member with that value exists. + +```csharp +bool exists = OrderStatus.ContainsValue(3); // true +bool exists = OrderStatus.ContainsValue(99); // false +``` + +## Enumeration + +### `All` + +Returns all members in declaration order as `IReadOnlyList`. + +```csharp +foreach (var status in OrderStatus.All) + Console.WriteLine($"{status.Name} = {status.Value}"); +``` + +### `Names` + +Returns all member names as `IReadOnlyList`. + +```csharp +IReadOnlyList names = OrderStatus.Names; +// ["Pending", "Paid", "Shipped"] +``` + +### `Values` + +Returns all member values as `IReadOnlyList`. + +```csharp +IReadOnlyList values = OrderStatus.Values; +// [1, 2, 3] +``` + +### `Count` + +A compile-time `const int` equal to the number of members. + +```csharp +int n = OrderStatus.Count; // 3 +``` + +## Base Class Members + +Every enum member also inherits from the base class: + +| Member | Type | Description | +|--------|------|-------------| +| `Name` | `string` | The member's name | +| `Value` | `TValue` | The member's value | +| `ToString()` | `string` | Returns `Name` | +| `Equals(TEnum?)` | `bool` | Value equality | +| `CompareTo(TEnum?)` | `int` | Comparison via `TValue.CompareTo` | +| `==` / `!=` | `bool` | Operator overloads | diff --git a/docs/usage/string-values.md b/docs/usage/string-values.md new file mode 100644 index 0000000..f3f8c46 --- /dev/null +++ b/docs/usage/string-values.md @@ -0,0 +1,60 @@ +# String Values + +## Overview + +Any value type implementing `IComparable` and `notnull` works as `TValue`. `string` is a common choice when you need enum values that are meaningful in serialization or external systems. + +## Defining a String-Valued Enum + +```csharp +using LayeredCraft.OptimizedEnums; + +public sealed partial class Color : OptimizedEnum +{ + public static readonly Color Red = new("red", nameof(Red)); + public static readonly Color Green = new("green", nameof(Green)); + public static readonly Color Blue = new("blue", nameof(Blue)); + + private Color(string value, string name) : base(value, name) { } +} +``` + +## Using It + +```csharp +// Lookup by value (the string) +Color color = Color.FromValue("red"); // Color.Red + +// Lookup by name (the C# identifier) +Color color = Color.FromName("Red"); // Color.Red + +// Note: name and value are different! +color.Name // "Red" +color.Value // "red" +``` + +!!! tip "Name vs. Value" + `Name` is always the C# identifier used in the declaration (e.g., `nameof(Red)` → `"Red"`). `Value` is whatever you pass as the first constructor argument (e.g., `"red"`). They can differ — and often should for string-valued enums where you want different casing or format in serialized output. + +## Value Lookup Uses Equality + +Value lookups for `string` use the default `EqualityComparer`, which is ordinal/case-sensitive. If you need case-insensitive value lookups, consider normalizing the value at construction time. + +## Comparison + +The base class `CompareTo` delegates to `TValue.CompareTo`. For `string`, this means lexicographic comparison using the default string comparer. + +## Other Value Types + +Any comparable non-null type works: + +```csharp +// long values +public sealed partial class EventCode : OptimizedEnum +{ + public static readonly EventCode Login = new(1001L, nameof(Login)); + public static readonly EventCode Logout = new(1002L, nameof(Logout)); + + private EventCode(long value, string name) : base(value, name) { } +} +``` diff --git a/global.json b/global.json new file mode 100644 index 0000000..248c7c5 --- /dev/null +++ b/global.json @@ -0,0 +1,9 @@ +{ + "sdk": { + "rollForward": "latestMinor", + "version": "10.0.103" + }, + "test": { + "runner": "Microsoft.Testing.Platform" + } +} diff --git a/icon.png b/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..17a036b65af4b2457cd6f8331aa17876d3d22181 GIT binary patch literal 85222 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%0AbgRhc=K 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 literal 0 HcmV?d00001 diff --git a/layeredcraft-optimized-enums-spec.md b/layeredcraft-optimized-enums-spec.md new file mode 100644 index 0000000..5bfa378 --- /dev/null +++ b/layeredcraft-optimized-enums-spec.md @@ -0,0 +1,289 @@ +# LayeredCraft.OptimizedEnums – Implementation Specification + +## Overview + +Build a .NET library that provides a high-performance alternative to traditional Enumeration/SmartEnum patterns using **source generation**. + +### Key goals + +- No reflection +- AOT / trimming friendly +- Excellent developer experience +- Strong compile-time validation +- Minimal runtime overhead + +--- + +## Core Design + +### Base Type + +```csharp +#nullable enable + +namespace LayeredCraft.OptimizedEnums; + +public abstract partial class OptimizedEnum : + IEquatable, + IComparable, + IComparable + where TEnum : OptimizedEnum + where TValue : notnull, IComparable +{ + public string Name { get; } + public TValue Value { get; } + + protected OptimizedEnum(TValue value, string name) + { + ArgumentNullException.ThrowIfNull(name); + + Value = value; + Name = name; + } + + public override string ToString() => Name; + + public sealed override bool Equals(object? obj) => + obj is TEnum other && Equals(other); + + public bool Equals(TEnum? other) => + other is not null && + GetType() == other.GetType() && + EqualityComparer.Default.Equals(Value, other.Value); + + public sealed override int GetHashCode() => + HashCode.Combine(GetType(), Value); + + public int CompareTo(object? obj) + { + if (obj is null) + return 1; + + if (obj is not TEnum other) + throw new ArgumentException($"Object must be of type {typeof(TEnum).FullName}.", nameof(obj)); + + return CompareTo(other); + } + + public int CompareTo(TEnum? other) + { + if (other is null) + return 1; + + return Value.CompareTo(other.Value); + } + + public static bool operator ==(OptimizedEnum? left, OptimizedEnum? right) => + Equals(left, right); + + public static bool operator !=(OptimizedEnum? left, OptimizedEnum? right) => + !Equals(left, right); +} +``` + +--- + +## Attribute + +Create an attribute used to opt-in to source generation: + +```csharp +[AttributeUsage(AttributeTargets.Class)] +public sealed class OptimizedEnumAttribute : Attribute +{ +} +``` + +--- + +## Consumer Usage Pattern + +```csharp +[OptimizedEnum] +public sealed partial class OrderStatus : OptimizedEnum +{ + public static readonly OrderStatus Pending = new(1, nameof(Pending)); + public static readonly OrderStatus Paid = new(2, nameof(Paid)); + public static readonly OrderStatus Shipped = new(3, nameof(Shipped)); + + private OrderStatus(int value, string name) + : base(value, name) + { + } +} +``` + +--- + +## Source Generator Responsibilities + +### Discovery + +The generator must: + +- Find all classes with `[OptimizedEnum]` +- Ensure they inherit from `OptimizedEnum` +- Ensure they are: + - `partial` + - `sealed` + +### Field Detection + +Identify all: + +- `public static readonly` fields +- Of the same type as the containing class + +--- + +## Generated Code (per enum type) + +### Static Fields + +```csharp +private static readonly OrderStatus[] s_all = new[] { Pending, Paid, Shipped }; + +private static readonly string[] s_names = new[] +{ + Pending.Name, + Paid.Name, + Shipped.Name +}; + +private static readonly int[] s_values = new[] +{ + Pending.Value, + Paid.Value, + Shipped.Value +}; + +private static readonly Dictionary s_byName = + new(StringComparer.Ordinal) + { + [Pending.Name] = Pending, + [Paid.Name] = Paid, + [Shipped.Name] = Shipped + }; + +private static readonly Dictionary s_byValue = + new() + { + [Pending.Value] = Pending, + [Paid.Value] = Paid, + [Shipped.Value] = Shipped + }; +``` + +--- + +### Public API + +```csharp +public static IReadOnlyList All => s_all; +public static IReadOnlyList Names => s_names; +public static IReadOnlyList Values => s_values; + +public static int Count => s_all.Length; + +public static OrderStatus FromName(string name) +{ + if (!s_byName.TryGetValue(name, out var result)) + throw new KeyNotFoundException($"'{name}' is not a valid name for {nameof(OrderStatus)}"); + + return result; +} + +public static bool TryFromName(string name, out OrderStatus? result) => + s_byName.TryGetValue(name, out result); + +public static OrderStatus FromValue(int value) +{ + if (!s_byValue.TryGetValue(value, out var result)) + throw new KeyNotFoundException($"'{value}' is not a valid value for {nameof(OrderStatus)}"); + + return result; +} + +public static bool TryFromValue(int value, out OrderStatus? result) => + s_byValue.TryGetValue(value, out result); + +public static bool ContainsName(string name) => s_byName.ContainsKey(name); +public static bool ContainsValue(int value) => s_byValue.ContainsKey(value); +``` + +--- + +## Diagnostics (Compile-Time) + +The generator must emit diagnostics for: + +### Errors + +- Type is not `partial` +- Type is not `sealed` +- Type does not inherit from `OptimizedEnum<,>` +- No valid enum fields found +- Duplicate values +- Duplicate names + +### Warnings (optional) + +- Non-private constructors +- Non-readonly fields + +--- + +## Design Rules + +- No reflection anywhere +- No LINQ in generated code +- All lookups must be O(1) +- All collections must be cached (no allocations per call) +- Use `StringComparer.Ordinal` for name lookups +- Generated members must feel native to the type + +--- + +## Explicit Non-Goals (v1) + +Do NOT include: + +- Implicit conversion operators +- Reflection fallback +- Localization support +- EF Core integration +- ASP.NET integration +- JSON converters (separate package) + +--- + +## Package Structure + +``` +src/ + LayeredCraft.OptimizedEnums/ + LayeredCraft.OptimizedEnums.Generator/ + +tests/ + LayeredCraft.OptimizedEnums.Tests/ + LayeredCraft.OptimizedEnums.Generator.Tests/ + LayeredCraft.OptimizedEnums.Benchmarks/ +``` + +--- + +## Future Extensions (Separate Packages) + +- LayeredCraft.OptimizedEnums.Json +- LayeredCraft.OptimizedEnums.EFCore +- LayeredCraft.OptimizedEnums.AspNetCore + +--- + +## Key Differentiators + +- Source-generated lookup tables +- Zero reflection +- AOT-safe +- Better performance than SmartEnum +- Compile-time validation instead of runtime errors diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..4a232ae --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,137 @@ +site_name: LayeredCraft.OptimizedEnums +site_description: High-performance, AOT-safe alternative to SmartEnum patterns using source generation +site_url: https://layeredcraft.github.io/optimized-enums/ +site_author: Nick Cipollina + +# Repository +repo_name: layeredcraft/optimized-enums +repo_url: https://github.com/layeredcraft/optimized-enums +edit_uri: edit/main/docs/ + +# Copyright +copyright: Copyright © 2025 LayeredCraft + +# Configuration +theme: + name: material + language: en + logo: assets/icon.png + favicon: assets/icon.png + + features: + - content.code.copy + - content.code.select + - navigation.expand + - navigation.footer + - navigation.instant + - navigation.sections + - navigation.tabs + - navigation.tabs.sticky + - navigation.top + - search.highlight + - search.share + - search.suggest + - toc.follow + + # Dark/Light mode toggle + palette: + # Light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: deep purple + accent: deep purple + toggle: + icon: material/brightness-7 + name: Switch to dark mode + + # Dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: deep purple + accent: deep purple + toggle: + icon: material/brightness-4 + name: Switch to light mode + + font: + text: Roboto + code: Roboto Mono + +# Plugins +plugins: + - search + - minify: + minify_html: true + +# Extensions +markdown_extensions: + # Python Markdown + - abbr + - admonition + - attr_list + - def_list + - footnotes + - md_in_html + - toc: + permalink: true + + # Python Markdown Extensions + - pymdownx.betterem: + smart_enable: all + - pymdownx.caret + - pymdownx.details + - pymdownx.emoji: + emoji_generator: !!python/name:material.extensions.emoji.to_svg + emoji_index: !!python/name:material.extensions.emoji.twemoji + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.keys + - pymdownx.mark + - pymdownx.smartsymbols + - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true + - pymdownx.tasklist: + custom_checkbox: true + - pymdownx.tilde + +# Navigation +nav: + - Home: index.md + - Getting Started: + - Installation: getting-started/installation.md + - Quick Start: getting-started/quick-start.md + - Core Concepts: + - How It Works: core-concepts/how-it-works.md + - Source Generators: core-concepts/source-generators.md + - Inheritance Model: core-concepts/inheritance-model.md + - Usage: + - Defining Enums: usage/defining-enums.md + - Lookups & Queries: usage/lookups.md + - String Values: usage/string-values.md + - Advanced: + - Performance: advanced/performance.md + - Diagnostics: advanced/diagnostics.md + - AOT & Trimming: advanced/aot-trimming.md + - API Reference: + - Base Class: api-reference/base-class.md + - Generated Members: api-reference/generated-members.md + - Contributing: contributing.md + - Changelog: changelog.md + +# Social links +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/layeredcraft/optimized-enums + name: GitHub Repository + - icon: fontawesome/solid/download + link: https://www.nuget.org/packages/LayeredCraft.OptimizedEnums/ + name: NuGet Package + +# Custom CSS +extra_css: + - assets/css/extra.css diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3ca83ae --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +mkdocs-material>=9.5.0 +mkdocs-minify-plugin>=0.8.0 diff --git a/src/LayeredCraft.OptimizedEnums.Generator/AnalyzerReleases.Shipped.md b/src/LayeredCraft.OptimizedEnums.Generator/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000..d027c51 --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.Generator/AnalyzerReleases.Shipped.md @@ -0,0 +1,3 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + diff --git a/src/LayeredCraft.OptimizedEnums.Generator/AnalyzerReleases.Unshipped.md b/src/LayeredCraft.OptimizedEnums.Generator/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000..fa97a99 --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.Generator/AnalyzerReleases.Unshipped.md @@ -0,0 +1,13 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +### New Rules + + Rule ID | Category | Severity | Notes +---------|------------------------|----------|----------------------- + OE0001 | OptimizedEnums.Usage | Error | DiagnosticDescriptors + OE0004 | OptimizedEnums.Usage | Error | DiagnosticDescriptors + OE0005 | OptimizedEnums.Usage | Error | DiagnosticDescriptors + OE0006 | OptimizedEnums.Usage | Error | DiagnosticDescriptors + OE0101 | OptimizedEnums.Usage | Warning | DiagnosticDescriptors + OE0102 | OptimizedEnums.Usage | Warning | DiagnosticDescriptors diff --git a/src/LayeredCraft.OptimizedEnums.Generator/Diagnostics/DiagnosticDescriptors.cs b/src/LayeredCraft.OptimizedEnums.Generator/Diagnostics/DiagnosticDescriptors.cs new file mode 100644 index 0000000..70bd815 --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.Generator/Diagnostics/DiagnosticDescriptors.cs @@ -0,0 +1,56 @@ +using Microsoft.CodeAnalysis; + +namespace LayeredCraft.OptimizedEnums.Generator.Diagnostics; + +internal static class DiagnosticDescriptors +{ + private const string UsageCategory = "OptimizedEnums.Usage"; + + internal static readonly DiagnosticDescriptor MustBePartial = new( + "OE0001", + "OptimizedEnum class must be partial", + "The class '{0}' must be declared as partial for OptimizedEnum source generation", + UsageCategory, + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + internal static readonly DiagnosticDescriptor NoMembersFound = new( + "OE0004", + "No enum members found", + "The class '{0}' has no public static readonly fields of its own type", + UsageCategory, + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + internal static readonly DiagnosticDescriptor DuplicateValue = new( + "OE0005", + "Duplicate enum value", + "The class '{0}' has duplicate value on fields '{1}' and '{2}'", + UsageCategory, + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + internal static readonly DiagnosticDescriptor DuplicateName = new( + "OE0006", + "Duplicate enum member name", + "The class '{0}' has a duplicate member name '{1}'", + UsageCategory, + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + internal static readonly DiagnosticDescriptor NonPrivateConstructor = new( + "OE0101", + "OptimizedEnum constructor should be private", + "The class '{0}' has a non-private constructor; OptimizedEnum constructors should be private to prevent direct instantiation", + UsageCategory, + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + internal static readonly DiagnosticDescriptor NonReadonlyField = new( + "OE0102", + "OptimizedEnum static field should be readonly", + "The field '{0}' in class '{1}' is a public static field of the enum type but is not readonly", + UsageCategory, + DiagnosticSeverity.Warning, + isEnabledByDefault: true); +} diff --git a/src/LayeredCraft.OptimizedEnums.Generator/Diagnostics/DiagnosticInfo.cs b/src/LayeredCraft.OptimizedEnums.Generator/Diagnostics/DiagnosticInfo.cs new file mode 100644 index 0000000..f532576 --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.Generator/Diagnostics/DiagnosticInfo.cs @@ -0,0 +1,34 @@ +using LayeredCraft.OptimizedEnums.Generator.Models; +using Microsoft.CodeAnalysis; + +namespace LayeredCraft.OptimizedEnums.Generator.Diagnostics; + +internal sealed record DiagnosticInfo( + DiagnosticDescriptor DiagnosticDescriptor, + LocationInfo? LocationInfo = null, + params object?[] MessageArgs +) +{ + public bool Equals(DiagnosticInfo? other) => + other is not null + && Equals(DiagnosticDescriptor.Id, other.DiagnosticDescriptor.Id) + && Equals(LocationInfo, other.LocationInfo); + + public override int GetHashCode() => + HashCode.Combine(DiagnosticDescriptor.Id, LocationInfo); +} + +internal static class DiagnosticInfoExtensions +{ + extension(DiagnosticInfo diagnosticInfo) + { + internal Diagnostic ToDiagnostic() => + Diagnostic.Create( + diagnosticInfo.DiagnosticDescriptor, + diagnosticInfo.LocationInfo?.ToLocation(), + diagnosticInfo.MessageArgs); + + internal void ReportDiagnostic(SourceProductionContext context) => + context.ReportDiagnostic(diagnosticInfo.ToDiagnostic()); + } +} diff --git a/src/LayeredCraft.OptimizedEnums.Generator/Emitters/EnumEmitter.cs b/src/LayeredCraft.OptimizedEnums.Generator/Emitters/EnumEmitter.cs new file mode 100644 index 0000000..2b7488f --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.Generator/Emitters/EnumEmitter.cs @@ -0,0 +1,41 @@ +using System.Reflection; +using LayeredCraft.OptimizedEnums.Generator.Diagnostics; +using LayeredCraft.OptimizedEnums.Generator.Models; +using Microsoft.CodeAnalysis; + +namespace LayeredCraft.OptimizedEnums.Generator.Emitters; + +internal static class EnumEmitter +{ + private static string GeneratedCodeAttribute + { + get + { + if (field is null) + { + var assembly = Assembly.GetExecutingAssembly(); + var name = assembly.GetName().Name; + var version = assembly.GetName().Version!.ToString(); + field = $"""[global::System.CodeDom.Compiler.GeneratedCode("{name}", "{version}")]"""; + } + + return field; + } + } + + internal static void Generate(SourceProductionContext context, EnumInfo info) + { + var model = new + { + GeneratedCodeAttribute, + info.Namespace, + info.ClassName, + info.FullyQualifiedClassName, + info.ValueTypeFullyQualified, + MemberNames = info.MemberNames.ToArray(), + }; + + var outputCode = TemplateHelper.Render("Templates.OptimizedEnum.scriban", model); + context.AddSource($"{info.ClassName}.g.cs", outputCode); + } +} diff --git a/src/LayeredCraft.OptimizedEnums.Generator/Emitters/TemplateHelper.cs b/src/LayeredCraft.OptimizedEnums.Generator/Emitters/TemplateHelper.cs new file mode 100644 index 0000000..1aca9c2 --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.Generator/Emitters/TemplateHelper.cs @@ -0,0 +1,70 @@ +using System.Collections.Concurrent; +using System.Reflection; +using Scriban; + +namespace LayeredCraft.OptimizedEnums.Generator.Emitters; + +/// +/// Helper class for loading, caching, and rendering Scriban templates from embedded resources. +/// +internal static class TemplateHelper +{ + private static readonly ConcurrentDictionary Cache = new(); + + /// + /// Renders a Scriban template with the provided model. Templates are cached after first load. + /// + /// The type of the model to render. + /// Relative path to the template (e.g., "Templates.OptimizedEnum.scriban") + /// The model object to render with the template. + /// The rendered template as a string. + internal static string Render(string resourceName, TModel model) + { + var template = Cache.GetOrAdd(resourceName, LoadTemplate); + return template.Render(model); + } + + private static Template LoadTemplate(string relativePath) + { + var assembly = Assembly.GetExecutingAssembly(); + var baseName = assembly.GetName().Name; + + var templateName = relativePath + .TrimStart('.') + .Replace(Path.DirectorySeparatorChar, '.') + .Replace(Path.AltDirectorySeparatorChar, '.'); + + var manifestTemplateName = assembly + .GetManifestResourceNames() + .FirstOrDefault(x => x.EndsWith(templateName, StringComparison.InvariantCulture)); + + if (string.IsNullOrEmpty(manifestTemplateName)) + { + var availableResources = string.Join(", ", assembly.GetManifestResourceNames()); + throw new InvalidOperationException( + $"Did not find required resource ending in '{templateName}' in assembly '{baseName}'. " + + $"Available resources: {availableResources}"); + } + + using var stream = assembly.GetManifestResourceStream(manifestTemplateName); + if (stream == null) + throw new FileNotFoundException( + $"Template '{relativePath}' not found in embedded resources. " + + $"Manifest resource name: '{manifestTemplateName}'"); + + using var reader = new StreamReader(stream); + var templateContent = reader.ReadToEnd(); + + var template = Template.Parse(templateContent, relativePath); + if (!template.HasErrors) + return template; + + var errors = string.Join( + "\n", + template.Messages.Select(m => + $"{relativePath}({m.Span.Start.Line},{m.Span.Start.Column}): {m.Message}")); + + throw new InvalidOperationException( + $"Failed to parse template '{relativePath}':\n{errors}"); + } +} diff --git a/src/LayeredCraft.OptimizedEnums.Generator/LayeredCraft.OptimizedEnums.Generator.csproj b/src/LayeredCraft.OptimizedEnums.Generator/LayeredCraft.OptimizedEnums.Generator.csproj new file mode 100644 index 0000000..b05ace8 --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.Generator/LayeredCraft.OptimizedEnums.Generator.csproj @@ -0,0 +1,89 @@ + + + netstandard2.0 + enable + latest + false + true + true + LayeredCraft.OptimizedEnums + LayeredCraft.OptimizedEnums.Generator + LayeredCraft.OptimizedEnums.Generator + LayeredCraft.OptimizedEnums + High-performance alternative to SmartEnum using source generation. Provides zero-reflection, AOT-safe enum types with compile-time validation and O(1) lookup tables. + enum;source-generator;smart-enum;dotnet;csharp;roslyn;aot + true + true + true + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + $(GetTargetPathDependsOn);GetDependencyTargetPaths + + + + + + + + + + + + + + + + diff --git a/src/LayeredCraft.OptimizedEnums.Generator/Models/EnumInfo.cs b/src/LayeredCraft.OptimizedEnums.Generator/Models/EnumInfo.cs new file mode 100644 index 0000000..68058f7 --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.Generator/Models/EnumInfo.cs @@ -0,0 +1,31 @@ +using LayeredCraft.OptimizedEnums.Generator.Diagnostics; + +namespace LayeredCraft.OptimizedEnums.Generator.Models; + +/// +/// Immutable model representing a validated [OptimizedEnum] class discovered during +/// the incremental generator transform step. +/// +internal sealed record EnumInfo( + string? Namespace, + string ClassName, + string FullyQualifiedClassName, + string ValueTypeFullyQualified, + EquatableArray MemberNames, + EquatableArray Diagnostics, + LocationInfo? Location +) +{ + public bool Equals(EnumInfo? other) => + other is not null + && Namespace == other.Namespace + && ClassName == other.ClassName + && FullyQualifiedClassName == other.FullyQualifiedClassName + && ValueTypeFullyQualified == other.ValueTypeFullyQualified + && MemberNames == other.MemberNames + && Diagnostics == other.Diagnostics; + + public override int GetHashCode() => + HashCode.Combine(Namespace, ClassName, FullyQualifiedClassName, ValueTypeFullyQualified, + MemberNames, Diagnostics); +} diff --git a/src/LayeredCraft.OptimizedEnums.Generator/Models/EquatableArray.cs b/src/LayeredCraft.OptimizedEnums.Generator/Models/EquatableArray.cs new file mode 100644 index 0000000..b9b2564 --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.Generator/Models/EquatableArray.cs @@ -0,0 +1,58 @@ +using System.Collections; +using System.Collections.Immutable; + +namespace LayeredCraft.OptimizedEnums.Generator.Models; + +/// +/// An immutable array wrapper that implements structural equality for use in +/// incremental generator models where caching depends on value equality. +/// +internal readonly struct EquatableArray : IEquatable>, IEnumerable + where T : IEquatable +{ + private readonly ImmutableArray _array; + + public EquatableArray(ImmutableArray array) => _array = array; + + public static readonly EquatableArray Empty = new(ImmutableArray.Empty); + + public int Length => _array.Length; + + public T this[int index] => _array[index]; + + public bool Equals(EquatableArray other) => _array.SequenceEqual(other._array); + + public override bool Equals(object? obj) => + obj is EquatableArray other && Equals(other); + + public override int GetHashCode() + { + var hash = new HashCode(); + foreach (var item in _array) + hash.Add(item); + return hash.ToHashCode(); + } + + public T[] ToArray() => _array.IsDefaultOrEmpty ? Array.Empty() : _array.ToArray(); + + public ImmutableArray.Enumerator GetEnumerator() => _array.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => + ((IEnumerable)_array).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => + ((IEnumerable)_array).GetEnumerator(); + + public static bool operator ==(EquatableArray left, EquatableArray right) => + left.Equals(right); + + public static bool operator !=(EquatableArray left, EquatableArray right) => + !left.Equals(right); +} + +internal static class EquatableArrayExtensions +{ + internal static EquatableArray ToEquatableArray(this IEnumerable source) + where T : IEquatable => + new(source.ToImmutableArray()); +} diff --git a/src/LayeredCraft.OptimizedEnums.Generator/Models/LocationInfo.cs b/src/LayeredCraft.OptimizedEnums.Generator/Models/LocationInfo.cs new file mode 100644 index 0000000..bed0dcf --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.Generator/Models/LocationInfo.cs @@ -0,0 +1,38 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace LayeredCraft.OptimizedEnums.Generator.Models; + +internal sealed record LocationInfo(string FilePath, TextSpan TextSpan, LinePositionSpan LineSpan); + +internal static class LocationInfoExtensions +{ + extension(LocationInfo locationInfo) + { + internal Location ToLocation() => + Location.Create(locationInfo.FilePath, locationInfo.TextSpan, locationInfo.LineSpan); + } + + extension(Location location) + { + internal LocationInfo? CreateLocationInfo() => + location.SourceTree is null + ? null + : new LocationInfo( + location.SourceTree.FilePath, + location.SourceSpan, + location.GetLineSpan().Span); + } + + extension(ISymbol symbol) + { + internal LocationInfo? CreateLocationInfo() => + symbol.Locations.FirstOrDefault()?.CreateLocationInfo(); + } + + extension(SyntaxNode syntaxNode) + { + internal LocationInfo? CreateLocationInfo() => + syntaxNode.GetLocation().CreateLocationInfo(); + } +} diff --git a/src/LayeredCraft.OptimizedEnums.Generator/OptimizedEnumGenerator.cs b/src/LayeredCraft.OptimizedEnums.Generator/OptimizedEnumGenerator.cs new file mode 100644 index 0000000..435dc3a --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.Generator/OptimizedEnumGenerator.cs @@ -0,0 +1,44 @@ +using LayeredCraft.OptimizedEnums.Generator.Diagnostics; +using LayeredCraft.OptimizedEnums.Generator.Emitters; +using LayeredCraft.OptimizedEnums.Generator.Providers; +using Microsoft.CodeAnalysis; + +namespace LayeredCraft.OptimizedEnums.Generator; + +/// Source code generator for LayeredCraft.OptimizedEnums. +[Generator] +public sealed class OptimizedEnumGenerator : IIncrementalGenerator +{ + /// + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var enumInfos = context.SyntaxProvider + .CreateSyntaxProvider( + EnumSyntaxProvider.Predicate, + EnumSyntaxProvider.Transform) + .WithTrackingName(TrackingNames.EnumSyntaxProvider_Extract) + .Where(static x => x is not null) + .Select(static (x, _) => x!) + .WithTrackingName(TrackingNames.EnumSyntaxProvider_FilterNotNull); + + context.RegisterSourceOutput(enumInfos, static (ctx, info) => + { + // Report all diagnostics first + foreach (var diagnostic in info.Diagnostics) + diagnostic.ReportDiagnostic(ctx); + + // Skip code generation if any error-level diagnostic was emitted + if (info.Diagnostics.Any(diagnostic => + diagnostic.DiagnosticDescriptor.DefaultSeverity == DiagnosticSeverity.Error)) + { + return; + } + + // Belt-and-suspenders: OE0004 should have fired if members are empty + if (info.MemberNames.Length == 0) + return; + + EnumEmitter.Generate(ctx, info); + }); + } +} diff --git a/src/LayeredCraft.OptimizedEnums.Generator/Providers/EnumSyntaxProvider.cs b/src/LayeredCraft.OptimizedEnums.Generator/Providers/EnumSyntaxProvider.cs new file mode 100644 index 0000000..8d63091 --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.Generator/Providers/EnumSyntaxProvider.cs @@ -0,0 +1,223 @@ +using System.Collections.Immutable; +using LayeredCraft.OptimizedEnums.Generator.Diagnostics; +using LayeredCraft.OptimizedEnums.Generator.Models; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace LayeredCraft.OptimizedEnums.Generator.Providers; + +internal static class EnumSyntaxProvider +{ + private const string OptimizedEnumBaseMetadataName = "LayeredCraft.OptimizedEnums.OptimizedEnum`2"; + + internal static bool Predicate(SyntaxNode node, CancellationToken _) => + node is ClassDeclarationSyntax { BaseList: not null }; + + internal static EnumInfo? Transform( + GeneratorSyntaxContext context, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (context.Node is not ClassDeclarationSyntax classDecl) + return null; + + if (context.SemanticModel.GetDeclaredSymbol(classDecl, cancellationToken) + is not { } classSymbol) + return null; + + // Only generate for types that actually inherit from OptimizedEnum + var baseType = FindOptimizedEnumBase(classSymbol, context.SemanticModel.Compilation); + if (baseType is null) + return null; + + var diagnostics = new List(); + var location = classDecl.CreateLocationInfo(); + var className = classSymbol.Name; + + // OE0001: Must be partial + var isPartial = classDecl.Modifiers.Any(static m => m.IsKind(SyntaxKind.PartialKeyword)); + if (!isPartial) + { + diagnostics.Add(new DiagnosticInfo( + DiagnosticDescriptors.MustBePartial, + location, + className)); + + return new EnumInfo( + Namespace: null, + ClassName: className, + FullyQualifiedClassName: classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + ValueTypeFullyQualified: string.Empty, + MemberNames: EquatableArray.Empty, + Diagnostics: diagnostics.ToEquatableArray(), + Location: location); + } + + // Extract TValue (second type argument of OptimizedEnum) + var valueTypeSymbol = baseType.TypeArguments[1]; + var valueTypeFullyQualified = valueTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + + cancellationToken.ThrowIfCancellationRequested(); + + // OE0101: Warn about non-private constructors + foreach (var ctor in classSymbol.Constructors) + { + if (ctor.IsImplicitlyDeclared) + continue; + if (ctor.DeclaredAccessibility != Accessibility.Private) + { + diagnostics.Add(new DiagnosticInfo( + DiagnosticDescriptors.NonPrivateConstructor, + ctor.CreateLocationInfo(), + className)); + } + } + + // Scan members for public static fields of the enum type + var validMembers = new List(); + var seenNames = new HashSet(StringComparer.Ordinal); + + foreach (var member in classSymbol.GetMembers()) + { + if (member is not IFieldSymbol field) + continue; + if (field.DeclaredAccessibility != Accessibility.Public) + continue; + if (!field.IsStatic) + continue; + + if (!SymbolEqualityComparer.Default.Equals(field.Type, classSymbol)) + continue; + + // OE0102: non-readonly public static field of enum type + if (!field.IsReadOnly) + { + diagnostics.Add(new DiagnosticInfo( + DiagnosticDescriptors.NonReadonlyField, + field.CreateLocationInfo(), + field.Name, + className)); + continue; + } + + // OE0006: duplicate member name + if (!seenNames.Add(field.Name)) + { + diagnostics.Add(new DiagnosticInfo( + DiagnosticDescriptors.DuplicateName, + field.CreateLocationInfo(), + className, + field.Name)); + continue; + } + + validMembers.Add(field.Name); + } + + // OE0005: duplicate values (best-effort, only for compile-time constants) + DetectDuplicateValues(classSymbol, classDecl, context.SemanticModel, validMembers, diagnostics, className, cancellationToken); + + // OE0004: no valid members + if (validMembers.Count == 0) + { + diagnostics.Add(new DiagnosticInfo( + DiagnosticDescriptors.NoMembersFound, + location, + className)); + } + + return new EnumInfo( + Namespace: GetNamespace(classSymbol), + ClassName: className, + FullyQualifiedClassName: classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + ValueTypeFullyQualified: valueTypeFullyQualified, + MemberNames: validMembers.ToEquatableArray(), + Diagnostics: diagnostics.ToEquatableArray(), + Location: location); + } + + private static INamedTypeSymbol? FindOptimizedEnumBase( + INamedTypeSymbol classSymbol, + Compilation compilation) + { + var optimizedEnumBase = compilation.GetTypeByMetadataName(OptimizedEnumBaseMetadataName); + if (optimizedEnumBase is null) + return null; + + var current = classSymbol.BaseType; + while (current is not null) + { + if (SymbolEqualityComparer.Default.Equals(current.OriginalDefinition, optimizedEnumBase)) + return current; + current = current.BaseType; + } + + return null; + } + + private static void DetectDuplicateValues( + INamedTypeSymbol classSymbol, + ClassDeclarationSyntax classDecl, + SemanticModel semanticModel, + List memberNames, + List diagnostics, + string className, + CancellationToken cancellationToken) + { + // Build a mapping of field name -> constant value (best effort, skips non-literals) + var valueToField = new Dictionary(StringComparer.Ordinal); + + foreach (var member in classDecl.Members) + { + if (member is not FieldDeclarationSyntax fieldDecl) + continue; + + foreach (var variable in fieldDecl.Declaration.Variables) + { + cancellationToken.ThrowIfCancellationRequested(); + + var fieldName = variable.Identifier.Text; + if (!memberNames.Contains(fieldName)) + continue; + + if (variable.Initializer?.Value is not ( + ObjectCreationExpressionSyntax or + ImplicitObjectCreationExpressionSyntax)) + continue; + + ArgumentSyntax? firstArg = variable.Initializer.Value switch + { + ObjectCreationExpressionSyntax oce => oce.ArgumentList?.Arguments.FirstOrDefault(), + ImplicitObjectCreationExpressionSyntax ioce => ioce.ArgumentList?.Arguments.FirstOrDefault(), + _ => null + }; + + if (firstArg is null) + continue; + + var constantValue = semanticModel.GetConstantValue(firstArg.Expression, cancellationToken); + if (!constantValue.HasValue || constantValue.Value is null) + continue; + + var key = constantValue.Value.ToString()!; + if (!valueToField.TryAdd(key, fieldName)) + { + var fieldSymbol = classSymbol.GetMembers(fieldName).OfType().FirstOrDefault(); + diagnostics.Add(new DiagnosticInfo( + DiagnosticDescriptors.DuplicateValue, + fieldSymbol?.CreateLocationInfo(), + className, + valueToField[key], + fieldName)); + } + } + } + } + + private static string? GetNamespace(INamedTypeSymbol symbol) => + symbol.ContainingNamespace.IsGlobalNamespace + ? null + : symbol.ContainingNamespace.ToDisplayString(); +} diff --git a/src/LayeredCraft.OptimizedEnums.Generator/Templates/OptimizedEnum.scriban b/src/LayeredCraft.OptimizedEnums.Generator/Templates/OptimizedEnum.scriban new file mode 100644 index 0000000..bc85f35 --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.Generator/Templates/OptimizedEnum.scriban @@ -0,0 +1,101 @@ +//------------------------------------------------------------------------------ +// +// 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 + +{{~ if namespace != null ~}} +namespace {{ namespace }}; + +{{~ end ~}} +{{ generated_code_attribute }} +partial class {{ class_name }} +{ + private static readonly {{ fully_qualified_class_name }}[] s_all = new {{ fully_qualified_class_name }}[] + { + {{~ for name in member_names ~}} + {{ name }}{{ if !for.last }},{{ end }} + {{~ end ~}} + }; + + private static readonly string[] s_names = new string[] + { + {{~ for name in member_names ~}} + {{ name }}.Name{{ if !for.last }},{{ end }} + {{~ end ~}} + }; + + private static readonly {{ value_type_fully_qualified }}[] s_values = new {{ value_type_fully_qualified }}[] + { + {{~ for name in member_names ~}} + {{ name }}.Value{{ if !for.last }},{{ end }} + {{~ end ~}} + }; + + private static readonly global::System.Collections.Generic.Dictionary s_byName = + new global::System.Collections.Generic.Dictionary(global::System.StringComparer.Ordinal) + { + {{~ for name in member_names ~}} + [{{ name }}.Name] = {{ name }}{{ if !for.last }},{{ end }} + {{~ end ~}} + }; + + private static readonly global::System.Collections.Generic.Dictionary<{{ value_type_fully_qualified }}, {{ fully_qualified_class_name }}> s_byValue = + new global::System.Collections.Generic.Dictionary<{{ value_type_fully_qualified }}, {{ fully_qualified_class_name }}>() + { + {{~ for name in member_names ~}} + [{{ name }}.Value] = {{ name }}{{ if !for.last }},{{ end }} + {{~ end ~}} + }; + + {{ generated_code_attribute }} + public static global::System.Collections.Generic.IReadOnlyList<{{ fully_qualified_class_name }}> All => s_all; + + {{ generated_code_attribute }} + public static global::System.Collections.Generic.IReadOnlyList Names => s_names; + + {{ generated_code_attribute }} + public static global::System.Collections.Generic.IReadOnlyList<{{ value_type_fully_qualified }}> Values => s_values; + + {{ generated_code_attribute }} + public static int Count => s_all.Length; + + {{ generated_code_attribute }} + public static {{ fully_qualified_class_name }} 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 {{ class_name }}"); + + return result; + } + + {{ generated_code_attribute }} + public static bool TryFromName(string name, out {{ fully_qualified_class_name }}? result) => + s_byName.TryGetValue(name, out result); + + {{ generated_code_attribute }} + public static {{ fully_qualified_class_name }} FromValue({{ value_type_fully_qualified }} value) + { + if (!s_byValue.TryGetValue(value, out var result)) + throw new global::System.Collections.Generic.KeyNotFoundException( + $"'{value}' is not a valid value for {{ class_name }}"); + + return result; + } + + {{ generated_code_attribute }} + public static bool TryFromValue({{ value_type_fully_qualified }} value, out {{ fully_qualified_class_name }}? result) => + s_byValue.TryGetValue(value, out result); + + {{ generated_code_attribute }} + public static bool ContainsName(string name) => s_byName.ContainsKey(name); + + {{ generated_code_attribute }} + public static bool ContainsValue({{ value_type_fully_qualified }} value) => s_byValue.ContainsKey(value); +} diff --git a/src/LayeredCraft.OptimizedEnums.Generator/TrackingNames.cs b/src/LayeredCraft.OptimizedEnums.Generator/TrackingNames.cs new file mode 100644 index 0000000..51af3b3 --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.Generator/TrackingNames.cs @@ -0,0 +1,9 @@ +// ReSharper disable InconsistentNaming + +namespace LayeredCraft.OptimizedEnums.Generator; + +internal static class TrackingNames +{ + internal const string EnumSyntaxProvider_Extract = nameof(EnumSyntaxProvider_Extract); + internal const string EnumSyntaxProvider_FilterNotNull = nameof(EnumSyntaxProvider_FilterNotNull); +} diff --git a/src/LayeredCraft.OptimizedEnums/LayeredCraft.OptimizedEnums.csproj b/src/LayeredCraft.OptimizedEnums/LayeredCraft.OptimizedEnums.csproj new file mode 100644 index 0000000..73cb686 --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums/LayeredCraft.OptimizedEnums.csproj @@ -0,0 +1,15 @@ + + + netstandard2.0 + enable + latest + enable + false + + LayeredCraft.OptimizedEnums.Core + + LayeredCraft.OptimizedEnums + LayeredCraft.OptimizedEnums + + \ No newline at end of file diff --git a/src/LayeredCraft.OptimizedEnums/OptimizedEnum.cs b/src/LayeredCraft.OptimizedEnums/OptimizedEnum.cs new file mode 100644 index 0000000..270a595 --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums/OptimizedEnum.cs @@ -0,0 +1,97 @@ +#nullable enable + +namespace LayeredCraft.OptimizedEnums; + +/// +/// Abstract base class for source-generated, high-performance enum types. +/// +/// The concrete enum type deriving from this class. +/// The underlying value type for this enum. +public abstract partial class OptimizedEnum : + IEquatable, + IComparable, + IComparable + where TEnum : OptimizedEnum + where TValue : notnull, IComparable +{ + /// Gets the name of this enum member. + public string Name { get; } + + /// Gets the underlying value of this enum member. + public TValue Value { get; } + + /// Initializes a new enum member with the given value and name. + protected OptimizedEnum(TValue value, string name) + { + if (name is null) + throw new ArgumentNullException(nameof(name)); + + Value = value; + Name = name; + } + + /// + public override string ToString() => Name; + + /// + public sealed override bool Equals(object? obj) => + obj is TEnum other && Equals(other); + + /// + public bool Equals(TEnum? other) => + other is not null && + GetType() == other.GetType() && + EqualityComparer.Default.Equals(Value, other.Value); + + /// + public sealed override int GetHashCode() + { + unchecked + { + var hash = 17; + hash = (hash * 31) + GetType().GetHashCode(); + hash = (hash * 31) + EqualityComparer.Default.GetHashCode(Value); + return hash; + } + } + + /// + public int CompareTo(object? obj) + { + if (obj is null) + return 1; + + if (obj is not TEnum other) + throw new ArgumentException($"Object must be of type {typeof(TEnum).FullName}.", nameof(obj)); + + return CompareTo(other); + } + + /// + public int CompareTo(TEnum? other) + { + if (other is null) + return 1; + + return Value.CompareTo(other.Value); + } + + /// Returns true if equals . + public static bool operator ==(OptimizedEnum? left, OptimizedEnum? right) => + Equals(left, right); + + /// Returns true if does not equal . + public static bool operator !=(OptimizedEnum? left, OptimizedEnum? right) => + !Equals(left, right); +} + +/// +/// Abstract base class for source-generated, high-performance enum types with an value. +/// +/// The concrete enum type deriving from this class. +public abstract class OptimizedEnum : OptimizedEnum + where TEnum : OptimizedEnum +{ + /// Initializes a new enum member with the given value and name. + protected OptimizedEnum(int value, string name) : base(value, name) { } +} \ No newline at end of file diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props new file mode 100644 index 0000000..3eaa75d --- /dev/null +++ b/tests/Directory.Build.props @@ -0,0 +1,6 @@ + + + + + + diff --git a/tests/LayeredCraft.OptimizedEnums.Benchmarks/Directory.Build.props b/tests/LayeredCraft.OptimizedEnums.Benchmarks/Directory.Build.props new file mode 100644 index 0000000..7224e51 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Benchmarks/Directory.Build.props @@ -0,0 +1,4 @@ + + + diff --git a/tests/LayeredCraft.OptimizedEnums.Benchmarks/EnumLookupBenchmarks.cs b/tests/LayeredCraft.OptimizedEnums.Benchmarks/EnumLookupBenchmarks.cs new file mode 100644 index 0000000..0b404ab --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Benchmarks/EnumLookupBenchmarks.cs @@ -0,0 +1,50 @@ +using BenchmarkDotNet.Attributes; + +namespace LayeredCraft.OptimizedEnums.Benchmarks; + +public sealed partial class OrderStatus : OptimizedEnum +{ + public static readonly OrderStatus Pending = new(1, nameof(Pending)); + public static readonly OrderStatus Paid = new(2, nameof(Paid)); + public static readonly OrderStatus Shipped = new(3, nameof(Shipped)); + public static readonly OrderStatus Delivered = new(4, nameof(Delivered)); + public static readonly OrderStatus Cancelled = new(5, nameof(Cancelled)); + + private OrderStatus(int value, string name) : base(value, name) { } +} + +[MemoryDiagnoser] +public class EnumLookupBenchmarks +{ + [Benchmark] + public OrderStatus FromName() => OrderStatus.FromName("Shipped"); + + [Benchmark] + public bool TryFromName() + { + OrderStatus.TryFromName("Shipped", out var result); + return result is not null; + } + + [Benchmark] + public OrderStatus FromValue() => OrderStatus.FromValue(3); + + [Benchmark] + public bool TryFromValue() + { + OrderStatus.TryFromValue(3, out var result); + return result is not null; + } + + [Benchmark] + public bool ContainsName() => OrderStatus.ContainsName("Shipped"); + + [Benchmark] + public bool ContainsValue() => OrderStatus.ContainsValue(3); + + [Benchmark] + public int GetAll() => OrderStatus.All.Count; + + [Benchmark] + public int GetCount() => OrderStatus.Count; +} diff --git a/tests/LayeredCraft.OptimizedEnums.Benchmarks/LayeredCraft.OptimizedEnums.Benchmarks.csproj b/tests/LayeredCraft.OptimizedEnums.Benchmarks/LayeredCraft.OptimizedEnums.Benchmarks.csproj new file mode 100644 index 0000000..246c0fc --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Benchmarks/LayeredCraft.OptimizedEnums.Benchmarks.csproj @@ -0,0 +1,22 @@ + + + Exe + net9.0 + enable + enable + false + default + true + true + + + + + + + + + diff --git a/tests/LayeredCraft.OptimizedEnums.Benchmarks/Program.cs b/tests/LayeredCraft.OptimizedEnums.Benchmarks/Program.cs new file mode 100644 index 0000000..c9a0467 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Benchmarks/Program.cs @@ -0,0 +1,3 @@ +using BenchmarkDotNet.Running; + +BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/GeneratorTestHelpers.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/GeneratorTestHelpers.cs new file mode 100644 index 0000000..d704d5a --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/GeneratorTestHelpers.cs @@ -0,0 +1,179 @@ +using System.Text.RegularExpressions; +using Basic.Reference.Assemblies; +using LayeredCraft.OptimizedEnums; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace LayeredCraft.OptimizedEnums.Generator.Tests; + +/// +/// Extends with additional options for verification testing. +/// +internal class VerifyTestOptions : CodeGenerationOptions +{ + /// Gets or initializes the expected number of syntax trees to be generated. + internal int? ExpectedTrees { get; init; } = null; + + /// Gets or initializes the expected diagnostic ID for failure tests. + internal string? ExpectedDiagnosticId { get; init; } = null; +} + +/// Configuration options for code generation testing. +internal class CodeGenerationOptions +{ + /// Gets or initializes the source code to compile and test. + internal required string SourceCode { get; init; } + + /// Gets or initializes the file path for the source code. + internal string CodePath { get; init; } = "Program.cs"; + + /// Gets or initializes the C# language version to use for compilation. + internal LanguageVersion LanguageVersion { get; init; } = LanguageVersion.CSharp13; + + /// Gets or initializes optional diagnostics to suppress during compilation. + internal Dictionary? DiagnosticsToSuppress { get; init; } = null; + + /// Gets or initializes the name of the test assembly. + internal string AssemblyName { get; init; } = "TestsAssembly"; +} + +internal static class GeneratorTestHelpers +{ + internal static Task Verify( + VerifyTestOptions options, + CancellationToken cancellationToken = default) + { + var (driver, originalCompilation) = GenerateFromSource(options, cancellationToken); + + driver.Should().NotBeNull(); + + var result = driver.GetRunResult(); + + result.Diagnostics + .Should() + .BeEmpty( + "code should be generated without errors, but found:\n" + + string.Join( + "\n---\n", + result.Diagnostics.Select(e => $" - {e.Id}: {e.GetMessage()} at {e.Location}"))); + + // Reparse generated trees with the same parse options to ensure consistent features + var parseOptions = originalCompilation.SyntaxTrees.First().Options; + var reparsedTrees = result.GeneratedTrees + .Select(tree => CSharpSyntaxTree.ParseText(tree.GetText(), (CSharpParseOptions)parseOptions)) + .ToArray(); + + var outputCompilation = originalCompilation.AddSyntaxTrees(reparsedTrees); + + var errors = outputCompilation + .GetDiagnostics(cancellationToken) + .Where(d => d.Severity == DiagnosticSeverity.Error) + .ToList(); + + errors.Should().BeEmpty( + "generated code should compile without errors, but found:\n" + + string.Join( + "\n---\n", + errors.Select(e => $" - {e.Id}: {e.GetMessage()} at {e.Location}"))); + + if (options.ExpectedTrees is not null) + result.GeneratedTrees.Length.Should().Be(options.ExpectedTrees); + + return Verifier + .Verify(driver) + .UseDirectory("Snapshots") + .DisableDiff() + .ScrubLinesWithReplace(line => + { + if (line.Contains("global::System.CodeDom.Compiler.GeneratedCode")) + return RegexHelper.GeneratedCodeAttributeRegex().Replace(line, "REPLACED"); + return line; + }); + } + + internal static Task VerifyFailure( + VerifyTestOptions options, + CancellationToken cancellationToken = default) + { + var (driver, _) = GenerateFromSource(options, cancellationToken); + + driver.Should().NotBeNull(); + + var result = driver.GetRunResult(); + + result.Diagnostics + .Should() + .NotBeEmpty("expected diagnostic errors to be generated"); + + if (options.ExpectedDiagnosticId is not null) + { + result.Diagnostics + .Should() + .Contain( + d => d.Id == options.ExpectedDiagnosticId, + $"expected diagnostic {options.ExpectedDiagnosticId} to be present, but found:\n" + + string.Join( + "\n---\n", + result.Diagnostics.Select(e => $" - {e.Id}: {e.GetMessage()} at {e.Location}"))); + } + + return Verifier + .Verify(driver) + .UseDirectory("Snapshots") + .DisableDiff() + .ScrubLinesWithReplace(line => + { + if (line.Contains("global::System.CodeDom.Compiler.GeneratedCode")) + return RegexHelper.GeneratedCodeAttributeRegex().Replace(line, "REPLACED"); + return line; + }); + } + + internal static (GeneratorDriver driver, Compilation compilation) GenerateFromSource( + CodeGenerationOptions options, + CancellationToken cancellationToken = default) + { + var parseOptions = CSharpParseOptions.Default.WithLanguageVersion(options.LanguageVersion); + + var syntaxTree = CSharpSyntaxTree.ParseText( + options.SourceCode, + parseOptions, + options.CodePath, + cancellationToken: cancellationToken); + + List references = + [ +#if NET10_0_OR_GREATER + .. Net100.References.All.ToList(), +#elif NET9_0 + .. Net90.References.All.ToList(), +#else + .. Net80.References.All.ToList(), +#endif + MetadataReference.CreateFromFile(typeof(OptimizedEnum<,>).Assembly.Location), + ]; + + var compilationOptions = new CSharpCompilationOptions( + OutputKind.DynamicallyLinkedLibrary, + nullableContextOptions: NullableContextOptions.Enable) + .WithSpecificDiagnosticOptions(options.DiagnosticsToSuppress); + + var compilation = CSharpCompilation.Create( + options.AssemblyName, + [syntaxTree], + references, + compilationOptions); + + var generator = new OptimizedEnumGenerator().AsSourceGenerator(); + var driver = CSharpGeneratorDriver.Create(generator); + var updatedDriver = driver.RunGenerators(compilation, cancellationToken); + + return (updatedDriver, compilation); + } +} + +internal static partial class RegexHelper +{ + [GeneratedRegex("""(\d+\.\d+\.\d+\.\d+)""", RegexOptions.None, "en-US")] + internal static partial Regex GeneratedCodeAttributeRegex(); +} diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/GeneratorVerifyTests.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/GeneratorVerifyTests.cs new file mode 100644 index 0000000..abd50e6 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/GeneratorVerifyTests.cs @@ -0,0 +1,165 @@ +using Microsoft.CodeAnalysis; + +namespace LayeredCraft.OptimizedEnums.Generator.Tests; + +public class GeneratorVerifyTests +{ + [Fact] + public async Task SimpleEnum_WithNamespace() => + await GeneratorTestHelpers.Verify( + new VerifyTestOptions + { + SourceCode = """ + using LayeredCraft.OptimizedEnums; + + namespace MyApp.Domain; + + public sealed partial class OrderStatus : OptimizedEnum + { + public static readonly OrderStatus Pending = new(1, nameof(Pending)); + public static readonly OrderStatus Paid = new(2, nameof(Paid)); + public static readonly OrderStatus Shipped = new(3, nameof(Shipped)); + + private OrderStatus(int value, string name) : base(value, name) { } + } + """, + ExpectedTrees = 1, + }, + TestContext.Current.CancellationToken); + + [Fact] + public async Task SimpleEnum_GlobalNamespace() => + await GeneratorTestHelpers.Verify( + new VerifyTestOptions + { + SourceCode = """ + using LayeredCraft.OptimizedEnums; + + public sealed partial class Priority : OptimizedEnum + { + public static readonly Priority Low = new(1, nameof(Low)); + public static readonly Priority Medium = new(2, nameof(Medium)); + public static readonly Priority High = new(3, nameof(High)); + + private Priority(int value, string name) : base(value, name) { } + } + """, + ExpectedTrees = 1, + }, + TestContext.Current.CancellationToken); + + [Fact] + public async Task StringValueType() => + await GeneratorTestHelpers.Verify( + new VerifyTestOptions + { + SourceCode = """ + using LayeredCraft.OptimizedEnums; + + namespace MyApp.Domain; + + public sealed partial class Color : OptimizedEnum + { + public static readonly Color Red = new("red", nameof(Red)); + public static readonly Color Green = new("green", nameof(Green)); + public static readonly Color Blue = new("blue", nameof(Blue)); + + private Color(string value, string name) : base(value, name) { } + } + """, + ExpectedTrees = 1, + }, + TestContext.Current.CancellationToken); + + [Fact] + public async Task MultipleMembers() => + await GeneratorTestHelpers.Verify( + new VerifyTestOptions + { + SourceCode = """ + using LayeredCraft.OptimizedEnums; + + namespace MyApp.Domain; + + public sealed partial class DayOfWeek : OptimizedEnum + { + public static readonly DayOfWeek Monday = new(1, nameof(Monday)); + public static readonly DayOfWeek Tuesday = new(2, nameof(Tuesday)); + public static readonly DayOfWeek Wednesday = new(3, nameof(Wednesday)); + public static readonly DayOfWeek Thursday = new(4, nameof(Thursday)); + public static readonly DayOfWeek Friday = new(5, nameof(Friday)); + public static readonly DayOfWeek Saturday = new(6, nameof(Saturday)); + public static readonly DayOfWeek Sunday = new(7, nameof(Sunday)); + + private DayOfWeek(int value, string name) : base(value, name) { } + } + """, + ExpectedTrees = 1, + }, + TestContext.Current.CancellationToken); + + [Fact] + public async Task Error_NotPartial() => + await GeneratorTestHelpers.VerifyFailure( + new VerifyTestOptions + { + SourceCode = """ + using LayeredCraft.OptimizedEnums; + + namespace MyApp.Domain; + + public sealed class OrderStatus : OptimizedEnum + { + public static readonly OrderStatus Pending = new(1, nameof(Pending)); + + private OrderStatus(int value, string name) : base(value, name) { } + } + """, + ExpectedDiagnosticId = "OE0001", + }, + TestContext.Current.CancellationToken); + + [Fact] + public async Task Error_NoMembers() => + await GeneratorTestHelpers.VerifyFailure( + new VerifyTestOptions + { + SourceCode = """ + using LayeredCraft.OptimizedEnums; + + namespace MyApp.Domain; + + public sealed partial class OrderStatus : OptimizedEnum + { + private OrderStatus(int value, string name) : base(value, name) { } + } + """, + ExpectedDiagnosticId = "OE0004", + }, + TestContext.Current.CancellationToken); + + [Fact] + public async Task Warning_NonPrivateConstructor() => + await GeneratorTestHelpers.Verify( + new VerifyTestOptions + { + SourceCode = """ + using LayeredCraft.OptimizedEnums; + + namespace MyApp.Domain; + + public sealed partial class OrderStatus : OptimizedEnum + { + public static readonly OrderStatus Pending = new(1, nameof(Pending)); + + public OrderStatus(int value, string name) : base(value, name) { } + } + """, + DiagnosticsToSuppress = new Dictionary + { + ["OE0101"] = ReportDiagnostic.Suppress, + }, + ExpectedTrees = 1, + }, + TestContext.Current.CancellationToken); +} diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/LayeredCraft.OptimizedEnums.Generator.Tests.csproj b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/LayeredCraft.OptimizedEnums.Generator.Tests.csproj new file mode 100644 index 0000000..77c3776 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/LayeredCraft.OptimizedEnums.Generator.Tests.csproj @@ -0,0 +1,42 @@ + + + enable + enable + Exe + LayeredCraft.OptimizedEnums.Generator.Tests + net8.0;net9.0;net10.0 + true + false + default + true + true + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/ModuleInitializer.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/ModuleInitializer.cs new file mode 100644 index 0000000..a330fd9 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/ModuleInitializer.cs @@ -0,0 +1,9 @@ +using System.Runtime.CompilerServices; + +namespace LayeredCraft.OptimizedEnums.Generator.Tests; + +public static class ModuleInitializer +{ + [ModuleInitializer] + public static void Init() => VerifySourceGenerators.Initialize(); +} diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Error_NoMembers.verified.txt b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Error_NoMembers.verified.txt new file mode 100644 index 0000000..99773ac --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Error_NoMembers.verified.txt @@ -0,0 +1,17 @@ +{ + Diagnostics: [ + { + Location: Program.cs: (4,0)-(7,1), + Message: The class 'OrderStatus' has no public static readonly fields of its own type, + Severity: Error, + Descriptor: { + Id: OE0004, + Title: No enum members found, + MessageFormat: The class '{0}' has no public static readonly fields of its own type, + Category: OptimizedEnums.Usage, + DefaultSeverity: Error, + IsEnabledByDefault: true + } + } + ] +} \ No newline at end of file diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Error_NotPartial.verified.txt b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Error_NotPartial.verified.txt new file mode 100644 index 0000000..4e77190 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Error_NotPartial.verified.txt @@ -0,0 +1,17 @@ +{ + Diagnostics: [ + { + Location: Program.cs: (4,0)-(9,1), + Message: The class 'OrderStatus' must be declared as partial for OptimizedEnum source generation, + Severity: Error, + Descriptor: { + Id: OE0001, + Title: OptimizedEnum class must be partial, + MessageFormat: The class '{0}' must be declared as partial for OptimizedEnum source generation, + Category: OptimizedEnums.Usage, + DefaultSeverity: Error, + IsEnabledByDefault: true + } + } + ] +} \ No newline at end of file diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.MultipleMembers#DayOfWeek.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.MultipleMembers#DayOfWeek.g.verified.cs new file mode 100644 index 0000000..4570474 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.MultipleMembers#DayOfWeek.g.verified.cs @@ -0,0 +1,120 @@ +//HintName: DayOfWeek.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 DayOfWeek +{ + private static readonly global::MyApp.Domain.DayOfWeek[] s_all = new global::MyApp.Domain.DayOfWeek[] + { + Monday, + Tuesday, + Wednesday, + Thursday, + Friday, + Saturday, + Sunday + }; + + private static readonly string[] s_names = new string[] + { + Monday.Name, + Tuesday.Name, + Wednesday.Name, + Thursday.Name, + Friday.Name, + Saturday.Name, + Sunday.Name + }; + + private static readonly int[] s_values = new int[] + { + Monday.Value, + Tuesday.Value, + Wednesday.Value, + Thursday.Value, + Friday.Value, + Saturday.Value, + Sunday.Value + }; + + private static readonly global::System.Collections.Generic.Dictionary s_byName = + new global::System.Collections.Generic.Dictionary(global::System.StringComparer.Ordinal) + { + [Monday.Name] = Monday, + [Tuesday.Name] = Tuesday, + [Wednesday.Name] = Wednesday, + [Thursday.Name] = Thursday, + [Friday.Name] = Friday, + [Saturday.Name] = Saturday, + [Sunday.Name] = Sunday + }; + + private static readonly global::System.Collections.Generic.Dictionary s_byValue = + new global::System.Collections.Generic.Dictionary() + { + [Monday.Value] = Monday, + [Tuesday.Value] = Tuesday, + [Wednesday.Value] = Wednesday, + [Thursday.Value] = Thursday, + [Friday.Value] = Friday, + [Saturday.Value] = Saturday, + [Sunday.Value] = Sunday + }; + + [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 static int Count => s_all.Length; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.DayOfWeek 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 DayOfWeek"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromName(string name, out global::MyApp.Domain.DayOfWeek? result) => + s_byName.TryGetValue(name, out result); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.DayOfWeek 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 DayOfWeek"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromValue(int value, out global::MyApp.Domain.DayOfWeek? 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.SimpleEnum_GlobalNamespace#Priority.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SimpleEnum_GlobalNamespace#Priority.g.verified.cs new file mode 100644 index 0000000..0f8dfcf --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SimpleEnum_GlobalNamespace#Priority.g.verified.cs @@ -0,0 +1,98 @@ +//HintName: Priority.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 + +[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] +partial class Priority +{ + private static readonly global::Priority[] s_all = new global::Priority[] + { + Low, + Medium, + High + }; + + private static readonly string[] s_names = new string[] + { + Low.Name, + Medium.Name, + High.Name + }; + + private static readonly int[] s_values = new int[] + { + Low.Value, + Medium.Value, + High.Value + }; + + private static readonly global::System.Collections.Generic.Dictionary s_byName = + new global::System.Collections.Generic.Dictionary(global::System.StringComparer.Ordinal) + { + [Low.Name] = Low, + [Medium.Name] = Medium, + [High.Name] = High + }; + + private static readonly global::System.Collections.Generic.Dictionary s_byValue = + new global::System.Collections.Generic.Dictionary() + { + [Low.Value] = Low, + [Medium.Value] = Medium, + [High.Value] = High + }; + + [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 static int Count => s_all.Length; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::Priority 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 Priority"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromName(string name, out global::Priority? result) => + s_byName.TryGetValue(name, out result); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::Priority 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 Priority"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromValue(int value, out global::Priority? 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.SimpleEnum_WithNamespace#OrderStatus.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SimpleEnum_WithNamespace#OrderStatus.g.verified.cs new file mode 100644 index 0000000..96ffef3 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SimpleEnum_WithNamespace#OrderStatus.g.verified.cs @@ -0,0 +1,100 @@ +//HintName: OrderStatus.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 OrderStatus +{ + private static readonly global::MyApp.Domain.OrderStatus[] s_all = new global::MyApp.Domain.OrderStatus[] + { + Pending, + Paid, + Shipped + }; + + private static readonly string[] s_names = new string[] + { + Pending.Name, + Paid.Name, + Shipped.Name + }; + + private static readonly int[] s_values = new int[] + { + Pending.Value, + Paid.Value, + Shipped.Value + }; + + private static readonly global::System.Collections.Generic.Dictionary s_byName = + new global::System.Collections.Generic.Dictionary(global::System.StringComparer.Ordinal) + { + [Pending.Name] = Pending, + [Paid.Name] = Paid, + [Shipped.Name] = Shipped + }; + + private static readonly global::System.Collections.Generic.Dictionary s_byValue = + new global::System.Collections.Generic.Dictionary() + { + [Pending.Value] = Pending, + [Paid.Value] = Paid, + [Shipped.Value] = Shipped + }; + + [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 static int Count => s_all.Length; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.OrderStatus 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 OrderStatus"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromName(string name, out global::MyApp.Domain.OrderStatus? result) => + s_byName.TryGetValue(name, out result); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.OrderStatus 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 OrderStatus"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromValue(int value, out global::MyApp.Domain.OrderStatus? 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.StringValueType#Color.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.StringValueType#Color.g.verified.cs new file mode 100644 index 0000000..0246c20 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.StringValueType#Color.g.verified.cs @@ -0,0 +1,100 @@ +//HintName: Color.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 Color +{ + private static readonly global::MyApp.Domain.Color[] s_all = new global::MyApp.Domain.Color[] + { + Red, + Green, + Blue + }; + + private static readonly string[] s_names = new string[] + { + Red.Name, + Green.Name, + Blue.Name + }; + + private static readonly string[] s_values = new string[] + { + Red.Value, + Green.Value, + Blue.Value + }; + + private static readonly global::System.Collections.Generic.Dictionary s_byName = + new global::System.Collections.Generic.Dictionary(global::System.StringComparer.Ordinal) + { + [Red.Name] = Red, + [Green.Name] = Green, + [Blue.Name] = Blue + }; + + private static readonly global::System.Collections.Generic.Dictionary s_byValue = + new global::System.Collections.Generic.Dictionary() + { + [Red.Value] = Red, + [Green.Value] = Green, + [Blue.Value] = Blue + }; + + [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 static int Count => s_all.Length; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.Color 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 Color"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromName(string name, out global::MyApp.Domain.Color? result) => + s_byName.TryGetValue(name, out result); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.Color FromValue(string value) + { + if (!s_byValue.TryGetValue(value, out var result)) + throw new global::System.Collections.Generic.KeyNotFoundException( + $"'{value}' is not a valid value for Color"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromValue(string value, out global::MyApp.Domain.Color? 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(string value) => s_byValue.ContainsKey(value); +} diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_NonPrivateConstructor#OrderStatus.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_NonPrivateConstructor#OrderStatus.g.verified.cs new file mode 100644 index 0000000..2d7605d --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_NonPrivateConstructor#OrderStatus.g.verified.cs @@ -0,0 +1,90 @@ +//HintName: OrderStatus.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 OrderStatus +{ + private static readonly global::MyApp.Domain.OrderStatus[] s_all = new global::MyApp.Domain.OrderStatus[] + { + Pending + }; + + private static readonly string[] s_names = new string[] + { + Pending.Name + }; + + private static readonly int[] s_values = new int[] + { + Pending.Value + }; + + private static readonly global::System.Collections.Generic.Dictionary s_byName = + new global::System.Collections.Generic.Dictionary(global::System.StringComparer.Ordinal) + { + [Pending.Name] = Pending + }; + + private static readonly global::System.Collections.Generic.Dictionary s_byValue = + new global::System.Collections.Generic.Dictionary() + { + [Pending.Value] = Pending + }; + + [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 static int Count => s_all.Length; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.OrderStatus 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 OrderStatus"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromName(string name, out global::MyApp.Domain.OrderStatus? result) => + s_byName.TryGetValue(name, out result); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.OrderStatus 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 OrderStatus"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromValue(int value, out global::MyApp.Domain.OrderStatus? 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/xunit.runner.json b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/xunit.runner.json new file mode 100644 index 0000000..86c7ea0 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/xunit.runner.json @@ -0,0 +1,3 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json" +} diff --git a/tests/LayeredCraft.OptimizedEnums.Tests/LayeredCraft.OptimizedEnums.Tests.csproj b/tests/LayeredCraft.OptimizedEnums.Tests/LayeredCraft.OptimizedEnums.Tests.csproj new file mode 100644 index 0000000..8ac6698 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Tests/LayeredCraft.OptimizedEnums.Tests.csproj @@ -0,0 +1,35 @@ + + + enable + enable + Exe + LayeredCraft.OptimizedEnums.Tests + net8.0;net9.0;net10.0 + true + false + default + true + true + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/tests/LayeredCraft.OptimizedEnums.Tests/OptimizedEnumTests.cs b/tests/LayeredCraft.OptimizedEnums.Tests/OptimizedEnumTests.cs new file mode 100644 index 0000000..8d1c1bc --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Tests/OptimizedEnumTests.cs @@ -0,0 +1,167 @@ +namespace LayeredCraft.OptimizedEnums.Tests; + +public class OptimizedEnumTests +{ + [Fact] + public void All_ReturnsAllThreeMembers() + { + OrderStatus.All.Should().HaveCount(3); + OrderStatus.All.Should().ContainInOrder(OrderStatus.Pending, OrderStatus.Paid, OrderStatus.Shipped); + } + + [Fact] + public void Names_ReturnsAllMemberNames() + { + OrderStatus.Names.Should().HaveCount(3); + OrderStatus.Names.Should().ContainInOrder("Pending", "Paid", "Shipped"); + } + + [Fact] + public void Values_ReturnsAllMemberValues() + { + OrderStatus.Values.Should().HaveCount(3); + OrderStatus.Values.Should().ContainInOrder(1, 2, 3); + } + + [Fact] + public void Count_ReturnsThree() + { + OrderStatus.Count.Should().Be(3); + } + + [Fact] + public void FromName_ValidName_ReturnsCorrectMember() + { + OrderStatus.FromName("Pending").Should().Be(OrderStatus.Pending); + OrderStatus.FromName("Paid").Should().Be(OrderStatus.Paid); + OrderStatus.FromName("Shipped").Should().Be(OrderStatus.Shipped); + } + + [Fact] + public void FromName_InvalidName_ThrowsKeyNotFoundException() + { + var act = () => OrderStatus.FromName("Unknown"); + act.Should().Throw(); + } + + [Fact] + public void TryFromName_ValidName_ReturnsTrueAndSetsResult() + { + var found = OrderStatus.TryFromName("Paid", out var result); + found.Should().BeTrue(); + result.Should().Be(OrderStatus.Paid); + } + + [Fact] + public void TryFromName_InvalidName_ReturnsFalse() + { + var found = OrderStatus.TryFromName("Unknown", out var result); + found.Should().BeFalse(); + result.Should().BeNull(); + } + + [Fact] + public void FromValue_ValidValue_ReturnsCorrectMember() + { + OrderStatus.FromValue(1).Should().Be(OrderStatus.Pending); + OrderStatus.FromValue(2).Should().Be(OrderStatus.Paid); + OrderStatus.FromValue(3).Should().Be(OrderStatus.Shipped); + } + + [Fact] + public void FromValue_InvalidValue_ThrowsKeyNotFoundException() + { + var act = () => OrderStatus.FromValue(99); + act.Should().Throw(); + } + + [Fact] + public void TryFromValue_ValidValue_ReturnsTrueAndSetsResult() + { + var found = OrderStatus.TryFromValue(3, out var result); + found.Should().BeTrue(); + result.Should().Be(OrderStatus.Shipped); + } + + [Fact] + public void TryFromValue_InvalidValue_ReturnsFalse() + { + var found = OrderStatus.TryFromValue(99, out var result); + found.Should().BeFalse(); + result.Should().BeNull(); + } + + [Fact] + public void ContainsName_KnownName_ReturnsTrue() + { + OrderStatus.ContainsName("Pending").Should().BeTrue(); + } + + [Fact] + public void ContainsName_UnknownName_ReturnsFalse() + { + OrderStatus.ContainsName("Unknown").Should().BeFalse(); + } + + [Fact] + public void ContainsValue_KnownValue_ReturnsTrue() + { + OrderStatus.ContainsValue(1).Should().BeTrue(); + } + + [Fact] + public void ContainsValue_UnknownValue_ReturnsFalse() + { + OrderStatus.ContainsValue(99).Should().BeFalse(); + } + + [Fact] + public void Equals_SameMember_ReturnsTrue() + { + var a = OrderStatus.Pending; + var b = OrderStatus.Pending; + a.Equals(b).Should().BeTrue(); + (a == b).Should().BeTrue(); + } + + [Fact] + public void Equals_DifferentMembers_ReturnsFalse() + { + OrderStatus.Pending.Equals(OrderStatus.Paid).Should().BeFalse(); + (OrderStatus.Pending == OrderStatus.Paid).Should().BeFalse(); + (OrderStatus.Pending != OrderStatus.Paid).Should().BeTrue(); + } + + [Fact] + public void ToString_ReturnsName() + { + OrderStatus.Pending.ToString().Should().Be("Pending"); + OrderStatus.Paid.ToString().Should().Be("Paid"); + } + + [Fact] + public void CompareTo_OrdersByValue() + { + OrderStatus.Pending.CompareTo(OrderStatus.Paid).Should().BeNegative(); + OrderStatus.Shipped.CompareTo(OrderStatus.Pending).Should().BePositive(); + OrderStatus.Paid.CompareTo(OrderStatus.Paid).Should().Be(0); + } + + [Fact] + public void CompareTo_Null_ReturnsPositive() + { + OrderStatus.Pending.CompareTo(null).Should().BePositive(); + } + + [Fact] + public void GetHashCode_SameMember_ReturnsSameHash() + { + OrderStatus.Pending.GetHashCode().Should().Be(OrderStatus.Pending.GetHashCode()); + } + + [Fact] + public void GetHashCode_DifferentMembers_ReturnsDifferentHash() + { + OrderStatus.Pending.GetHashCode().Should().NotBe(OrderStatus.Paid.GetHashCode()); + } +} diff --git a/tests/LayeredCraft.OptimizedEnums.Tests/OrderStatusFixture.cs b/tests/LayeredCraft.OptimizedEnums.Tests/OrderStatusFixture.cs new file mode 100644 index 0000000..74e09fb --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Tests/OrderStatusFixture.cs @@ -0,0 +1,12 @@ +using LayeredCraft.OptimizedEnums; + +namespace LayeredCraft.OptimizedEnums.Tests; + +public sealed partial class OrderStatus : OptimizedEnum +{ + public static readonly OrderStatus Pending = new(1, nameof(Pending)); + public static readonly OrderStatus Paid = new(2, nameof(Paid)); + public static readonly OrderStatus Shipped = new(3, nameof(Shipped)); + + private OrderStatus(int value, string name) : base(value, name) { } +} diff --git a/tests/LayeredCraft.OptimizedEnums.Tests/xunit.runner.json b/tests/LayeredCraft.OptimizedEnums.Tests/xunit.runner.json new file mode 100644 index 0000000..86c7ea0 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Tests/xunit.runner.json @@ -0,0 +1,3 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json" +} From 559feb38586f7a6810eb99b83fcc069d702172b5 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Mon, 30 Mar 2026 14:34:21 -0400 Subject: [PATCH 2/7] docs: update README with packages table and build badge Co-Authored-By: Claude Sonnet 4.6 --- README.md | 42 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0d76fcb..cc007e5 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,27 @@ # LayeredCraft.OptimizedEnums -A high-performance, AOT-safe alternative to SmartEnum patterns using source generation. +**LayeredCraft.OptimizedEnums** is a modular C# .NET library providing high-performance, AOT-safe smart enum patterns using source generation. Inherit from a base class and the generator produces O(1) lookup tables, collection properties, and factory methods — all at compile time with zero reflection at runtime. -## Features +## Key Features - **Zero reflection** — all lookup tables are source-generated at compile time -- **AOT / trimming friendly** — no runtime type discovery +- **AOT / trimming friendly** — compatible with NativeAOT, ReadyToRun, and Blazor WASM - **O(1) lookups** — `FromName`, `FromValue`, `ContainsName`, `ContainsValue` - **Compile-time validation** — errors for missing `partial`, duplicate values/names - **No allocations per call** — all collections are statically cached +- **Inheritance-based triggering** — no attribute required, just inherit and go + +## 📦 Packages + +| Package | NuGet | Downloads | +|---------|-------|-----------| +| **LayeredCraft.OptimizedEnums** | [![NuGet](https://img.shields.io/nuget/v/LayeredCraft.OptimizedEnums.svg)](https://www.nuget.org/packages/LayeredCraft.OptimizedEnums) | [![Downloads](https://img.shields.io/nuget/dt/LayeredCraft.OptimizedEnums.svg)](https://www.nuget.org/packages/LayeredCraft.OptimizedEnums/) | +| **LayeredCraft.OptimizedEnums.SystemTextJson** | _coming soon_ | | +| **LayeredCraft.OptimizedEnums.EFCore** | _coming soon_ | | +| **LayeredCraft.OptimizedEnums.Dapper** | _coming soon_ | | +| **LayeredCraft.OptimizedEnums.AutoFixture** | _coming soon_ | | + +[![Build Status](https://github.com/LayeredCraft/optimized-enums/actions/workflows/build.yaml/badge.svg)](https://github.com/LayeredCraft/optimized-enums/actions/workflows/build.yaml) ## Usage @@ -23,6 +36,19 @@ public sealed partial class OrderStatus : OptimizedEnum } ``` +Or use the `int`-defaulting convenience base class: + +```csharp +public sealed partial class Priority : OptimizedEnum +{ + public static readonly Priority Low = new(1, nameof(Low)); + public static readonly Priority Medium = new(2, nameof(Medium)); + public static readonly Priority High = new(3, nameof(High)); + + private Priority(int value, string name) : base(value, name) { } +} +``` + The source generator produces: ```csharp @@ -42,7 +68,7 @@ OrderStatus.ContainsValue(99); // false IReadOnlyList all = OrderStatus.All; IReadOnlyList names = OrderStatus.Names; IReadOnlyList values = OrderStatus.Values; -int count = OrderStatus.Count; +int count = OrderStatus.Count; // compile-time constant ``` ## Performance @@ -64,10 +90,16 @@ All lookups are O(1) via statically-cached dictionaries. `Count` is a compile-ti ## Installation -``` +```bash dotnet add package LayeredCraft.OptimizedEnums ``` +Supports **.NET 8.0**, **.NET 9.0**, **.NET 10.0**. + +## Documentation + +Full documentation is available at the [LayeredCraft.OptimizedEnums docs site](https://layeredcraft.github.io/optimized-enums). + ## License MIT From 5ff621089305f29cd644e3ea5987a87d244bd96e Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Mon, 30 Mar 2026 15:03:17 -0400 Subject: [PATCH 3/7] fix: resolve hint name collisions and nested type generation - EnumEmitter: use fully-qualified name (minus global::) as hint name to prevent DuplicateHintNameException when two types share a class name across different namespaces - EnumSyntaxProvider: capture containing type chain via GetContainingTypeDeclarations, stored as EquatableArray on EnumInfo - EnumEmitter: build preamble/suffix strings in C# instead of Scriban to avoid whitespace control complexity; nested types now emit correct partial class wrappers in the generated output - Scriban template: replace namespace if-block with {{ preamble ~}} and add {{ suffix }} for containing type closing braces - Rename snapshots to reflect new namespace-qualified hint names - Add NestedType and SameClassName_DifferentNamespaces snapshot tests Co-Authored-By: Claude Sonnet 4.6 --- .../Emitters/EnumEmitter.cs | 35 ++++++- .../Models/EnumInfo.cs | 4 +- .../Providers/EnumSyntaxProvider.cs | 21 ++++ .../Templates/OptimizedEnum.scriban | 7 +- .../GeneratorVerifyTests.cs | 55 +++++++++++ ...bers#MyApp.Domain.DayOfWeek.g.verified.cs} | 2 +- ...pe#MyApp.Domain.Outer.Status.g.verified.cs | 98 +++++++++++++++++++ ...espaces#MyApp.Domain1.Status.g.verified.cs | 90 +++++++++++++++++ ...espaces#MyApp.Domain2.Status.g.verified.cs | 90 +++++++++++++++++ ...ce#MyApp.Domain.OrderStatus.g.verified.cs} | 2 +- ...alueType#MyApp.Domain.Color.g.verified.cs} | 2 +- ...or#MyApp.Domain.OrderStatus.g.verified.cs} | 2 +- 12 files changed, 396 insertions(+), 12 deletions(-) rename tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/{GeneratorVerifyTests.MultipleMembers#DayOfWeek.g.verified.cs => GeneratorVerifyTests.MultipleMembers#MyApp.Domain.DayOfWeek.g.verified.cs} (99%) create mode 100644 tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.NestedType#MyApp.Domain.Outer.Status.g.verified.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SameClassName_DifferentNamespaces#MyApp.Domain1.Status.g.verified.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SameClassName_DifferentNamespaces#MyApp.Domain2.Status.g.verified.cs rename tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/{GeneratorVerifyTests.SimpleEnum_WithNamespace#OrderStatus.g.verified.cs => GeneratorVerifyTests.SimpleEnum_WithNamespace#MyApp.Domain.OrderStatus.g.verified.cs} (98%) rename tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/{GeneratorVerifyTests.StringValueType#Color.g.verified.cs => GeneratorVerifyTests.StringValueType#MyApp.Domain.Color.g.verified.cs} (99%) rename tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/{GeneratorVerifyTests.Warning_NonPrivateConstructor#OrderStatus.g.verified.cs => GeneratorVerifyTests.Warning_NonPrivateConstructor#MyApp.Domain.OrderStatus.g.verified.cs} (98%) diff --git a/src/LayeredCraft.OptimizedEnums.Generator/Emitters/EnumEmitter.cs b/src/LayeredCraft.OptimizedEnums.Generator/Emitters/EnumEmitter.cs index 2b7488f..af6b176 100644 --- a/src/LayeredCraft.OptimizedEnums.Generator/Emitters/EnumEmitter.cs +++ b/src/LayeredCraft.OptimizedEnums.Generator/Emitters/EnumEmitter.cs @@ -28,14 +28,45 @@ internal static void Generate(SourceProductionContext context, EnumInfo info) var model = new { GeneratedCodeAttribute, - info.Namespace, info.ClassName, info.FullyQualifiedClassName, info.ValueTypeFullyQualified, MemberNames = info.MemberNames.ToArray(), + Preamble = BuildPreamble(info), + Suffix = BuildSuffix(info), }; var outputCode = TemplateHelper.Render("Templates.OptimizedEnum.scriban", model); - context.AddSource($"{info.ClassName}.g.cs", outputCode); + // Use the fully-qualified name (minus "global::") as the hint name to avoid + // collisions when two types share a class name in different namespaces. + var hintName = info.FullyQualifiedClassName.Replace("global::", "") + ".g.cs"; + context.AddSource(hintName, outputCode); + } + + private static string BuildPreamble(EnumInfo info) + { + if (info.Namespace is null && info.ContainingTypeNames.Length == 0) + return string.Empty; + + var sb = new System.Text.StringBuilder(); + if (info.Namespace is not null) + sb.Append("namespace ").Append(info.Namespace).Append(";\n\n"); + + foreach (var ct in info.ContainingTypeNames) + sb.Append(ct).Append("\n{\n"); + + return sb.ToString(); + } + + private static string BuildSuffix(EnumInfo info) + { + if (info.ContainingTypeNames.Length == 0) + return string.Empty; + + var sb = new System.Text.StringBuilder(); + for (var i = 0; i < info.ContainingTypeNames.Length; i++) + sb.Append("\n}"); + + return sb.ToString(); } } diff --git a/src/LayeredCraft.OptimizedEnums.Generator/Models/EnumInfo.cs b/src/LayeredCraft.OptimizedEnums.Generator/Models/EnumInfo.cs index 68058f7..252b153 100644 --- a/src/LayeredCraft.OptimizedEnums.Generator/Models/EnumInfo.cs +++ b/src/LayeredCraft.OptimizedEnums.Generator/Models/EnumInfo.cs @@ -12,6 +12,7 @@ internal sealed record EnumInfo( string FullyQualifiedClassName, string ValueTypeFullyQualified, EquatableArray MemberNames, + EquatableArray ContainingTypeNames, EquatableArray Diagnostics, LocationInfo? Location ) @@ -23,9 +24,10 @@ other is not null && FullyQualifiedClassName == other.FullyQualifiedClassName && ValueTypeFullyQualified == other.ValueTypeFullyQualified && MemberNames == other.MemberNames + && ContainingTypeNames == other.ContainingTypeNames && Diagnostics == other.Diagnostics; public override int GetHashCode() => HashCode.Combine(Namespace, ClassName, FullyQualifiedClassName, ValueTypeFullyQualified, - MemberNames, Diagnostics); + MemberNames, ContainingTypeNames, Diagnostics); } diff --git a/src/LayeredCraft.OptimizedEnums.Generator/Providers/EnumSyntaxProvider.cs b/src/LayeredCraft.OptimizedEnums.Generator/Providers/EnumSyntaxProvider.cs index 8d63091..635403c 100644 --- a/src/LayeredCraft.OptimizedEnums.Generator/Providers/EnumSyntaxProvider.cs +++ b/src/LayeredCraft.OptimizedEnums.Generator/Providers/EnumSyntaxProvider.cs @@ -51,6 +51,7 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) => FullyQualifiedClassName: classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), ValueTypeFullyQualified: string.Empty, MemberNames: EquatableArray.Empty, + ContainingTypeNames: EquatableArray.Empty, Diagnostics: diagnostics.ToEquatableArray(), Location: location); } @@ -134,6 +135,7 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) => FullyQualifiedClassName: classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), ValueTypeFullyQualified: valueTypeFullyQualified, MemberNames: validMembers.ToEquatableArray(), + ContainingTypeNames: GetContainingTypeDeclarations(classSymbol), Diagnostics: diagnostics.ToEquatableArray(), Location: location); } @@ -216,6 +218,25 @@ ObjectCreationExpressionSyntax or } } + private static EquatableArray GetContainingTypeDeclarations(INamedTypeSymbol symbol) + { + var result = new List(); + var current = symbol.ContainingType; + while (current is not null) + { + var keyword = (current.IsRecord, current.TypeKind) switch + { + (true, TypeKind.Struct) => "record struct", + (true, _) => "record", + (_, TypeKind.Struct) => "struct", + _ => "class" + }; + result.Insert(0, $"partial {keyword} {current.Name}"); + current = current.ContainingType; + } + return result.ToEquatableArray(); + } + private static string? GetNamespace(INamedTypeSymbol symbol) => symbol.ContainingNamespace.IsGlobalNamespace ? null diff --git a/src/LayeredCraft.OptimizedEnums.Generator/Templates/OptimizedEnum.scriban b/src/LayeredCraft.OptimizedEnums.Generator/Templates/OptimizedEnum.scriban index bc85f35..888cb28 100644 --- a/src/LayeredCraft.OptimizedEnums.Generator/Templates/OptimizedEnum.scriban +++ b/src/LayeredCraft.OptimizedEnums.Generator/Templates/OptimizedEnum.scriban @@ -9,10 +9,7 @@ #nullable enable -{{~ if namespace != null ~}} -namespace {{ namespace }}; - -{{~ end ~}} +{{ preamble ~}} {{ generated_code_attribute }} partial class {{ class_name }} { @@ -98,4 +95,4 @@ partial class {{ class_name }} {{ generated_code_attribute }} public static bool ContainsValue({{ value_type_fully_qualified }} value) => s_byValue.ContainsKey(value); -} +}{{ suffix }} diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/GeneratorVerifyTests.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/GeneratorVerifyTests.cs index abd50e6..98faa86 100644 --- a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/GeneratorVerifyTests.cs +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/GeneratorVerifyTests.cs @@ -138,6 +138,61 @@ private OrderStatus(int value, string name) : base(value, name) { } }, TestContext.Current.CancellationToken); + [Fact] + public async Task NestedType() => + await GeneratorTestHelpers.Verify( + new VerifyTestOptions + { + SourceCode = """ + using LayeredCraft.OptimizedEnums; + + namespace MyApp.Domain; + + public partial class Outer + { + public sealed partial class Status : OptimizedEnum + { + public static readonly Status Active = new(1, nameof(Active)); + public static readonly Status Inactive = new(2, nameof(Inactive)); + + private Status(int value, string name) : base(value, name) { } + } + } + """, + ExpectedTrees = 1, + }, + TestContext.Current.CancellationToken); + + [Fact] + public async Task SameClassName_DifferentNamespaces() => + await GeneratorTestHelpers.Verify( + new VerifyTestOptions + { + SourceCode = """ + using LayeredCraft.OptimizedEnums; + + namespace MyApp.Domain1 + { + public sealed partial class Status : OptimizedEnum + { + public static readonly Status Active = new(1, nameof(Active)); + private Status(int value, string name) : base(value, name) { } + } + } + + namespace MyApp.Domain2 + { + public sealed partial class Status : OptimizedEnum + { + public static readonly Status Active = new(1, nameof(Active)); + private Status(int value, string name) : base(value, name) { } + } + } + """, + ExpectedTrees = 2, + }, + TestContext.Current.CancellationToken); + [Fact] public async Task Warning_NonPrivateConstructor() => await GeneratorTestHelpers.Verify( diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.MultipleMembers#DayOfWeek.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.MultipleMembers#MyApp.Domain.DayOfWeek.g.verified.cs similarity index 99% rename from tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.MultipleMembers#DayOfWeek.g.verified.cs rename to tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.MultipleMembers#MyApp.Domain.DayOfWeek.g.verified.cs index 4570474..30a22e5 100644 --- a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.MultipleMembers#DayOfWeek.g.verified.cs +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.MultipleMembers#MyApp.Domain.DayOfWeek.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: DayOfWeek.g.cs +//HintName: MyApp.Domain.DayOfWeek.g.cs //------------------------------------------------------------------------------ // // This code was generated by a tool. 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 new file mode 100644 index 0000000..727b13d --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.NestedType#MyApp.Domain.Outer.Status.g.verified.cs @@ -0,0 +1,98 @@ +//HintName: MyApp.Domain.Outer.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; + +partial class Outer +{ +[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] +partial class Status +{ + private static readonly global::MyApp.Domain.Outer.Status[] s_all = new global::MyApp.Domain.Outer.Status[] + { + Active, + Inactive + }; + + private static readonly string[] s_names = new string[] + { + Active.Name, + Inactive.Name + }; + + private static readonly int[] s_values = new int[] + { + Active.Value, + Inactive.Value + }; + + private static readonly global::System.Collections.Generic.Dictionary s_byName = + new global::System.Collections.Generic.Dictionary(global::System.StringComparer.Ordinal) + { + [Active.Name] = Active, + [Inactive.Name] = Inactive + }; + + private static readonly global::System.Collections.Generic.Dictionary s_byValue = + new global::System.Collections.Generic.Dictionary() + { + [Active.Value] = Active, + [Inactive.Value] = Inactive + }; + + [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 static int Count => s_all.Length; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.Outer.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.Outer.Status? result) => + s_byName.TryGetValue(name, out result); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.Outer.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.Outer.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.SameClassName_DifferentNamespaces#MyApp.Domain1.Status.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SameClassName_DifferentNamespaces#MyApp.Domain1.Status.g.verified.cs new file mode 100644 index 0000000..66955c4 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SameClassName_DifferentNamespaces#MyApp.Domain1.Status.g.verified.cs @@ -0,0 +1,90 @@ +//HintName: MyApp.Domain1.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.Domain1; + +[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] +partial class Status +{ + private static readonly global::MyApp.Domain1.Status[] s_all = new global::MyApp.Domain1.Status[] + { + Active + }; + + private static readonly string[] s_names = new string[] + { + Active.Name + }; + + private static readonly int[] s_values = new int[] + { + Active.Value + }; + + private static readonly global::System.Collections.Generic.Dictionary s_byName = + new global::System.Collections.Generic.Dictionary(global::System.StringComparer.Ordinal) + { + [Active.Name] = Active + }; + + private static readonly global::System.Collections.Generic.Dictionary s_byValue = + new global::System.Collections.Generic.Dictionary() + { + [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 static int Count => s_all.Length; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain1.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.Domain1.Status? result) => + s_byName.TryGetValue(name, out result); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain1.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.Domain1.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.SameClassName_DifferentNamespaces#MyApp.Domain2.Status.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SameClassName_DifferentNamespaces#MyApp.Domain2.Status.g.verified.cs new file mode 100644 index 0000000..7f5f9b1 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SameClassName_DifferentNamespaces#MyApp.Domain2.Status.g.verified.cs @@ -0,0 +1,90 @@ +//HintName: MyApp.Domain2.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.Domain2; + +[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] +partial class Status +{ + private static readonly global::MyApp.Domain2.Status[] s_all = new global::MyApp.Domain2.Status[] + { + Active + }; + + private static readonly string[] s_names = new string[] + { + Active.Name + }; + + private static readonly int[] s_values = new int[] + { + Active.Value + }; + + private static readonly global::System.Collections.Generic.Dictionary s_byName = + new global::System.Collections.Generic.Dictionary(global::System.StringComparer.Ordinal) + { + [Active.Name] = Active + }; + + private static readonly global::System.Collections.Generic.Dictionary s_byValue = + new global::System.Collections.Generic.Dictionary() + { + [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 static int Count => s_all.Length; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain2.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.Domain2.Status? result) => + s_byName.TryGetValue(name, out result); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain2.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.Domain2.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.SimpleEnum_WithNamespace#OrderStatus.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SimpleEnum_WithNamespace#MyApp.Domain.OrderStatus.g.verified.cs similarity index 98% rename from tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SimpleEnum_WithNamespace#OrderStatus.g.verified.cs rename to tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SimpleEnum_WithNamespace#MyApp.Domain.OrderStatus.g.verified.cs index 96ffef3..7e42a21 100644 --- a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SimpleEnum_WithNamespace#OrderStatus.g.verified.cs +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SimpleEnum_WithNamespace#MyApp.Domain.OrderStatus.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: OrderStatus.g.cs +//HintName: MyApp.Domain.OrderStatus.g.cs //------------------------------------------------------------------------------ // // This code was generated by a tool. diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.StringValueType#Color.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.StringValueType#MyApp.Domain.Color.g.verified.cs similarity index 99% rename from tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.StringValueType#Color.g.verified.cs rename to tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.StringValueType#MyApp.Domain.Color.g.verified.cs index 0246c20..32295f6 100644 --- a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.StringValueType#Color.g.verified.cs +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.StringValueType#MyApp.Domain.Color.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: Color.g.cs +//HintName: MyApp.Domain.Color.g.cs //------------------------------------------------------------------------------ // // This code was generated by a tool. diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_NonPrivateConstructor#OrderStatus.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_NonPrivateConstructor#MyApp.Domain.OrderStatus.g.verified.cs similarity index 98% rename from tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_NonPrivateConstructor#OrderStatus.g.verified.cs rename to tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_NonPrivateConstructor#MyApp.Domain.OrderStatus.g.verified.cs index 2d7605d..141c2f7 100644 --- a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_NonPrivateConstructor#OrderStatus.g.verified.cs +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_NonPrivateConstructor#MyApp.Domain.OrderStatus.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: OrderStatus.g.cs +//HintName: MyApp.Domain.OrderStatus.g.cs //------------------------------------------------------------------------------ // // This code was generated by a tool. From 255b1db46abe478491f4b1fb101bda52a1512539 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Mon, 30 Mar 2026 15:05:26 -0400 Subject: [PATCH 4/7] chore: update version prefix to 1.0.0 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index d7df32f..31643fd 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 0.1.0-alpha + 1.0.0 MIT https://github.com/layeredcraft/optimized-enums git From a92eacf09b76a46027a4ef70424d822aeea3f3be Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Mon, 30 Mar 2026 15:25:55 -0400 Subject: [PATCH 5/7] fix: address sr-net-reviewer findings and add missing diagnostic test coverage - DiagnosticInfo.Equals/GetHashCode now include MessageArgs for correct incremental cache invalidation - Add OE9001 GeneratorInternalError descriptor; wrap template render+AddSource in try/catch - Replace C# 13 field keyword lazy prop with static readonly for thread safety in EnumEmitter - Use HashSet in DetectDuplicateValues to avoid O(n) Contains on validMembers list - ReadOnlyCollection via Array.AsReadOnly prevents cast-and-mutate of singleton collections - Dictionary initial capacity hints and public const Count (compile-time constant) in template - Add document Location exclusion comment in EnumInfo equality - Add OE0101 assert, OE0102 non-readonly field, and OE0005 duplicate value tests (12 total, all pass) Co-Authored-By: Claude Sonnet 4.6 --- Directory.Packages.props | 2 +- .../Diagnostics/DiagnosticDescriptors.cs | 8 ++ .../Diagnostics/DiagnosticInfo.cs | 16 +++- .../Emitters/EnumEmitter.cs | 34 ++++--- .../Models/EnumInfo.cs | 3 + .../Providers/EnumSyntaxProvider.cs | 4 +- .../Templates/OptimizedEnum.scriban | 45 ++++----- .../GeneratorVerifyTests.cs | 65 +++++++++++++ ...raft.OptimizedEnums.Generator.Tests.csproj | 1 + ...0005_DuplicateValue_IsEmitted.verified.txt | 17 ++++ ...mbers#MyApp.Domain.DayOfWeek.g.verified.cs | 73 ++++++++------- ...pe#MyApp.Domain.Outer.Status.g.verified.cs | 39 ++++---- ...espaces#MyApp.Domain1.Status.g.verified.cs | 33 ++++--- ...espaces#MyApp.Domain2.Status.g.verified.cs | 33 ++++--- ...num_GlobalNamespace#Priority.g.verified.cs | 45 ++++----- ...ace#MyApp.Domain.OrderStatus.g.verified.cs | 45 ++++----- ...ValueType#MyApp.Domain.Color.g.verified.cs | 45 ++++----- ...tor#MyApp.Domain.OrderStatus.g.verified.cs | 33 ++++--- ...ted#MyApp.Domain.OrderStatus.g.verified.cs | 93 +++++++++++++++++++ ...nPrivateConstructor_IsEmitted.verified.txt | 18 ++++ ...ted#MyApp.Domain.OrderStatus.g.verified.cs | 93 +++++++++++++++++++ ...02_NonReadonlyField_IsEmitted.verified.txt | 18 ++++ 22 files changed, 560 insertions(+), 203 deletions(-) create mode 100644 tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Error_OE0005_DuplicateValue_IsEmitted.verified.txt create mode 100644 tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0101_NonPrivateConstructor_IsEmitted#MyApp.Domain.OrderStatus.g.verified.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0101_NonPrivateConstructor_IsEmitted.verified.txt create mode 100644 tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0102_NonReadonlyField_IsEmitted#MyApp.Domain.OrderStatus.g.verified.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0102_NonReadonlyField_IsEmitted.verified.txt diff --git a/Directory.Packages.props b/Directory.Packages.props index 3375eba..22754f9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -11,7 +11,7 @@ - + diff --git a/src/LayeredCraft.OptimizedEnums.Generator/Diagnostics/DiagnosticDescriptors.cs b/src/LayeredCraft.OptimizedEnums.Generator/Diagnostics/DiagnosticDescriptors.cs index 70bd815..81fc72a 100644 --- a/src/LayeredCraft.OptimizedEnums.Generator/Diagnostics/DiagnosticDescriptors.cs +++ b/src/LayeredCraft.OptimizedEnums.Generator/Diagnostics/DiagnosticDescriptors.cs @@ -53,4 +53,12 @@ internal static class DiagnosticDescriptors UsageCategory, DiagnosticSeverity.Warning, isEnabledByDefault: true); + + internal static readonly DiagnosticDescriptor GeneratorInternalError = new( + "OE9001", + "OptimizedEnum generator internal error", + "An unexpected error occurred while generating code for '{0}': {1}", + UsageCategory, + DiagnosticSeverity.Error, + isEnabledByDefault: true); } diff --git a/src/LayeredCraft.OptimizedEnums.Generator/Diagnostics/DiagnosticInfo.cs b/src/LayeredCraft.OptimizedEnums.Generator/Diagnostics/DiagnosticInfo.cs index f532576..d3178b0 100644 --- a/src/LayeredCraft.OptimizedEnums.Generator/Diagnostics/DiagnosticInfo.cs +++ b/src/LayeredCraft.OptimizedEnums.Generator/Diagnostics/DiagnosticInfo.cs @@ -11,11 +11,19 @@ params object?[] MessageArgs { public bool Equals(DiagnosticInfo? other) => other is not null - && Equals(DiagnosticDescriptor.Id, other.DiagnosticDescriptor.Id) - && Equals(LocationInfo, other.LocationInfo); + && DiagnosticDescriptor.Id == other.DiagnosticDescriptor.Id + && Equals(LocationInfo, other.LocationInfo) + && MessageArgs.SequenceEqual(other.MessageArgs); - public override int GetHashCode() => - HashCode.Combine(DiagnosticDescriptor.Id, LocationInfo); + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(DiagnosticDescriptor.Id); + hash.Add(LocationInfo); + foreach (var arg in MessageArgs) + hash.Add(arg); + return hash.ToHashCode(); + } } internal static class DiagnosticInfoExtensions diff --git a/src/LayeredCraft.OptimizedEnums.Generator/Emitters/EnumEmitter.cs b/src/LayeredCraft.OptimizedEnums.Generator/Emitters/EnumEmitter.cs index af6b176..6974cea 100644 --- a/src/LayeredCraft.OptimizedEnums.Generator/Emitters/EnumEmitter.cs +++ b/src/LayeredCraft.OptimizedEnums.Generator/Emitters/EnumEmitter.cs @@ -7,20 +7,12 @@ namespace LayeredCraft.OptimizedEnums.Generator.Emitters; internal static class EnumEmitter { - private static string GeneratedCodeAttribute - { - get - { - if (field is null) - { - var assembly = Assembly.GetExecutingAssembly(); - var name = assembly.GetName().Name; - var version = assembly.GetName().Version!.ToString(); - field = $"""[global::System.CodeDom.Compiler.GeneratedCode("{name}", "{version}")]"""; - } + private static readonly string GeneratedCodeAttribute = BuildGeneratedCodeAttribute(); - return field; - } + private static string BuildGeneratedCodeAttribute() + { + var asm = Assembly.GetExecutingAssembly(); + return $"""[global::System.CodeDom.Compiler.GeneratedCode("{asm.GetName().Name}", "{asm.GetName().Version}")]"""; } internal static void Generate(SourceProductionContext context, EnumInfo info) @@ -36,11 +28,23 @@ internal static void Generate(SourceProductionContext context, EnumInfo info) Suffix = BuildSuffix(info), }; - var outputCode = TemplateHelper.Render("Templates.OptimizedEnum.scriban", model); // Use the fully-qualified name (minus "global::") as the hint name to avoid // collisions when two types share a class name in different namespaces. var hintName = info.FullyQualifiedClassName.Replace("global::", "") + ".g.cs"; - context.AddSource(hintName, outputCode); + + try + { + var outputCode = TemplateHelper.Render("Templates.OptimizedEnum.scriban", model); + context.AddSource(hintName, outputCode); + } + catch (Exception ex) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.GeneratorInternalError, + info.Location?.ToLocation(), + info.ClassName, + ex.Message)); + } } private static string BuildPreamble(EnumInfo info) diff --git a/src/LayeredCraft.OptimizedEnums.Generator/Models/EnumInfo.cs b/src/LayeredCraft.OptimizedEnums.Generator/Models/EnumInfo.cs index 252b153..433b5c2 100644 --- a/src/LayeredCraft.OptimizedEnums.Generator/Models/EnumInfo.cs +++ b/src/LayeredCraft.OptimizedEnums.Generator/Models/EnumInfo.cs @@ -17,6 +17,9 @@ internal sealed record EnumInfo( LocationInfo? Location ) { + // Location is intentionally excluded from equality so that a position-only change + // (e.g. the user adds a blank line above the class) does not bust the incremental + // cache and trigger unnecessary re-emission of the same generated file. public bool Equals(EnumInfo? other) => other is not null && Namespace == other.Namespace diff --git a/src/LayeredCraft.OptimizedEnums.Generator/Providers/EnumSyntaxProvider.cs b/src/LayeredCraft.OptimizedEnums.Generator/Providers/EnumSyntaxProvider.cs index 635403c..7b4d7fd 100644 --- a/src/LayeredCraft.OptimizedEnums.Generator/Providers/EnumSyntaxProvider.cs +++ b/src/LayeredCraft.OptimizedEnums.Generator/Providers/EnumSyntaxProvider.cs @@ -170,6 +170,7 @@ private static void DetectDuplicateValues( { // Build a mapping of field name -> constant value (best effort, skips non-literals) var valueToField = new Dictionary(StringComparer.Ordinal); + var memberSet = new HashSet(memberNames, StringComparer.Ordinal); foreach (var member in classDecl.Members) { @@ -181,7 +182,7 @@ private static void DetectDuplicateValues( cancellationToken.ThrowIfCancellationRequested(); var fieldName = variable.Identifier.Text; - if (!memberNames.Contains(fieldName)) + if (!memberSet.Contains(fieldName)) continue; if (variable.Initializer?.Value is not ( @@ -229,6 +230,7 @@ private static EquatableArray GetContainingTypeDeclarations(INamedTypeSy (true, TypeKind.Struct) => "record struct", (true, _) => "record", (_, TypeKind.Struct) => "struct", + (_, TypeKind.Interface) => "interface", _ => "class" }; result.Insert(0, $"partial {keyword} {current.Name}"); diff --git a/src/LayeredCraft.OptimizedEnums.Generator/Templates/OptimizedEnum.scriban b/src/LayeredCraft.OptimizedEnums.Generator/Templates/OptimizedEnum.scriban index 888cb28..2d70536 100644 --- a/src/LayeredCraft.OptimizedEnums.Generator/Templates/OptimizedEnum.scriban +++ b/src/LayeredCraft.OptimizedEnums.Generator/Templates/OptimizedEnum.scriban @@ -13,29 +13,32 @@ {{ generated_code_attribute }} partial class {{ class_name }} { - private static readonly {{ fully_qualified_class_name }}[] s_all = new {{ fully_qualified_class_name }}[] - { - {{~ for name in member_names ~}} - {{ name }}{{ if !for.last }},{{ end }} - {{~ end ~}} - }; + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection<{{ fully_qualified_class_name }}> s_all = + global::System.Array.AsReadOnly(new {{ fully_qualified_class_name }}[] + { + {{~ for name in member_names ~}} + {{ name }}{{ if !for.last }},{{ end }} + {{~ end ~}} + }); - private static readonly string[] s_names = new string[] - { - {{~ for name in member_names ~}} - {{ name }}.Name{{ if !for.last }},{{ end }} - {{~ end ~}} - }; + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_names = + global::System.Array.AsReadOnly(new string[] + { + {{~ for name in member_names ~}} + {{ name }}.Name{{ if !for.last }},{{ end }} + {{~ end ~}} + }); - private static readonly {{ value_type_fully_qualified }}[] s_values = new {{ value_type_fully_qualified }}[] - { - {{~ for name in member_names ~}} - {{ name }}.Value{{ if !for.last }},{{ end }} - {{~ end ~}} - }; + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection<{{ value_type_fully_qualified }}> s_values = + global::System.Array.AsReadOnly(new {{ value_type_fully_qualified }}[] + { + {{~ for name in member_names ~}} + {{ name }}.Value{{ if !for.last }},{{ end }} + {{~ end ~}} + }); private static readonly global::System.Collections.Generic.Dictionary s_byName = - new global::System.Collections.Generic.Dictionary(global::System.StringComparer.Ordinal) + new global::System.Collections.Generic.Dictionary({{ member_names.size }}, global::System.StringComparer.Ordinal) { {{~ for name in member_names ~}} [{{ name }}.Name] = {{ name }}{{ if !for.last }},{{ end }} @@ -43,7 +46,7 @@ partial class {{ class_name }} }; private static readonly global::System.Collections.Generic.Dictionary<{{ value_type_fully_qualified }}, {{ fully_qualified_class_name }}> s_byValue = - new global::System.Collections.Generic.Dictionary<{{ value_type_fully_qualified }}, {{ fully_qualified_class_name }}>() + new global::System.Collections.Generic.Dictionary<{{ value_type_fully_qualified }}, {{ fully_qualified_class_name }}>({{ member_names.size }}) { {{~ for name in member_names ~}} [{{ name }}.Value] = {{ name }}{{ if !for.last }},{{ end }} @@ -60,7 +63,7 @@ partial class {{ class_name }} public static global::System.Collections.Generic.IReadOnlyList<{{ value_type_fully_qualified }}> Values => s_values; {{ generated_code_attribute }} - public static int Count => s_all.Length; + public const int Count = {{ member_names.size }}; {{ generated_code_attribute }} public static {{ fully_qualified_class_name }} FromName(string name) diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/GeneratorVerifyTests.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/GeneratorVerifyTests.cs index 98faa86..04e0744 100644 --- a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/GeneratorVerifyTests.cs +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/GeneratorVerifyTests.cs @@ -217,4 +217,69 @@ public OrderStatus(int value, string name) : base(value, name) { } ExpectedTrees = 1, }, TestContext.Current.CancellationToken); + + [Fact] + public async Task Warning_OE0101_NonPrivateConstructor_IsEmitted() => + await GeneratorTestHelpers.VerifyFailure( + new VerifyTestOptions + { + SourceCode = """ + using LayeredCraft.OptimizedEnums; + + namespace MyApp.Domain; + + public sealed partial class OrderStatus : OptimizedEnum + { + public static readonly OrderStatus Pending = new(1, nameof(Pending)); + + public OrderStatus(int value, string name) : base(value, name) { } + } + """, + ExpectedDiagnosticId = "OE0101", + }, + TestContext.Current.CancellationToken); + + [Fact] + public async Task Warning_OE0102_NonReadonlyField_IsEmitted() => + await GeneratorTestHelpers.VerifyFailure( + new VerifyTestOptions + { + SourceCode = """ + using LayeredCraft.OptimizedEnums; + + namespace MyApp.Domain; + + public sealed partial class OrderStatus : OptimizedEnum + { + public static readonly OrderStatus Pending = new(1, nameof(Pending)); + public static OrderStatus NonReadonly = new(2, nameof(NonReadonly)); + + private OrderStatus(int value, string name) : base(value, name) { } + } + """, + ExpectedDiagnosticId = "OE0102", + }, + TestContext.Current.CancellationToken); + + [Fact] + public async Task Error_OE0005_DuplicateValue_IsEmitted() => + await GeneratorTestHelpers.VerifyFailure( + new VerifyTestOptions + { + SourceCode = """ + using LayeredCraft.OptimizedEnums; + + namespace MyApp.Domain; + + public sealed partial class OrderStatus : OptimizedEnum + { + public static readonly OrderStatus Pending = new(1, nameof(Pending)); + public static readonly OrderStatus Duplicate = new(1, nameof(Duplicate)); + + private OrderStatus(int value, string name) : base(value, name) { } + } + """, + ExpectedDiagnosticId = "OE0005", + }, + TestContext.Current.CancellationToken); } diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/LayeredCraft.OptimizedEnums.Generator.Tests.csproj b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/LayeredCraft.OptimizedEnums.Generator.Tests.csproj index 77c3776..0252a0f 100644 --- a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/LayeredCraft.OptimizedEnums.Generator.Tests.csproj +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/LayeredCraft.OptimizedEnums.Generator.Tests.csproj @@ -8,6 +8,7 @@ true false default + MSB3243 true true diff --git a/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Error_OE0005_DuplicateValue_IsEmitted.verified.txt b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Error_OE0005_DuplicateValue_IsEmitted.verified.txt new file mode 100644 index 0000000..ba071df --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Error_OE0005_DuplicateValue_IsEmitted.verified.txt @@ -0,0 +1,17 @@ +{ + Diagnostics: [ + { + Location: Program.cs: (7,39)-(7,48), + Message: The class 'OrderStatus' has duplicate value on fields 'Pending' and 'Duplicate', + Severity: Error, + Descriptor: { + Id: OE0005, + Title: Duplicate enum value, + MessageFormat: The class '{0}' has duplicate value on fields '{1}' and '{2}', + Category: OptimizedEnums.Usage, + DefaultSeverity: Error, + IsEnabledByDefault: true + } + } + ] +} \ No newline at end of file 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 30a22e5..161b62c 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 @@ -15,41 +15,44 @@ namespace MyApp.Domain; [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] partial class DayOfWeek { - private static readonly global::MyApp.Domain.DayOfWeek[] s_all = new global::MyApp.Domain.DayOfWeek[] - { - Monday, - Tuesday, - Wednesday, - Thursday, - Friday, - Saturday, - Sunday - }; - - private static readonly string[] s_names = new string[] - { - Monday.Name, - Tuesday.Name, - Wednesday.Name, - Thursday.Name, - Friday.Name, - Saturday.Name, - Sunday.Name - }; - - private static readonly int[] s_values = new int[] - { - Monday.Value, - Tuesday.Value, - Wednesday.Value, - Thursday.Value, - Friday.Value, - Saturday.Value, - Sunday.Value - }; + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_all = + global::System.Array.AsReadOnly(new global::MyApp.Domain.DayOfWeek[] + { + Monday, + Tuesday, + Wednesday, + Thursday, + Friday, + Saturday, + Sunday + }); + + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_names = + global::System.Array.AsReadOnly(new string[] + { + Monday.Name, + Tuesday.Name, + Wednesday.Name, + Thursday.Name, + Friday.Name, + Saturday.Name, + Sunday.Name + }); + + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_values = + global::System.Array.AsReadOnly(new int[] + { + Monday.Value, + Tuesday.Value, + Wednesday.Value, + Thursday.Value, + Friday.Value, + Saturday.Value, + Sunday.Value + }); private static readonly global::System.Collections.Generic.Dictionary s_byName = - new global::System.Collections.Generic.Dictionary(global::System.StringComparer.Ordinal) + new global::System.Collections.Generic.Dictionary(7, global::System.StringComparer.Ordinal) { [Monday.Name] = Monday, [Tuesday.Name] = Tuesday, @@ -61,7 +64,7 @@ partial class DayOfWeek }; private static readonly global::System.Collections.Generic.Dictionary s_byValue = - new global::System.Collections.Generic.Dictionary() + new global::System.Collections.Generic.Dictionary(7) { [Monday.Value] = Monday, [Tuesday.Value] = Tuesday, @@ -82,7 +85,7 @@ partial class DayOfWeek public static global::System.Collections.Generic.IReadOnlyList Values => s_values; [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static int Count => s_all.Length; + public const int Count = 7; [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] public static global::MyApp.Domain.DayOfWeek FromName(string name) 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 727b13d..b518f29 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 @@ -17,33 +17,36 @@ partial class Outer [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] partial class Status { - private static readonly global::MyApp.Domain.Outer.Status[] s_all = new global::MyApp.Domain.Outer.Status[] - { - Active, - Inactive - }; + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_all = + global::System.Array.AsReadOnly(new global::MyApp.Domain.Outer.Status[] + { + Active, + Inactive + }); - private static readonly string[] s_names = new string[] - { - Active.Name, - Inactive.Name - }; + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_names = + global::System.Array.AsReadOnly(new string[] + { + Active.Name, + Inactive.Name + }); - private static readonly int[] s_values = new int[] - { - Active.Value, - Inactive.Value - }; + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_values = + global::System.Array.AsReadOnly(new int[] + { + Active.Value, + Inactive.Value + }); private static readonly global::System.Collections.Generic.Dictionary s_byName = - new global::System.Collections.Generic.Dictionary(global::System.StringComparer.Ordinal) + new global::System.Collections.Generic.Dictionary(2, global::System.StringComparer.Ordinal) { [Active.Name] = Active, [Inactive.Name] = Inactive }; private static readonly global::System.Collections.Generic.Dictionary s_byValue = - new global::System.Collections.Generic.Dictionary() + new global::System.Collections.Generic.Dictionary(2) { [Active.Value] = Active, [Inactive.Value] = Inactive @@ -59,7 +62,7 @@ partial class Status public static global::System.Collections.Generic.IReadOnlyList Values => s_values; [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static int Count => s_all.Length; + public const int Count = 2; [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] public static global::MyApp.Domain.Outer.Status FromName(string name) 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 66955c4..196385f 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 @@ -15,29 +15,32 @@ namespace MyApp.Domain1; [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] partial class Status { - private static readonly global::MyApp.Domain1.Status[] s_all = new global::MyApp.Domain1.Status[] - { - Active - }; + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_all = + global::System.Array.AsReadOnly(new global::MyApp.Domain1.Status[] + { + Active + }); - private static readonly string[] s_names = new string[] - { - Active.Name - }; + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_names = + global::System.Array.AsReadOnly(new string[] + { + Active.Name + }); - private static readonly int[] s_values = new int[] - { - Active.Value - }; + 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(global::System.StringComparer.Ordinal) + 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() + new global::System.Collections.Generic.Dictionary(1) { [Active.Value] = Active }; @@ -52,7 +55,7 @@ partial class Status public static global::System.Collections.Generic.IReadOnlyList Values => s_values; [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static int Count => s_all.Length; + public const int Count = 1; [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] public static global::MyApp.Domain1.Status FromName(string name) 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 7f5f9b1..94f1a72 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 @@ -15,29 +15,32 @@ namespace MyApp.Domain2; [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] partial class Status { - private static readonly global::MyApp.Domain2.Status[] s_all = new global::MyApp.Domain2.Status[] - { - Active - }; + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_all = + global::System.Array.AsReadOnly(new global::MyApp.Domain2.Status[] + { + Active + }); - private static readonly string[] s_names = new string[] - { - Active.Name - }; + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_names = + global::System.Array.AsReadOnly(new string[] + { + Active.Name + }); - private static readonly int[] s_values = new int[] - { - Active.Value - }; + 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(global::System.StringComparer.Ordinal) + 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() + new global::System.Collections.Generic.Dictionary(1) { [Active.Value] = Active }; @@ -52,7 +55,7 @@ partial class Status public static global::System.Collections.Generic.IReadOnlyList Values => s_values; [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static int Count => s_all.Length; + public const int Count = 1; [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] public static global::MyApp.Domain2.Status FromName(string name) 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 0f8dfcf..591ee43 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 @@ -13,29 +13,32 @@ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] partial class Priority { - private static readonly global::Priority[] s_all = new global::Priority[] - { - Low, - Medium, - High - }; + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_all = + global::System.Array.AsReadOnly(new global::Priority[] + { + Low, + Medium, + High + }); - private static readonly string[] s_names = new string[] - { - Low.Name, - Medium.Name, - High.Name - }; + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_names = + global::System.Array.AsReadOnly(new string[] + { + Low.Name, + Medium.Name, + High.Name + }); - private static readonly int[] s_values = new int[] - { - Low.Value, - Medium.Value, - High.Value - }; + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_values = + global::System.Array.AsReadOnly(new int[] + { + Low.Value, + Medium.Value, + High.Value + }); private static readonly global::System.Collections.Generic.Dictionary s_byName = - new global::System.Collections.Generic.Dictionary(global::System.StringComparer.Ordinal) + new global::System.Collections.Generic.Dictionary(3, global::System.StringComparer.Ordinal) { [Low.Name] = Low, [Medium.Name] = Medium, @@ -43,7 +46,7 @@ partial class Priority }; private static readonly global::System.Collections.Generic.Dictionary s_byValue = - new global::System.Collections.Generic.Dictionary() + new global::System.Collections.Generic.Dictionary(3) { [Low.Value] = Low, [Medium.Value] = Medium, @@ -60,7 +63,7 @@ partial class Priority public static global::System.Collections.Generic.IReadOnlyList Values => s_values; [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static int Count => s_all.Length; + public const int Count = 3; [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] public static global::Priority FromName(string name) 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 7e42a21..b85c817 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 @@ -15,29 +15,32 @@ namespace MyApp.Domain; [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] partial class OrderStatus { - private static readonly global::MyApp.Domain.OrderStatus[] s_all = new global::MyApp.Domain.OrderStatus[] - { - Pending, - Paid, - Shipped - }; + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_all = + global::System.Array.AsReadOnly(new global::MyApp.Domain.OrderStatus[] + { + Pending, + Paid, + Shipped + }); - private static readonly string[] s_names = new string[] - { - Pending.Name, - Paid.Name, - Shipped.Name - }; + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_names = + global::System.Array.AsReadOnly(new string[] + { + Pending.Name, + Paid.Name, + Shipped.Name + }); - private static readonly int[] s_values = new int[] - { - Pending.Value, - Paid.Value, - Shipped.Value - }; + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_values = + global::System.Array.AsReadOnly(new int[] + { + Pending.Value, + Paid.Value, + Shipped.Value + }); private static readonly global::System.Collections.Generic.Dictionary s_byName = - new global::System.Collections.Generic.Dictionary(global::System.StringComparer.Ordinal) + new global::System.Collections.Generic.Dictionary(3, global::System.StringComparer.Ordinal) { [Pending.Name] = Pending, [Paid.Name] = Paid, @@ -45,7 +48,7 @@ partial class OrderStatus }; private static readonly global::System.Collections.Generic.Dictionary s_byValue = - new global::System.Collections.Generic.Dictionary() + new global::System.Collections.Generic.Dictionary(3) { [Pending.Value] = Pending, [Paid.Value] = Paid, @@ -62,7 +65,7 @@ partial class OrderStatus public static global::System.Collections.Generic.IReadOnlyList Values => s_values; [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static int Count => s_all.Length; + public const int Count = 3; [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] public static global::MyApp.Domain.OrderStatus FromName(string name) 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 32295f6..f48c64b 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 @@ -15,29 +15,32 @@ namespace MyApp.Domain; [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] partial class Color { - private static readonly global::MyApp.Domain.Color[] s_all = new global::MyApp.Domain.Color[] - { - Red, - Green, - Blue - }; + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_all = + global::System.Array.AsReadOnly(new global::MyApp.Domain.Color[] + { + Red, + Green, + Blue + }); - private static readonly string[] s_names = new string[] - { - Red.Name, - Green.Name, - Blue.Name - }; + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_names = + global::System.Array.AsReadOnly(new string[] + { + Red.Name, + Green.Name, + Blue.Name + }); - private static readonly string[] s_values = new string[] - { - Red.Value, - Green.Value, - Blue.Value - }; + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_values = + global::System.Array.AsReadOnly(new string[] + { + Red.Value, + Green.Value, + Blue.Value + }); private static readonly global::System.Collections.Generic.Dictionary s_byName = - new global::System.Collections.Generic.Dictionary(global::System.StringComparer.Ordinal) + new global::System.Collections.Generic.Dictionary(3, global::System.StringComparer.Ordinal) { [Red.Name] = Red, [Green.Name] = Green, @@ -45,7 +48,7 @@ partial class Color }; private static readonly global::System.Collections.Generic.Dictionary s_byValue = - new global::System.Collections.Generic.Dictionary() + new global::System.Collections.Generic.Dictionary(3) { [Red.Value] = Red, [Green.Value] = Green, @@ -62,7 +65,7 @@ partial class Color public static global::System.Collections.Generic.IReadOnlyList Values => s_values; [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static int Count => s_all.Length; + public const int Count = 3; [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] public static global::MyApp.Domain.Color FromName(string name) 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 141c2f7..a64d62c 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 @@ -15,29 +15,32 @@ namespace MyApp.Domain; [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] partial class OrderStatus { - private static readonly global::MyApp.Domain.OrderStatus[] s_all = new global::MyApp.Domain.OrderStatus[] - { - Pending - }; + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_all = + global::System.Array.AsReadOnly(new global::MyApp.Domain.OrderStatus[] + { + Pending + }); - private static readonly string[] s_names = new string[] - { - Pending.Name - }; + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_names = + global::System.Array.AsReadOnly(new string[] + { + Pending.Name + }); - private static readonly int[] s_values = new int[] - { - Pending.Value - }; + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_values = + global::System.Array.AsReadOnly(new int[] + { + Pending.Value + }); private static readonly global::System.Collections.Generic.Dictionary s_byName = - new global::System.Collections.Generic.Dictionary(global::System.StringComparer.Ordinal) + new global::System.Collections.Generic.Dictionary(1, global::System.StringComparer.Ordinal) { [Pending.Name] = Pending }; private static readonly global::System.Collections.Generic.Dictionary s_byValue = - new global::System.Collections.Generic.Dictionary() + new global::System.Collections.Generic.Dictionary(1) { [Pending.Value] = Pending }; @@ -52,7 +55,7 @@ partial class OrderStatus public static global::System.Collections.Generic.IReadOnlyList Values => s_values; [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] - public static int Count => s_all.Length; + public const int Count = 1; [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] public static global::MyApp.Domain.OrderStatus FromName(string name) 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 new file mode 100644 index 0000000..a64d62c --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0101_NonPrivateConstructor_IsEmitted#MyApp.Domain.OrderStatus.g.verified.cs @@ -0,0 +1,93 @@ +//HintName: MyApp.Domain.OrderStatus.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 OrderStatus +{ + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_all = + global::System.Array.AsReadOnly(new global::MyApp.Domain.OrderStatus[] + { + Pending + }); + + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_names = + global::System.Array.AsReadOnly(new string[] + { + Pending.Name + }); + + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_values = + global::System.Array.AsReadOnly(new int[] + { + Pending.Value + }); + + private static readonly global::System.Collections.Generic.Dictionary s_byName = + new global::System.Collections.Generic.Dictionary(1, global::System.StringComparer.Ordinal) + { + [Pending.Name] = Pending + }; + + private static readonly global::System.Collections.Generic.Dictionary s_byValue = + new global::System.Collections.Generic.Dictionary(1) + { + [Pending.Value] = Pending + }; + + [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.OrderStatus 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 OrderStatus"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromName(string name, out global::MyApp.Domain.OrderStatus? result) => + s_byName.TryGetValue(name, out result); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.OrderStatus 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 OrderStatus"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromValue(int value, out global::MyApp.Domain.OrderStatus? 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_OE0101_NonPrivateConstructor_IsEmitted.verified.txt b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0101_NonPrivateConstructor_IsEmitted.verified.txt new file mode 100644 index 0000000..1a17607 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0101_NonPrivateConstructor_IsEmitted.verified.txt @@ -0,0 +1,18 @@ +{ + Diagnostics: [ + { + Location: Program.cs: (8,11)-(8,22), + Message: The class 'OrderStatus' has a non-private constructor; OptimizedEnum constructors should be private to prevent direct instantiation, + Severity: Warning, + WarningLevel: 1, + Descriptor: { + Id: OE0101, + Title: OptimizedEnum constructor should be private, + MessageFormat: The class '{0}' has a non-private constructor; OptimizedEnum constructors should be private to prevent direct instantiation, + Category: OptimizedEnums.Usage, + DefaultSeverity: Warning, + IsEnabledByDefault: true + } + } + ] +} \ No newline at end of file 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 new file mode 100644 index 0000000..a64d62c --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0102_NonReadonlyField_IsEmitted#MyApp.Domain.OrderStatus.g.verified.cs @@ -0,0 +1,93 @@ +//HintName: MyApp.Domain.OrderStatus.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 OrderStatus +{ + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_all = + global::System.Array.AsReadOnly(new global::MyApp.Domain.OrderStatus[] + { + Pending + }); + + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_names = + global::System.Array.AsReadOnly(new string[] + { + Pending.Name + }); + + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_values = + global::System.Array.AsReadOnly(new int[] + { + Pending.Value + }); + + private static readonly global::System.Collections.Generic.Dictionary s_byName = + new global::System.Collections.Generic.Dictionary(1, global::System.StringComparer.Ordinal) + { + [Pending.Name] = Pending + }; + + private static readonly global::System.Collections.Generic.Dictionary s_byValue = + new global::System.Collections.Generic.Dictionary(1) + { + [Pending.Value] = Pending + }; + + [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.OrderStatus 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 OrderStatus"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromName(string name, out global::MyApp.Domain.OrderStatus? result) => + s_byName.TryGetValue(name, out result); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.OrderStatus 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 OrderStatus"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromValue(int value, out global::MyApp.Domain.OrderStatus? 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_OE0102_NonReadonlyField_IsEmitted.verified.txt b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0102_NonReadonlyField_IsEmitted.verified.txt new file mode 100644 index 0000000..8cc1417 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_OE0102_NonReadonlyField_IsEmitted.verified.txt @@ -0,0 +1,18 @@ +{ + Diagnostics: [ + { + Location: Program.cs: (7,30)-(7,41), + Message: The field 'NonReadonly' in class 'OrderStatus' is a public static field of the enum type but is not readonly, + Severity: Warning, + WarningLevel: 1, + Descriptor: { + Id: OE0102, + Title: OptimizedEnum static field should be readonly, + MessageFormat: The field '{0}' in class '{1}' is a public static field of the enum type but is not readonly, + Category: OptimizedEnums.Usage, + DefaultSeverity: Warning, + IsEnabledByDefault: true + } + } + ] +} \ No newline at end of file From 05b01ab61022e1d9f080f0fbfdd48c762e0496c4 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Mon, 30 Mar 2026 15:37:10 -0400 Subject: [PATCH 6/7] fix(generator): preserve outer type signatures, fix partial-file duplicate scan, use value equality for duplicate detection - GetContainingTypeDeclarations now includes type parameters (MinimallyQualifiedFormat) and static modifier so generated partial wrappers compile for generic/static outer types - DetectDuplicateValues now iterates all DeclaringSyntaxReferences instead of a single classDecl so members declared in other partial files are included in duplicate-value checks - Switch duplicate-value dictionary key from .ToString() to raw object so value-type equality (e.g. decimal 1.0m == 1.00m) is used rather than string representation Co-Authored-By: Claude Sonnet 4.6 --- .../Providers/EnumSyntaxProvider.cs | 91 +++++++++++-------- 1 file changed, 51 insertions(+), 40 deletions(-) diff --git a/src/LayeredCraft.OptimizedEnums.Generator/Providers/EnumSyntaxProvider.cs b/src/LayeredCraft.OptimizedEnums.Generator/Providers/EnumSyntaxProvider.cs index 7b4d7fd..d03c275 100644 --- a/src/LayeredCraft.OptimizedEnums.Generator/Providers/EnumSyntaxProvider.cs +++ b/src/LayeredCraft.OptimizedEnums.Generator/Providers/EnumSyntaxProvider.cs @@ -118,7 +118,7 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) => } // OE0005: duplicate values (best-effort, only for compile-time constants) - DetectDuplicateValues(classSymbol, classDecl, context.SemanticModel, validMembers, diagnostics, className, cancellationToken); + DetectDuplicateValues(classSymbol, context.SemanticModel, validMembers, diagnostics, className, cancellationToken); // OE0004: no valid members if (validMembers.Count == 0) @@ -161,59 +161,66 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) => private static void DetectDuplicateValues( INamedTypeSymbol classSymbol, - ClassDeclarationSyntax classDecl, SemanticModel semanticModel, List memberNames, List diagnostics, string className, CancellationToken cancellationToken) { - // Build a mapping of field name -> constant value (best effort, skips non-literals) - var valueToField = new Dictionary(StringComparer.Ordinal); + // Build a mapping of constant value -> first field name (best effort, skips non-literals). + // Use object keys so that value-type equality (e.g. decimal 1.0m == 1.00m) is respected + // rather than their differing string representations. + var valueToField = new Dictionary(); var memberSet = new HashSet(memberNames, StringComparer.Ordinal); - foreach (var member in classDecl.Members) + // Iterate ALL partial declarations so that members defined in other files are covered. + foreach (var syntaxRef in classSymbol.DeclaringSyntaxReferences) { - if (member is not FieldDeclarationSyntax fieldDecl) + if (syntaxRef.GetSyntax(cancellationToken) is not ClassDeclarationSyntax partialDecl) continue; - foreach (var variable in fieldDecl.Declaration.Variables) + foreach (var member in partialDecl.Members) { - cancellationToken.ThrowIfCancellationRequested(); - - var fieldName = variable.Identifier.Text; - if (!memberSet.Contains(fieldName)) - continue; - - if (variable.Initializer?.Value is not ( - ObjectCreationExpressionSyntax or - ImplicitObjectCreationExpressionSyntax)) - continue; - - ArgumentSyntax? firstArg = variable.Initializer.Value switch - { - ObjectCreationExpressionSyntax oce => oce.ArgumentList?.Arguments.FirstOrDefault(), - ImplicitObjectCreationExpressionSyntax ioce => ioce.ArgumentList?.Arguments.FirstOrDefault(), - _ => null - }; - - if (firstArg is null) - continue; - - var constantValue = semanticModel.GetConstantValue(firstArg.Expression, cancellationToken); - if (!constantValue.HasValue || constantValue.Value is null) + if (member is not FieldDeclarationSyntax fieldDecl) continue; - var key = constantValue.Value.ToString()!; - if (!valueToField.TryAdd(key, fieldName)) + foreach (var variable in fieldDecl.Declaration.Variables) { - var fieldSymbol = classSymbol.GetMembers(fieldName).OfType().FirstOrDefault(); - diagnostics.Add(new DiagnosticInfo( - DiagnosticDescriptors.DuplicateValue, - fieldSymbol?.CreateLocationInfo(), - className, - valueToField[key], - fieldName)); + cancellationToken.ThrowIfCancellationRequested(); + + var fieldName = variable.Identifier.Text; + if (!memberSet.Contains(fieldName)) + continue; + + if (variable.Initializer?.Value is not ( + ObjectCreationExpressionSyntax or + ImplicitObjectCreationExpressionSyntax)) + continue; + + ArgumentSyntax? firstArg = variable.Initializer.Value switch + { + ObjectCreationExpressionSyntax oce => oce.ArgumentList?.Arguments.FirstOrDefault(), + ImplicitObjectCreationExpressionSyntax ioce => ioce.ArgumentList?.Arguments.FirstOrDefault(), + _ => null + }; + + if (firstArg is null) + continue; + + var constantValue = semanticModel.GetConstantValue(firstArg.Expression, cancellationToken); + if (!constantValue.HasValue || constantValue.Value is null) + continue; + + if (!valueToField.TryAdd(constantValue.Value, fieldName)) + { + var fieldSymbol = classSymbol.GetMembers(fieldName).OfType().FirstOrDefault(); + diagnostics.Add(new DiagnosticInfo( + DiagnosticDescriptors.DuplicateValue, + fieldSymbol?.CreateLocationInfo(), + className, + valueToField[constantValue.Value], + fieldName)); + } } } } @@ -233,7 +240,11 @@ private static EquatableArray GetContainingTypeDeclarations(INamedTypeSy (_, TypeKind.Interface) => "interface", _ => "class" }; - result.Insert(0, $"partial {keyword} {current.Name}"); + // Include static modifier and type parameters so the generated partial declaration + // matches the original (e.g. static partial class Outer or partial class Outer). + var staticModifier = current.IsStatic ? "static " : ""; + var nameWithTypeParams = current.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); + result.Insert(0, $"partial {staticModifier}{keyword} {nameWithTypeParams}"); current = current.ContainingType; } return result.ToEquatableArray(); From 39902870d97599193f679ed468e56a55065f4e89 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Mon, 30 Mar 2026 15:46:37 -0400 Subject: [PATCH 7/7] chore: untrack .idea folder from git Co-Authored-By: Claude Sonnet 4.6 --- .../.idea/.gitignore | 15 --------------- .../.idea/encodings.xml | 4 ---- 2 files changed, 19 deletions(-) delete mode 100644 .idea/.idea.LayeredCraft.OptimizedEnums/.idea/.gitignore delete mode 100644 .idea/.idea.LayeredCraft.OptimizedEnums/.idea/encodings.xml diff --git a/.idea/.idea.LayeredCraft.OptimizedEnums/.idea/.gitignore b/.idea/.idea.LayeredCraft.OptimizedEnums/.idea/.gitignore deleted file mode 100644 index 71c5671..0000000 --- a/.idea/.idea.LayeredCraft.OptimizedEnums/.idea/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Rider ignored files -/projectSettingsUpdater.xml -/contentModel.xml -/modules.xml -/.idea.LayeredCraft.OptimizedEnums.iml -# Ignored default folder with query files -/queries/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml -# Editor-based HTTP Client requests -/httpRequests/ diff --git a/.idea/.idea.LayeredCraft.OptimizedEnums/.idea/encodings.xml b/.idea/.idea.LayeredCraft.OptimizedEnums/.idea/encodings.xml deleted file mode 100644 index df87cf9..0000000 --- a/.idea/.idea.LayeredCraft.OptimizedEnums/.idea/encodings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file