@@ -23,6 +23,11 @@ namespace ExpressiveSharp.Generator.Interpretation;
2323/// </remarks>
2424static internal class ExpressivePropertyInterpreter
2525{
26+ private static readonly SymbolDisplayFormat FullyQualifiedNullableFormat =
27+ SymbolDisplayFormat . FullyQualifiedFormat . WithMiscellaneousOptions (
28+ SymbolDisplayFormat . FullyQualifiedFormat . MiscellaneousOptions
29+ | SymbolDisplayMiscellaneousOptions . IncludeNullableReferenceTypeModifier ) ;
30+
2631 public static ( ExpressiveDescriptor Descriptor , SynthesizedPropertySpec Spec ) ? GetDescriptor (
2732 SemanticModel semanticModel ,
2833 PropertyDeclarationSyntax stubProperty ,
@@ -107,8 +112,9 @@ public static (ExpressiveDescriptor Descriptor, SynthesizedPropertySpec Spec)? G
107112 // Synthesis spec for the partial-class emitter.
108113 var returnType = stubSymbol . Type ;
109114 var useTernary = IsNullablePropertyType ( returnType ) ;
110- var propertyTypeFqn = returnType . ToDisplayString ( SymbolDisplayFormat . FullyQualifiedFormat ) ;
115+ var propertyTypeFqn = returnType . ToDisplayString ( FullyQualifiedNullableFormat ) ;
111116 var backingFieldTypeFqn = useTernary ? propertyTypeFqn : MakeNullableTypeFqn ( returnType ) ;
117+ var ( backingFieldName , hasValueFlagName ) = ChooseBackingNames ( containingType , targetName ! , useTernary ) ;
112118
113119 var spec = new SynthesizedPropertySpec
114120 {
@@ -118,11 +124,14 @@ public static (ExpressiveDescriptor Descriptor, SynthesizedPropertySpec Spec)? G
118124 StubIsMethod = false , // rule 1 enforces property stub
119125 UseTernaryShape = useTernary ,
120126 BackingFieldTypeFqn = backingFieldTypeFqn ,
127+ BackingFieldName = backingFieldName ,
128+ HasValueFlagName = hasValueFlagName ,
121129 ContainingTypeName = containingType . Name ,
122130 ContainingTypeNamespace = containingType . ContainingNamespace . IsGlobalNamespace
123131 ? null
124132 : containingType . ContainingNamespace . ToDisplayString ( ) ,
125133 ContainingTypePath = GetNestedInClassPath ( containingType ) . ToList ( ) ,
134+ ContainingTypeKeywords = GetNestedInClassKeywords ( containingType ) . ToList ( ) ,
126135 ContainingTypeKeyword = GetTypeKeyword ( containingType ) ,
127136 } ;
128137
@@ -158,8 +167,9 @@ public static (ExpressiveDescriptor Descriptor, SynthesizedPropertySpec Spec)? G
158167
159168 var returnType = stubSymbol . Type ;
160169 var useTernary = IsNullablePropertyType ( returnType ) ;
161- var propertyTypeFqn = returnType . ToDisplayString ( SymbolDisplayFormat . FullyQualifiedFormat ) ;
170+ var propertyTypeFqn = returnType . ToDisplayString ( FullyQualifiedNullableFormat ) ;
162171 var backingFieldTypeFqn = useTernary ? propertyTypeFqn : MakeNullableTypeFqn ( returnType ) ;
172+ var ( backingFieldName , hasValueFlagName ) = ChooseBackingNames ( containingType , targetName ! , useTernary ) ;
163173
164174 return new SynthesizedPropertySpec
165175 {
@@ -169,11 +179,14 @@ public static (ExpressiveDescriptor Descriptor, SynthesizedPropertySpec Spec)? G
169179 StubIsMethod = false ,
170180 UseTernaryShape = useTernary ,
171181 BackingFieldTypeFqn = backingFieldTypeFqn ,
182+ BackingFieldName = backingFieldName ,
183+ HasValueFlagName = hasValueFlagName ,
172184 ContainingTypeName = containingType . Name ,
173185 ContainingTypeNamespace = containingType . ContainingNamespace . IsGlobalNamespace
174186 ? null
175187 : containingType . ContainingNamespace . ToDisplayString ( ) ,
176188 ContainingTypePath = GetNestedInClassPath ( containingType ) . ToList ( ) ,
189+ ContainingTypeKeywords = GetNestedInClassKeywords ( containingType ) . ToList ( ) ,
177190 ContainingTypeKeyword = GetTypeKeyword ( containingType ) ,
178191 } ;
179192 }
@@ -299,6 +312,40 @@ private static IEnumerable<string> GetNestedInClassPath(ITypeSymbol typeSymbol)
299312 yield return typeSymbol . Name ;
300313 }
301314
315+ private static IEnumerable < string > GetNestedInClassKeywords ( INamedTypeSymbol typeSymbol )
316+ {
317+ if ( typeSymbol . ContainingType is not null )
318+ {
319+ foreach ( var keyword in GetNestedInClassKeywords ( typeSymbol . ContainingType ) )
320+ yield return keyword ;
321+ }
322+ yield return GetTypeKeyword ( typeSymbol ) ;
323+ }
324+
325+ private static ( string BackingFieldName , string HasValueFlagName ) ChooseBackingNames (
326+ INamedTypeSymbol containingType , string propertyName , bool useTernary )
327+ {
328+ var existing = new HashSet < string > ( containingType . GetMembers ( ) . Select ( m => m . Name ) ) ;
329+ var camel = char . ToLowerInvariant ( propertyName [ 0 ] ) + propertyName . Substring ( 1 ) ;
330+ var basePrefix = "_" + camel ;
331+
332+ // Single suffix shared across the pair so collision-resolved names stay related
333+ // (`_foo2` / `_foo2HasValue`, never `_foo2` / `_foo3HasValue`).
334+ var suffix = 0 ;
335+ while ( true )
336+ {
337+ var suffixStr = suffix == 0 ? "" : suffix . ToString ( System . Globalization . CultureInfo . InvariantCulture ) ;
338+ var fieldName = basePrefix + suffixStr ;
339+ var flagName = basePrefix + suffixStr + "HasValue" ;
340+ var fieldOk = ! existing . Contains ( fieldName ) ;
341+ var flagOk = ! useTernary || ! existing . Contains ( flagName ) ;
342+ if ( fieldOk && flagOk )
343+ return ( fieldName , useTernary ? flagName : "" ) ;
344+ // First retry uses suffix 2 (skip 1 — `_foo1` reads worse than `_foo2`).
345+ suffix = suffix == 0 ? 2 : suffix + 1 ;
346+ }
347+ }
348+
302349 private static INamedTypeSymbol ? FindInheritedMember ( INamedTypeSymbol type , string memberName )
303350 {
304351 // Walk the base chain — System.Object itself is not interesting for user-defined collisions
0 commit comments