Skip to content

Resolve resource display names the same way on both enum metadata paths - #428

Merged
shibayan merged 7 commits into
masterfrom
enum-display-resources
Oct 21, 2026
Merged

shibayan merged 7 commits into
masterfrom
enum-display-resources

Conversation

@shibayan

@shibayan shibayan commented Oct 20, 2026 •

Copy link
Copy Markdown
Owner

Summary

Closes #409. This fills the remaining gaps in its acceptance criteria.

#420 aligned ordering and aliases between the generated enum metadata and the reflection fallback, and #425 added Native AOT verification with an enum from another assembly. Checking the criteria against the code turned up one remaining difference between the two paths, and no documentation of unsupported usage:

Acceptance criterion Before This PR
Rules for display names, ordering, and aliases are documented Ordering and aliases documented Adds display names and localization
Tests confirm both metadata paths follow the same rules [Display(ResourceType = ...)] differed (see below) Aligned by construction and tested
AOT verification includes enums from external assemblies Covered by #425 Adds resource-backed names, in the app and in another assembly
Unsupported attributes or usage patterns are clearly explained Missing New "Notes and Limitations" section

The difference

For [Display(Name = "Key", ResourceType = typeof(Strings))]:

  • Fallback: DisplayAttribute.GetName() shows the localized value of Strings.Key.
  • Generator: it emitted "Key", the resource key itself.

Approach

An enum that uses ResourceType on any member is left to the reflection fallback. The generator emits nothing for it, and the fallback resolves names through DisplayAttribute.GetName itself, so the two paths cannot differ.

An earlier revision of this PR emitted direct references to the resource property instead. Every review round found another shape that compiled differently from GetName:

  • inaccessible or ref properties, and static abstract interface members
  • [Obsolete] and [Experimental] members
  • extern aliases
  • name collisions and base-class ambiguity
  • names that C# cannot spell

That approach was dropped in favor of this one. The fallback is already AOT-safe, and this PR verifies it under Native AOT with resource names.

Changes

  • EnumMetadataGenerator: skips enums with a non-null ResourceType on any [Display], including on aliases (14 lines). ResourceType = null means no resource and is generated as before.
  • EnumMetadataRegistry.CreateFallbackMetadata: calls GetName() each time items are shown, instead of once when the metadata is created, so localized names follow later CultureInfo.CurrentUICulture changes. The remarks on EnumMetadataRegistry state the display-name rule.
  • Docs (en/ja), advanced.md:
    • "Display Names and Localization" covers how names are resolved, and that Prompt.Culture only affects Sharprompt's own messages.
    • "Notes and Limitations" covers:
      • which [Display] properties and other attributes are ignored
      • how [Flags] members behave
      • which enums use the fallback, including resource-backed ones
      • overriding the metadata with EnumMetadataRegistry.Register

Testing

  • EnumMetadataGeneratorTests:
    • Enums with ResourceType (public, internal, array type, and on an alias only) produce no generated source.
    • ResourceType = null is generated and matches the fallback.
    • Without the generator change, the four skip cases fail.
  • EnumMetadataRegistryTests: the fallback follows a UI culture change after the metadata is created. It failed before the registry change.
  • dotnet build, dotnet test (1226 passed), and dotnet format --verify-no-changes.
  • Native AOT verification on win-x64, 65/65 for both net8.0 and net10.0, with no trim or AOT warnings:
    • LocalTimeOfDay (declared in the app) and ExternalTimeOfDay (in the trimmable external library) read their names from a resource type and follow a switch to ja-JP.
    • verify.ps1 checks that neither of them got generated metadata.
  • VitePress docs build.

After merge, I'll comment on #409 with where each acceptance criterion is met.

🤖 Generated with Claude Code

For [Display(Name = ..., ResourceType = ...)], the reflection fallback
showed the localized resource string while the source generator emitted
the resource key itself, so the same enum listed different text depending
on where it was declared.

- The generator now reads the resource property in the generated text
  selector. When the property is not one DisplayAttribute can read (a
  public static string on a public type), it leaves the whole enum to
  the fallback, so the result cannot differ.
- Both paths resolve names each time items are shown instead of once,
  so they follow the current UI culture. The fallback called GetName
  when the metadata was created.
- Native AOT verification covers resource-backed names on both paths,
  including a UI culture change.
- Docs (en/ja) describe display names and localization, and which
  attributes and usage patterns are ignored or use the fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 20, 2026 18:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Resource validation still permits generated-code compilation failures and behavior that differs from the reflection fallback.

3 open issues
What changed in this PR

Aligns resource-backed enum display names between generated metadata and the reflection fallback, addressing the remaining localization requirements of #409.

Changes:

  • Resolves resource names dynamically on both metadata paths.
  • Adds culture-switching tests and Native AOT coverage.
  • Documents localization rules and unsupported usage in English and Japanese.
File Description
tests/​Sharprompt.Tests/​Binding/​EnumMetadataRegistryTests.cs Tests fallback localization after culture changes.
tests/​Sharprompt.SourceGenerator.Tests/​EnumMetadataGeneratorTests.cs Tests resource lookup parity and rejection cases.
tests/​Sharprompt.NativeAot/​verify.ps1 Verifies expected generated and fallback metadata paths.
tests/​Sharprompt.NativeAot/​Sharprompt.NativeAot.External/​ExternalTimeOfDay.cs Adds external resource-backed enum.
tests/​Sharprompt.NativeAot/​Sharprompt.NativeAot.App/​Program.cs Exercises localized enum selection under Native AOT.
tests/​Sharprompt.NativeAot/​Sharprompt.NativeAot.App/​Models.cs Adds local resource-backed enum.
tests/​Sharprompt.NativeAot/​README.md Documents expanded localization coverage.
src/​Sharprompt/​EnumMetadataRegistry.cs Resolves fallback display names on each call.
src/​Sharprompt.SourceGenerator/​EnumMetadataGenerator.cs Validates resource properties and emits direct access.
docs/​ja/​advanced.md Documents localization and limitations in Japanese.
docs/​en/​advanced.md Documents localization and limitations in English.

Review effort: Balanced

Comment thread src/Sharprompt.SourceGenerator/EnumMetadataGenerator.cs Outdated
Comment thread src/Sharprompt.SourceGenerator/EnumMetadataGenerator.cs Outdated
Comment thread src/Sharprompt.SourceGenerator/EnumMetadataGenerator.cs Outdated
The generator named the resource property directly for some shapes that
either do not compile or differ from DisplayAttribute.GetName:

- A non-named ResourceType such as typeof(string[]) was treated as no
  resource type, so the literal name was emitted while GetName throws.
- [Obsolete] properties: CS0619, or a warning that fails a
  warnings-as-errors build.
- ref and ref readonly returns: reflection reports string&, which
  GetName rejects.
- static abstract or virtual interface properties: CS8926.

These enums now use the fallback. A null ResourceType still means no
resource.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 20, 2026 18:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Generated references to obsolete resource types can break otherwise valid warnings-as-errors builds.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

Medium severity Skip obsolete resource types to avoid generated warnings

src/​Sharprompt.SourceGenerator/​EnumMetadataGenerator.cs:245

Also leave obsolete resource types and containing types to the fallback. A public [Obsolete("Old")] resource class with an ordinary public static string property passes this check. If the consumer suppresses CS0618 locally around its [Display(ResourceType = typeof(Strings))] declaration, the generated Strings.Low access introduces the warning outside that suppression and breaks warnings-as-errors builds. Reflection can still read the property. Check type-level obsolescence in this loop, as already done for properties, and add a regression test with a locally suppressed attribute declaration.

3 resolved since last review

Review effort: Balanced

An [Obsolete] resource type, or a type containing it, passed the check
for directly accessible resource properties. The generated access then
warned (or failed with CS0619) even where the consumer suppressed the
warning around its own [Display] attribute, which breaks
warnings-as-errors builds. Check every type in the containing chain, as
properties already are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 21, 2026 02:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Resource validation still permits consumer build failures and differences between generated and reflection-based behavior.

2 open issues
1 previously missed issue

In code that hasn't changed since last review

Medium severity Handle inherited properties that cause ambiguous resource lookups

src/​Sharprompt.SourceGenerator/​EnumMetadataGenerator.cs:252

This lookup checks only declared properties, but reflection's name-only lookup also includes inherited public instance properties. If a base class declares public int Low { get; } and the resource class declares public new static string Low => "Low", generation succeeds while DisplayAttribute.GetName() throws AmbiguousMatchException. Leave potentially ambiguous inherited-property cases to the fallback and add a regression test for this shape.

Review effort: Balanced

Comment thread src/Sharprompt.SourceGenerator/EnumMetadataGenerator.cs Outdated
Comment thread src/Sharprompt.SourceGenerator/EnumMetadataGenerator.cs Outdated
- The generated file names the resource type through global::, which
  cannot reach an assembly referenced only through an extern alias, so
  typeof(res::ResourceLib.Strings) produced code that did not compile.
- [Experimental] on the resource property, its type, or its assembly or
  module reports a diagnostic (such as EXP001) on the generated direct
  access, like [Obsolete] does.

These enums now use the reflection fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 21, 2026 03:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Resource-name collisions and inherited-property ambiguity can still break builds or produce different behavior between metadata paths.

1 open issue
1 previously missed issue

In code that hasn't changed since last review

Medium severity Fallback on inherited property name collisions to avoid ambiguous reflection

src/​Sharprompt.SourceGenerator/​EnumMetadataGenerator.cs:260

This lookup only examines declared properties. If a public base class has public int Low => 1 and its public subclass declares public new static string Low => "Low", the generated selector returns the string. However, DisplayAttribute.GetName() throws AmbiguousMatchException because reflection finds both property signatures. Moving the enum between assemblies therefore still changes its behavior. Conservatively leave inherited public instance-property name collisions to the fallback, and add a regression test for this case.

2 resolved since last review

Review effort: Balanced

Comment thread src/Sharprompt.SourceGenerator/EnumMetadataGenerator.cs Outdated
- global::ResourceLib.Strings did not always name the resource type: the
  same name in another globally referenced assembly made it ambiguous
  (CS0433), and one declared in the consumer shadowed it. The generator
  now requires the resource type to be the only accessible type of that
  metadata name reachable through global::.
- A public property of the same name on a base type makes reflection
  ambiguous, so DisplayAttribute.GetName throws, while the generated
  access read the hiding property. Such enums now use the fallback.

GeneratorTestHelper.CreateLibraryReference takes an assembly name for
tests that need two libraries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 21, 2026 03:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Two remaining resource-property validation gaps can make generated code break otherwise valid consumer builds.

2 open issues
1 resolved since last review

Review effort: Balanced

Comment thread src/Sharprompt.SourceGenerator/EnumMetadataGenerator.cs Outdated
Comment thread src/Sharprompt.SourceGenerator/EnumMetadataGenerator.cs Outdated
…o the fallback

- Other languages, such as F#, can declare property names that are not C#
  identifiers ("Low priority"). Reflection reads them, but the generated
  access did not compile. Only valid identifiers (escaped with @ when they
  are keywords) are referenced directly now.
- [Obsolete] or [Experimental] on the getter alone was not checked, so the
  generated access failed with CS0619 or an EXP diagnostic.

The identifier test builds the resource assembly with
PersistedAssemblyBuilder, so it runs on .NET 9 and later.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 21, 2026 03:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The generator’s resource-name binding and reflection-parity safeguards warrant final human review.

No open issues 🎉

2 resolved since last review

Review effort: Balanced

Reading a [Display(ResourceType = ...)] property from generated code kept
needing special cases to match DisplayAttribute.GetName: inaccessible or
unusual property shapes, obsolete and experimental members, extern
aliases, name collisions, and names C# cannot spell. Instead of
approximating that lookup, the generator now emits nothing for an enum
that uses ResourceType on any member, and the fallback resolves its names
through GetName itself, so both paths agree by construction. The fallback
is AOT-safe, and the Native AOT verification covers such an enum declared
in the app and in another assembly.

This reverts the resource property handling added earlier in this branch.
ResourceType = null still means no resource and is generated as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 21, 2026 07:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No blocking issues remain in the focused implementation, with regression and Native AOT coverage for the changed behavior.

No open issues 🎉

Review effort: Balanced

@shibayan
shibayan merged commit b7b11af into master Oct 21, 2026
10 checks passed
@shibayan
shibayan deleted the enum-display-resources branch October 21, 2026 07:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Align enum behavior between generated metadata and the fallback

2 participants