@@ -22,29 +22,33 @@ namespace BB84.SourceGenerators;
2222[ Generator ( LanguageNames . CSharp ) ]
2323public sealed class ToStringGenerator : IIncrementalGenerator
2424{
25- private static readonly string GeneratorAttributeName = typeof ( GenerateToStringAttribute ) . FullName ;
25+ private static readonly ( string MetadataName , string FullName , string ShortName ) AttributeNames =
26+ GeneratorHelpers . GetAttributeNames < GenerateToStringAttribute > ( ) ;
2627
2728 /// <inheritdoc/>
2829 public void Initialize ( IncrementalGeneratorInitializationContext context )
29- => GeneratorHelpers . RegisterClassGenerator ( context , GeneratorAttributeName , Execute ) ;
30+ => GeneratorHelpers . RegisterClassGenerator ( context , AttributeNames . MetadataName , Execute ) ;
3031
3132 private void Execute ( SourceProductionContext context , ( ClassDeclarationSyntax ClassSyntax , SemanticModel SemanticModel ) ? input )
3233 {
3334 if ( ! GeneratorHelpers . TryCreateContext ( input , out GeneratorContext ctx ) )
3435 return ;
3536
37+ CollectionFormat collectionFormat = GetCollectionFormat ( ctx . ClassDeclaration , ctx . SemanticModel ) ;
3638 HashSet < string > excludedProperties = GeneratorHelpers . GetExcludedProperties ( ctx . ClassDeclaration , ctx . SemanticModel , nameof ( GenerateToStringAttribute ) ) ;
37- ImmutableArray < PropertyDescriptor > properties = GeneratorHelpers . GetPropertyDescriptors ( ctx . ClassSymbol , excludedProperties ) ;
39+ ImmutableArray < PropertyDescriptor > properties = GeneratorHelpers . GetPropertyDescriptors ( ctx . ClassSymbol , excludedProperties , detectCollections : true ) ;
3840
3941 SourceBuilder sb = new ( ) ;
4042
4143 sb . AppendAutoGeneratedWarning ( GeneratorNames . ToStringGeneratorFullName ) ;
4244 sb . AppendNullableEnable ( ) ;
45+ sb . AppendUsing ( NamespaceNames . System ) ;
46+ sb . AppendUsing ( NamespaceNames . SystemCollectionsGeneric ) ;
4347 sb . AppendLine ( ) ;
4448 sb . OpenNamespace ( ctx . NamespaceName ) ;
4549 sb . OpenOuterClasses ( ctx . OuterClasses ) ;
4650
47- AppendPartialClass ( sb , ctx . ClassName , ctx . Accessibility , properties ) ;
51+ AppendPartialClass ( sb , ctx . ClassName , ctx . Accessibility , properties , collectionFormat ) ;
4852
4953 sb . CloseOuterClasses ( ctx . OuterClasses ) ;
5054 sb . CloseNamespace ( ) ;
@@ -56,19 +60,41 @@ private void Execute(SourceProductionContext context, (ClassDeclarationSyntax Cl
5660 context . AddSource ( hintName , sb . ToString ( ) ) ;
5761 }
5862
59- private static void AppendPartialClass ( SourceBuilder sb , string className , string accessibility , ImmutableArray < PropertyDescriptor > properties )
63+ private static void AppendPartialClass ( SourceBuilder sb , string className , string accessibility , ImmutableArray < PropertyDescriptor > properties , CollectionFormat collectionFormat )
6064 {
65+ bool hasDictionary = collectionFormat == CollectionFormat . Elements
66+ && properties . Any ( static p => p . CollectionKind == CollectionKind . Dictionary ) ;
67+
6168 sb . OpenClass ( accessibility , className ) ;
6269 sb . AppendLine ( "/// <inheritdoc/>" ) ;
6370 sb . AppendLine ( "public override string ToString()" ) ;
6471 sb . OpenBrace ( ) ;
6572
6673 if ( properties . Length == 0 )
74+ {
6775 sb . AppendLine ( $ "return \" { className } {{ }}\" ;") ;
76+ }
6877 else
78+ {
79+ foreach ( PropertyDescriptor prop in properties )
80+ {
81+ if ( prop . CollectionKind == CollectionKind . None )
82+ continue ;
83+
84+ sb . AppendLine ( $ "string { prop . Name . ToLowerInvariant ( ) } = { BuildCollectionFormatExpression ( prop , collectionFormat ) } ;") ;
85+ }
86+
6987 sb . AppendLine ( $ "return $\" { className } {{{{ { BuildFormatString ( properties ) } }}}}\" ;") ;
88+ }
7089
7190 sb . CloseBrace ( ) ;
91+
92+ if ( hasDictionary )
93+ {
94+ sb . AppendLine ( ) ;
95+ AppendDictionaryFormatHelper ( sb ) ;
96+ }
97+
7298 sb . CloseClass ( ) ;
7399 }
74100
@@ -81,9 +107,102 @@ private static string BuildFormatString(ImmutableArray<PropertyDescriptor> prope
81107 if ( i > 0 )
82108 format . Append ( ", " ) ;
83109
84- format . Append ( $ "{ properties [ i ] . Name } = {{{properties[i].Name}}}") ;
110+ string token = properties [ i ] . CollectionKind != CollectionKind . None
111+ ? $ "{ properties [ i ] . Name . ToLowerInvariant ( ) } "
112+ : properties [ i ] . Name ;
113+
114+ format . Append ( $ "{{nameof({ properties [ i ] . Name } )}} = {{{token}}}") ;
85115 }
86116
87117 return format . ToString ( ) ;
88118 }
119+
120+ private static string BuildCollectionFormatExpression ( PropertyDescriptor prop , CollectionFormat format )
121+ {
122+ bool isImmutableArray = prop . CollectionKind == CollectionKind . ImmutableArray ;
123+ bool isArray = prop . CollectionKind == CollectionKind . Array ;
124+ bool isDictionary = prop . CollectionKind == CollectionKind . Dictionary ;
125+
126+ // ImmutableArray<T> (non-nullable struct): IsValueType = true
127+ // ImmutableArray<T>? (Nullable<ImmutableArray<T>>): IsValueType = false
128+ bool isNullableImmutableArray = isImmutableArray && ! prop . IsValueType ;
129+ bool isNonNullableImmutableArray = isImmutableArray && prop . IsValueType ;
130+
131+ // Arrays and ImmutableArrays use .Length; all other collection types use .Count
132+ bool usesLength = isArray || isImmutableArray ;
133+
134+ // Expression for the non-null collection value
135+ string directAccess = isNullableImmutableArray ? $ "{ prop . Name } .Value" : prop . Name ;
136+ string countAccess = usesLength ? $ "{ directAccess } .Length" : $ "{ directAccess } .Count";
137+
138+ // Null guard (null means no null check required for non-nullable value types)
139+ string ? nullGuard = isNonNullableImmutableArray
140+ ? null
141+ : isNullableImmutableArray
142+ ? $ "!{ prop . Name } .HasValue"
143+ : $ "{ prop . Name } == null";
144+
145+ string WrapGuard ( string valueExpr )
146+ => nullGuard is not null ? $ "{ nullGuard } ? \" null\" : { valueExpr } " : valueExpr ;
147+
148+ return format switch
149+ {
150+ CollectionFormat . Elements when isDictionary => $ "DictionaryToString({ prop . Name } )", // The DictionaryToString helper handles null internally
151+ CollectionFormat . Elements => WrapGuard ( $ "\" [\" + string.Join(\" , \" , { directAccess } ) + \" ]\" ") ,
152+ CollectionFormat . Count => WrapGuard ( $ "\" Count = \" + { countAccess } ") ,
153+ CollectionFormat . TypeAndCount => WrapGuard ( $ "{ directAccess } .GetType().Name + \" (Count = \" + { countAccess } + \" )\" ") ,
154+ _ => prop . Name ,
155+ } ;
156+ }
157+
158+ private static void AppendDictionaryFormatHelper ( SourceBuilder sb )
159+ {
160+ sb . AppendLine ( "private static string DictionaryToString<TKey, TValue>(IDictionary<TKey, TValue>? dictionary)" ) ;
161+ sb . OpenBrace ( ) ;
162+ sb . AppendLine ( "if (dictionary == null)" ) ;
163+ sb . Indent ( ) ;
164+ sb . AppendLine ( "return \" null\" ;" ) ;
165+ sb . Outdent ( ) ;
166+ sb . AppendLine ( "List<string> parts = new List<string>(dictionary.Count);" ) ;
167+ sb . AppendLine ( "foreach (KeyValuePair<TKey, TValue> kvp in dictionary)" ) ;
168+ sb . Indent ( ) ;
169+ sb . AppendLine ( "parts.Add(kvp.Key + \" : \" + kvp.Value);" ) ;
170+ sb . Outdent ( ) ;
171+ sb . AppendLine ( "return \" {\" + string.Join(\" , \" , parts) + \" }\" ;" ) ;
172+ sb . CloseBrace ( ) ;
173+ }
174+
175+ private static CollectionFormat GetCollectionFormat ( ClassDeclarationSyntax classDeclaration , SemanticModel semanticModel )
176+ {
177+ foreach ( AttributeListSyntax attributeList in classDeclaration . AttributeLists )
178+ {
179+ foreach ( AttributeSyntax attribute in attributeList . Attributes )
180+ {
181+ string name = attribute . Name . ToString ( ) ;
182+
183+ if ( name != AttributeNames . ShortName && name != AttributeNames . FullName )
184+ continue ;
185+
186+ if ( attribute . ArgumentList is null )
187+ return CollectionFormat . Count ;
188+
189+ foreach ( AttributeArgumentSyntax arg in attribute . ArgumentList . Arguments )
190+ {
191+ if ( arg . NameEquals ? . Name . Identifier . Text != nameof ( GenerateToStringAttribute . CollectionFormat ) )
192+ continue ;
193+
194+ Optional < object ? > value = semanticModel . GetConstantValue ( arg . Expression ) ;
195+
196+ if ( value . HasValue && value . Value is int intValue )
197+ return ( CollectionFormat ) intValue ;
198+
199+ break ;
200+ }
201+
202+ return CollectionFormat . Count ;
203+ }
204+ }
205+
206+ return CollectionFormat . Count ;
207+ }
89208}
0 commit comments