You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Resolve resource display names the same way on both enum metadata paths - #428
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
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.
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>
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.
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>
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.
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>
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.
- 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>
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.
- 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>
…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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
[Display(ResourceType = ...)]differed (see below)The difference
For
[Display(Name = "Key", ResourceType = typeof(Strings))]:DisplayAttribute.GetName()shows the localized value ofStrings.Key."Key", the resource key itself.Approach
An enum that uses
ResourceTypeon any member is left to the reflection fallback. The generator emits nothing for it, and the fallback resolves names throughDisplayAttribute.GetNameitself, 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:refproperties, and static abstract interface members[Obsolete]and[Experimental]membersThat 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-nullResourceTypeon any[Display], including on aliases (14 lines).ResourceType = nullmeans no resource and is generated as before.EnumMetadataRegistry.CreateFallbackMetadata: callsGetName()each time items are shown, instead of once when the metadata is created, so localized names follow laterCultureInfo.CurrentUICulturechanges. The remarks onEnumMetadataRegistrystate the display-name rule.advanced.md:Prompt.Cultureonly affects Sharprompt's own messages.[Display]properties and other attributes are ignored[Flags]members behaveEnumMetadataRegistry.RegisterTesting
EnumMetadataGeneratorTests:ResourceType(public, internal, array type, and on an alias only) produce no generated source.ResourceType = nullis generated and matches the fallback.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), anddotnet format --verify-no-changes.LocalTimeOfDay(declared in the app) andExternalTimeOfDay(in the trimmable external library) read their names from a resource type and follow a switch to ja-JP.verify.ps1checks that neither of them got generated metadata.After merge, I'll comment on #409 with where each acceptance criterion is met.
🤖 Generated with Claude Code