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/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..31643fd --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,37 @@ + + + 1.0.0 + 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..22754f9 --- /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..cc007e5 --- /dev/null +++ b/README.md @@ -0,0 +1,105 @@ +# LayeredCraft.OptimizedEnums + +**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. + +## Key Features + +- **Zero reflection** — all lookup tables are source-generated at compile time +- **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 + +```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) { } +} +``` + +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 +// 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; // compile-time constant +``` + +## 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 + +```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 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 0000000..17a036b Binary files /dev/null and b/docs/assets/icon.png differ 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 0000000..17a036b Binary files /dev/null and b/icon.png differ 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..81fc72a --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.Generator/Diagnostics/DiagnosticDescriptors.cs @@ -0,0 +1,64 @@ +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); + + 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 new file mode 100644 index 0000000..d3178b0 --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.Generator/Diagnostics/DiagnosticInfo.cs @@ -0,0 +1,42 @@ +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 + && DiagnosticDescriptor.Id == other.DiagnosticDescriptor.Id + && Equals(LocationInfo, other.LocationInfo) + && MessageArgs.SequenceEqual(other.MessageArgs); + + 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 +{ + 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..6974cea --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.Generator/Emitters/EnumEmitter.cs @@ -0,0 +1,76 @@ +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 readonly string GeneratedCodeAttribute = BuildGeneratedCodeAttribute(); + + 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) + { + var model = new + { + GeneratedCodeAttribute, + info.ClassName, + info.FullyQualifiedClassName, + info.ValueTypeFullyQualified, + MemberNames = info.MemberNames.ToArray(), + Preamble = BuildPreamble(info), + Suffix = BuildSuffix(info), + }; + + // 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"; + + 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) + { + 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/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..433b5c2 --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.Generator/Models/EnumInfo.cs @@ -0,0 +1,36 @@ +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 ContainingTypeNames, + EquatableArray Diagnostics, + 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 + && ClassName == other.ClassName + && 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, ContainingTypeNames, 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..d03c275 --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.Generator/Providers/EnumSyntaxProvider.cs @@ -0,0 +1,257 @@ +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, + ContainingTypeNames: 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, 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(), + ContainingTypeNames: GetContainingTypeDeclarations(classSymbol), + 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, + SemanticModel semanticModel, + List memberNames, + List diagnostics, + string className, + CancellationToken cancellationToken) + { + // 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); + + // Iterate ALL partial declarations so that members defined in other files are covered. + foreach (var syntaxRef in classSymbol.DeclaringSyntaxReferences) + { + if (syntaxRef.GetSyntax(cancellationToken) is not ClassDeclarationSyntax partialDecl) + continue; + + foreach (var member in partialDecl.Members) + { + if (member is not FieldDeclarationSyntax fieldDecl) + continue; + + foreach (var variable in fieldDecl.Declaration.Variables) + { + 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)); + } + } + } + } + } + + 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", + (_, TypeKind.Interface) => "interface", + _ => "class" + }; + // 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(); + } + + 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..2d70536 --- /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 + +{{ preamble ~}} +{{ generated_code_attribute }} +partial class {{ class_name }} +{ + 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 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 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({{ member_names.size }}, 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 }}>({{ member_names.size }}) + { + {{~ 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 const int Count = {{ member_names.size }}; + + {{ 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); +}{{ suffix }} 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..04e0744 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/GeneratorVerifyTests.cs @@ -0,0 +1,285 @@ +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 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( + 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); + + [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 new file mode 100644 index 0000000..0252a0f --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/LayeredCraft.OptimizedEnums.Generator.Tests.csproj @@ -0,0 +1,43 @@ + + + enable + enable + Exe + LayeredCraft.OptimizedEnums.Generator.Tests + net8.0;net9.0;net10.0 + true + false + default + MSB3243 + 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.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 new file mode 100644 index 0000000..161b62c --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.MultipleMembers#MyApp.Domain.DayOfWeek.g.verified.cs @@ -0,0 +1,123 @@ +//HintName: MyApp.Domain.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::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(7, 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(7) + { + [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 const int Count = 7; + + [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.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..b518f29 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.NestedType#MyApp.Domain.Outer.Status.g.verified.cs @@ -0,0 +1,101 @@ +//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::System.Collections.ObjectModel.ReadOnlyCollection s_all = + global::System.Array.AsReadOnly(new global::MyApp.Domain.Outer.Status[] + { + Active, + Inactive + }); + + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_names = + global::System.Array.AsReadOnly(new string[] + { + Active.Name, + Inactive.Name + }); + + 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(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(2) + { + [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 const int Count = 2; + + [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..196385f --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SameClassName_DifferentNamespaces#MyApp.Domain1.Status.g.verified.cs @@ -0,0 +1,93 @@ +//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::System.Collections.ObjectModel.ReadOnlyCollection s_all = + global::System.Array.AsReadOnly(new global::MyApp.Domain1.Status[] + { + Active + }); + + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_names = + global::System.Array.AsReadOnly(new string[] + { + Active.Name + }); + + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_values = + global::System.Array.AsReadOnly(new int[] + { + Active.Value + }); + + private static readonly global::System.Collections.Generic.Dictionary s_byName = + new global::System.Collections.Generic.Dictionary(1, global::System.StringComparer.Ordinal) + { + [Active.Name] = Active + }; + + private static readonly global::System.Collections.Generic.Dictionary s_byValue = + new global::System.Collections.Generic.Dictionary(1) + { + [Active.Value] = Active + }; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList All => s_all; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList Names => s_names; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList Values => s_values; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public const int Count = 1; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.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..94f1a72 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SameClassName_DifferentNamespaces#MyApp.Domain2.Status.g.verified.cs @@ -0,0 +1,93 @@ +//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::System.Collections.ObjectModel.ReadOnlyCollection s_all = + global::System.Array.AsReadOnly(new global::MyApp.Domain2.Status[] + { + Active + }); + + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_names = + global::System.Array.AsReadOnly(new string[] + { + Active.Name + }); + + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_values = + global::System.Array.AsReadOnly(new int[] + { + Active.Value + }); + + private static readonly global::System.Collections.Generic.Dictionary s_byName = + new global::System.Collections.Generic.Dictionary(1, global::System.StringComparer.Ordinal) + { + [Active.Name] = Active + }; + + private static readonly global::System.Collections.Generic.Dictionary s_byValue = + new global::System.Collections.Generic.Dictionary(1) + { + [Active.Value] = Active + }; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList All => s_all; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList Names => s_names; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList Values => s_values; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public const int Count = 1; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.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_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..591ee43 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SimpleEnum_GlobalNamespace#Priority.g.verified.cs @@ -0,0 +1,101 @@ +//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::System.Collections.ObjectModel.ReadOnlyCollection s_all = + global::System.Array.AsReadOnly(new global::Priority[] + { + Low, + Medium, + High + }); + + 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 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(3, 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(3) + { + [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 const int Count = 3; + + [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#MyApp.Domain.OrderStatus.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SimpleEnum_WithNamespace#MyApp.Domain.OrderStatus.g.verified.cs new file mode 100644 index 0000000..b85c817 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.SimpleEnum_WithNamespace#MyApp.Domain.OrderStatus.g.verified.cs @@ -0,0 +1,103 @@ +//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, + Paid, + Shipped + }); + + 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 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(3, 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(3) + { + [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 const int Count = 3; + + [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#MyApp.Domain.Color.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.StringValueType#MyApp.Domain.Color.g.verified.cs new file mode 100644 index 0000000..f48c64b --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.StringValueType#MyApp.Domain.Color.g.verified.cs @@ -0,0 +1,103 @@ +//HintName: MyApp.Domain.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::System.Collections.ObjectModel.ReadOnlyCollection s_all = + global::System.Array.AsReadOnly(new global::MyApp.Domain.Color[] + { + Red, + Green, + Blue + }); + + 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 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(3, 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(3) + { + [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 const int Count = 3; + + [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#MyApp.Domain.OrderStatus.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_NonPrivateConstructor#MyApp.Domain.OrderStatus.g.verified.cs new file mode 100644 index 0000000..a64d62c --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.Generator.Tests/Snapshots/GeneratorVerifyTests.Warning_NonPrivateConstructor#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#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 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" +}