55using System . Text ;
66
77using Microsoft . CodeAnalysis ;
8- using Microsoft . CodeAnalysis . CSharp ;
98using Microsoft . CodeAnalysis . CSharp . Syntax ;
9+ using Microsoft . CodeAnalysis . Text ;
10+
1011
1112namespace SourceGenerators
1213{
1314 [ Generator ]
14- public class ToExprGenerator : ISourceGenerator
15+ public class ToExprGenerator : IIncrementalGenerator
1516 {
16- public void Initialize ( GeneratorInitializationContext context )
17+ public void Initialize ( IncrementalGeneratorInitializationContext context )
1718 {
18- #if DEBUG1
19+ #if DEBUG1
1920 if ( ! Debugger . IsAttached )
2021 {
2122 Debugger . Launch ( ) ;
2223 }
23- #endif
24+ #endif
2425
25- context . RegisterForPostInitialization ( i => i . AddSource ( "ToExprAttribute.g.cs" ,
26- """
27- // <auto-generated/>
28- #pragma warning disable
29- #nullable enable annotations
26+ // Generate the attribute definition
27+ context . RegisterPostInitializationOutput ( ctx =>
28+ {
29+ ctx . AddSource ( "ToExprAttribute.g.cs" ,
30+ """
31+ // <auto-generated/>
32+ #pragma warning disable
33+ #nullable enable annotations
3034
31- using System;
35+ using System;
3236
33- namespace Linq.Expressions.Deconstruct
34- {
35- [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
36- [System.Diagnostics.Conditional("ToExprGenerator_DEBUG")]
37- sealed class ToExprAttribute : Attribute
37+ namespace Linq.Expressions.Deconstruct
3838 {
39- public string PropertyName { get; set; }
40- public bool IsNullable { get; set; }
39+ [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
40+ [System.Diagnostics.Conditional("ToExprGenerator_DEBUG")]
41+ sealed class ToExprAttribute : Attribute
42+ {
43+ public string PropertyName { get; set; }
44+ public bool IsNullable { get; set; }
45+ }
4146 }
42- }
43-
44- """ ) ) ;
47+ """ ) ;
48+ } ) ;
49+
50+ // Collect all fields with the target attribute
51+ //
52+ var fieldsWithAttribute = context . SyntaxProvider
53+ . CreateSyntaxProvider (
54+ static ( node , _ ) => node is FieldDeclarationSyntax { AttributeLists . Count : > 0 } ,
55+ static ( ctx , _ ) => GetFieldSymbolWithAttribute ( ctx ) )
56+ . Where ( static field => field is not null )
57+ . Select ( static ( field , _ ) => field ! )
58+ . Collect ( ) ;
59+
60+ // Main code generation step
61+ //
62+ context . RegisterSourceOutput ( fieldsWithAttribute , ( spc , fields ) =>
63+ {
64+ var grouped = fields . GroupBy ( f => f . ContainingType , SymbolEqualityComparer . Default ) ;
4565
46- context . RegisterForSyntaxNotifications ( ( ) => new SyntaxReceiver ( ) ) ;
66+ foreach ( var group in grouped )
67+ {
68+ var classSource = ProcessClass ( ( INamedTypeSymbol ) group . Key , group . ToList ( ) , spc ) ;
69+ spc . AddSource ( $ "{ group . Key . Name } .ToExpr.g.cs", SourceText . From ( classSource , Encoding . UTF8 ) ) ;
70+ }
71+ } ) ;
4772 }
4873
49- public void Execute ( GeneratorExecutionContext context )
74+ static IFieldSymbol GetFieldSymbolWithAttribute ( GeneratorSyntaxContext context )
5075 {
51- if ( context . SyntaxContextReceiver is not SyntaxReceiver receiver )
52- return ;
76+ if ( context . Node is not FieldDeclarationSyntax fieldDeclaration )
77+ return null ;
5378
54- var attributeSymbol = context . Compilation . GetTypeByMetadataName ( "Linq.Expressions.Deconstruct.ToExprAttribute" ) ;
55-
56- foreach ( var group in receiver . Fields . GroupBy < IFieldSymbol , INamedTypeSymbol > ( f => f . ContainingType , SymbolEqualityComparer . Default ) )
79+ foreach ( var variable in fieldDeclaration . Declaration . Variables )
5780 {
58- var classSource = ProcessClass ( group . Key , group . ToList ( ) , attributeSymbol , context ) ;
59- context . AddSource ( $ "{ group . Key . Name } .ToExpr.g.cs", classSource ) ;
81+ if ( context . SemanticModel . GetDeclaredSymbol ( variable ) is not IFieldSymbol symbol )
82+ continue ;
83+
84+ if ( symbol . GetAttributes ( ) . Any ( attr => attr . AttributeClass ? . ToDisplayString ( ) == "Linq.Expressions.Deconstruct.ToExprAttribute" ) )
85+ return symbol ;
6086 }
87+
88+ return null ;
6189 }
6290
63- string ProcessClass ( INamedTypeSymbol classSymbol , List < IFieldSymbol > fields , ISymbol attributeSymbol , GeneratorExecutionContext context )
91+ static string ProcessClass ( INamedTypeSymbol classSymbol , List < IFieldSymbol > fields , SourceProductionContext context )
6492 {
6593// if (!classSymbol.ContainingSymbol.Equals(classSymbol.ContainingNamespace, SymbolEqualityComparer.Default))
6694// {
@@ -77,8 +105,6 @@ string ProcessClass(INamedTypeSymbol classSymbol, List<IFieldSymbol> fields, ISy
77105
78106 using System;
79107
80- #nullable enable
81-
82108 namespace {{ namespaceName }}
83109 {
84110 partial class Expr
@@ -88,6 +114,8 @@ partial class {{classSymbol.Name}}
88114
89115 """ ) ;
90116
117+ var attributeSymbol = fields [ 0 ] . GetAttributes ( ) . First ( a => a . AttributeClass ? . Name == "ToExprAttribute" ) . AttributeClass ! ;
118+
91119 foreach ( var fieldSymbol in fields )
92120 ProcessField ( source , classSymbol , fieldSymbol , attributeSymbol ) ;
93121
@@ -100,18 +128,18 @@ partial class {{classSymbol.Name}}
100128 return source . ToString ( ) ;
101129 }
102130
103- void ProcessField ( StringBuilder source , INamedTypeSymbol classSymbol , IFieldSymbol fieldSymbol , ISymbol attributeSymbol )
131+ static void ProcessField ( StringBuilder source , INamedTypeSymbol classSymbol , IFieldSymbol fieldSymbol , INamedTypeSymbol attributeSymbol )
104132 {
105133 var fieldName = fieldSymbol . Name ;
106134 var fieldType = fieldSymbol . Type . ToDisplayString ( ) ;
107135
108- var attributeData = fieldSymbol . GetAttributes ( ) . Single ( ad => ad . AttributeClass ! . Equals ( attributeSymbol , SymbolEqualityComparer . Default ) ) ;
109- var overridenName = attributeData . NamedArguments . SingleOrDefault ( kvp => kvp . Key == "PropertyName" ) . Value ;
110- var isNullable = attributeData . NamedArguments . SingleOrDefault ( kvp => kvp . Key == "IsNullable" ) . Value ;
136+ var attributeData = fieldSymbol . GetAttributes ( ) . Single ( ad => SymbolEqualityComparer . Default . Equals ( ad . AttributeClass , attributeSymbol ) ) ;
137+ var overriddenName = attributeData . NamedArguments . FirstOrDefault ( kvp => kvp . Key == "PropertyName" ) . Value ;
138+ var isNullable = attributeData . NamedArguments . FirstOrDefault ( kvp => kvp . Key == "IsNullable" ) . Value ;
111139
112140 string propertyName ;
113141
114- if ( overridenName . IsNull )
142+ if ( overriddenName . IsNull )
115143 {
116144 propertyName = fieldName . TrimStart ( '_' ) ;
117145
@@ -120,26 +148,29 @@ void ProcessField(StringBuilder source, INamedTypeSymbol classSymbol, IFieldSymb
120148 }
121149 else
122150 {
123- propertyName = overridenName . Value ! . ToString ( ) ;
151+ propertyName = overriddenName . Value ? . ToString ( ) ?? "" ;
124152 }
125153
154+ // Skip if name is invalid or clashes with other members
155+ //
126156 if ( propertyName . Length == 0 || propertyName == fieldName || classSymbol . MemberNames . Contains ( propertyName ) )
127157 {
128158 if ( fieldSymbol . Locations [ 0 ] is { Kind : LocationKind . SourceFile } location )
129159 {
130- var ls = location . GetMappedLineSpan ( ) ;
131- source
132- . AppendLine ( $ "#line ({ ls . Span . Start . Line + 1 } ,{ ls . Span . Start . Character } )-({ ls . Span . End . Line + 1 } ,{ ls . Span . End . Character } ) \" { ls . Path } \" ")
133- ;
160+ var ls = location . GetLineSpan ( ) ;
161+ source . AppendLine ( $ "#line ({ ls . StartLinePosition . Line + 1 } ,{ ls . StartLinePosition . Character } )-({ ls . EndLinePosition . Line + 1 } ,{ ls . EndLinePosition . Character } ) \" { ls . Path } \" ") ;
134162 }
135163
136164 source
137165 . AppendLine ( $ "#error Generator failed on '{ fieldName } '.")
138166 . AppendLine ( "#line default" )
139167 ;
168+
140169 return ;
141170 }
142171
172+ // If IsNullable is false or unset, trim the trailing '?'
173+ //
143174 if ( isNullable . IsNull || isNullable . Value is false )
144175 fieldType = fieldType . TrimEnd ( '?' ) ;
145176
@@ -149,25 +180,5 @@ void ProcessField(StringBuilder source, INamedTypeSymbol classSymbol, IFieldSymb
149180
150181 """ ) ;
151182 }
152-
153- public class SyntaxReceiver : ISyntaxContextReceiver
154- {
155- public List < IFieldSymbol > Fields { get ; } = new ( ) ;
156-
157- public void OnVisitSyntaxNode ( GeneratorSyntaxContext context )
158- {
159- if ( context . Node is FieldDeclarationSyntax { AttributeLists : [ _, ..] } field )
160- {
161- foreach ( var variable in field . Declaration . Variables )
162- {
163- var fieldSymbol = context . SemanticModel . GetDeclaredSymbol ( variable ) as IFieldSymbol ;
164- var attributes = fieldSymbol ! . GetAttributes ( ) ;
165-
166- if ( attributes . Any ( ad => ad . AttributeClass ! . ToDisplayString ( ) == "Linq.Expressions.Deconstruct.ToExprAttribute" ) )
167- Fields . Add ( fieldSymbol ) ;
168- }
169- }
170- }
171- }
172183 }
173184}
0 commit comments