1111using Microsoft . CodeAnalysis ;
1212using Microsoft . CodeAnalysis . CSharp ;
1313using Microsoft . CodeAnalysis . CSharp . Syntax ;
14+ using SourceGenerators ;
1415
1516[ assembly: System . Resources . NeutralResourcesLanguage ( "en-us" ) ]
1617
@@ -77,7 +78,12 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
7778 {
7879 RegexTree regexTree = RegexParser . Parse ( method . Pattern , method . Options | RegexOptions . Compiled , method . Culture ) ; // make sure Compiled is included to get all optimizations applied to it
7980 AnalysisResults analysis = RegexTreeAnalyzer . Analyze ( regexTree ) ;
80- return new RegexMethod ( method . DeclaringType , method . IsProperty , method . DiagnosticLocation , method . MemberName , method . Modifiers , method . NullableRegex , method . Pattern , method . Options , method . MatchTimeout , regexTree , analysis , method . CompilationData ) ;
81+ return new RegexMethod ( method . DeclaringType , method . IsProperty , method . MemberName , method . Modifiers , method . NullableRegex , method . Pattern , method . Options , method . MatchTimeout , method . CompilationData )
82+ {
83+ DiagnosticLocation = method . DiagnosticLocation ,
84+ Tree = regexTree ,
85+ Analysis = analysis ,
86+ } ;
8187 }
8288 catch ( Exception e )
8389 {
@@ -101,7 +107,12 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
101107 // We'll still output a limited implementation that just caches a new Regex(...).
102108 if ( ! SupportsCodeGeneration ( regexMethod , regexMethod . CompilationData . LanguageVersion , out string ? reason ) )
103109 {
104- return ( regexMethod , reason , Diagnostic . Create ( DiagnosticDescriptors . LimitedSourceGeneration , regexMethod . DiagnosticLocation ) , regexMethod . CompilationData ) ;
110+ return ( object ) (
111+ regexMethod ,
112+ ( string ? ) null ,
113+ ( string ? ) reason ,
114+ ImmutableEquatableArray < ( string , ImmutableEquatableArray < string > ) > . Empty ,
115+ ( Diagnostic ? ) Diagnostic . Create ( DiagnosticDescriptors . LimitedSourceGeneration , regexMethod . DiagnosticLocation ) ) ;
105116 }
106117
107118 // Generate the core logic for the regex.
@@ -112,35 +123,51 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
112123 writer . WriteLine ( ) ;
113124 EmitRegexDerivedTypeRunnerFactory ( writer , regexMethod , requiredHelpers , regexMethod . CompilationData . CheckOverflow ) ;
114125 writer . Indent -= 2 ;
115- return ( regexMethod , sw . ToString ( ) , requiredHelpers , regexMethod . CompilationData ) ;
126+
127+ // Convert helpers to an equatable form for the incremental pipeline.
128+ var equatableHelpers = requiredHelpers
129+ . OrderBy ( h => h . Key , StringComparer . Ordinal )
130+ . Select ( h => ( h . Key , h . Value . ToImmutableEquatableArray ( ) ) )
131+ . ToImmutableEquatableArray ( ) ;
132+
133+ return ( object ) ( regexMethod , ( string ? ) sw . ToString ( ) , ( string ? ) null , equatableHelpers , ( Diagnostic ? ) null ) ;
116134 } )
117135
118136 // Combine all of the generated text outputs into a single batch, then split
119137 // the source model from diagnostics so they can be emitted independently.
138+ // The Results use ImmutableEquatableArray for deep value equality, ensuring the
139+ // source output callback only fires when emitted code would actually change.
120140 . Collect ( )
121141 . Select ( static ( results , _ ) =>
122142 {
123143 ImmutableArray < Diagnostic > . Builder ? diagnostics = null ;
144+ var methods = new List < ( RegexMethod Method , string ? RunnerFactory , string ? LimitedReason , ImmutableEquatableArray < ( string Key , ImmutableEquatableArray < string > Lines ) > Helpers ) > ( ) ;
124145
125146 foreach ( object result in results )
126147 {
127148 if ( result is Diagnostic d )
128149 {
129150 ( diagnostics ??= ImmutableArray . CreateBuilder < Diagnostic > ( ) ) . Add ( d ) ;
130151 }
131- else if ( result is ValueTuple < RegexMethod , string , Diagnostic , CompilationData > limitedSupportResult )
152+ else if ( result is ValueTuple < RegexMethod , string , string , ImmutableEquatableArray < ( string , ImmutableEquatableArray < string > ) > , Diagnostic > entry )
132153 {
133- ( diagnostics ??= ImmutableArray . CreateBuilder < Diagnostic > ( ) ) . Add ( limitedSupportResult . Item3 ) ;
154+ methods . Add ( ( entry . Item1 , entry . Item2 , entry . Item3 , entry . Item4 ) ) ;
155+ if ( entry . Item5 is Diagnostic diag )
156+ {
157+ ( diagnostics ??= ImmutableArray . CreateBuilder < Diagnostic > ( ) ) . Add ( diag ) ;
158+ }
134159 }
135160 }
136161
137- return ( Results : results , Diagnostics : diagnostics ? . ToImmutable ( ) ?? ImmutableArray < Diagnostic > . Empty ) ;
162+ return (
163+ Results : methods . ToImmutableEquatableArray ( ) ,
164+ Diagnostics : diagnostics ? . ToImmutable ( ) ?? ImmutableArray < Diagnostic > . Empty ) ;
138165 } ) ;
139166
140- // Project to just the source model for code generation.
167+ // Project to just the equatable source model for code generation.
141168 context . RegisterSourceOutput ( collected . Select ( static ( t , _ ) => t . Results ) , static ( context , results ) =>
142169 {
143- if ( results . All ( static r => r is Diagnostic ) )
170+ if ( results . Count == 0 )
144171 {
145172 return ;
146173 }
@@ -170,48 +197,34 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
170197 // pair is the implementation used for the key.
171198 var emittedExpressions = new Dictionary < ( string Pattern , RegexOptions Options , int ? Timeout ) , RegexMethod > ( ) ;
172199
173- // If we have any (RegexMethod regexMethod, string generatedName, string reason, Diagnostic diagnostic), these are regexes for which we have
174- // limited support and need to simply output boilerplate.
175- // If we have any (RegexMethod regexMethod, string generatedName, string runnerFactoryImplementation, Dictionary<string, string[]> requiredHelpers),
176- // those are generated implementations to be emitted. We need to gather up their required helpers.
177- Dictionary < string , string [ ] > requiredHelpers = new ( ) ;
178- foreach ( object ? result in results )
200+ // Gather required helpers from all regex methods with full implementations.
201+ var requiredHelpers = new Dictionary < string , ImmutableEquatableArray < string > > ( ) ;
202+ foreach ( ( RegexMethod method , string ? runnerFactory , string ? limitedReason , ImmutableEquatableArray < ( string Key , ImmutableEquatableArray < string > Lines ) > helpers ) in results )
179203 {
180- RegexMethod ? regexMethod = null ;
181- if ( result is ValueTuple < RegexMethod , string , Diagnostic , CompilationData > limitedSupportResult )
204+ var key = ( method . Pattern , method . Options , method . MatchTimeout ) ;
205+ if ( emittedExpressions . TryGetValue ( key , out RegexMethod ? implementation ) )
182206 {
183- regexMethod = limitedSupportResult . Item1 ;
207+ method . IsDuplicate = true ;
208+ method . GeneratedName = implementation . GeneratedName ;
184209 }
185- else if ( result is ValueTuple < RegexMethod , string , Dictionary < string , string [ ] > , CompilationData > regexImpl )
210+ else
186211 {
187- foreach ( KeyValuePair < string , string [ ] > helper in regexImpl . Item3 )
188- {
189- if ( ! requiredHelpers . ContainsKey ( helper . Key ) )
190- {
191- requiredHelpers . Add ( helper . Key , helper . Value ) ;
192- }
193- }
194-
195- regexMethod = regexImpl . Item1 ;
212+ method . IsDuplicate = false ;
213+ method . GeneratedName = $ "{ method . MemberName } _{ id ++ } ";
214+ emittedExpressions . Add ( key , method ) ;
196215 }
197216
198- if ( regexMethod is not null )
217+ EmitRegexPartialMethod ( method , writer ) ;
218+ writer . WriteLine ( ) ;
219+
220+ foreach ( ( string helperKey , ImmutableEquatableArray < string > helperLines ) in helpers )
199221 {
200- var key = ( regexMethod . Pattern , regexMethod . Options , regexMethod . MatchTimeout ) ;
201- if ( emittedExpressions . TryGetValue ( key , out RegexMethod ? implementation ) )
202- {
203- regexMethod . IsDuplicate = true ;
204- regexMethod . GeneratedName = implementation . GeneratedName ;
205- }
206- else
222+ #pragma warning disable CA1864 // Prefer Dictionary.TryAdd -- not available on netstandard2.0
223+ if ( ! requiredHelpers . ContainsKey ( helperKey ) )
207224 {
208- regexMethod . IsDuplicate = false ;
209- regexMethod . GeneratedName = $ "{ regexMethod . MemberName } _{ id ++ } ";
210- emittedExpressions . Add ( key , regexMethod ) ;
225+ requiredHelpers . Add ( helperKey , helperLines ) ;
211226 }
212-
213- EmitRegexPartialMethod ( regexMethod , writer ) ;
214- writer . WriteLine ( ) ;
227+ #pragma warning restore CA1864
215228 }
216229 }
217230
@@ -237,21 +250,18 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
237250
238251 // Emit each Regex-derived type.
239252 writer . Indent ++ ;
240- foreach ( object ? result in results )
253+ foreach ( ( RegexMethod method , string ? runnerFactory , string ? limitedReason , _ ) in results )
241254 {
242- if ( result is ValueTuple < RegexMethod , string , Diagnostic , CompilationData > limitedSupportResult )
255+ if ( ! method . IsDuplicate )
243256 {
244- if ( ! limitedSupportResult . Item1 . IsDuplicate )
257+ if ( limitedReason is not null )
245258 {
246- EmitRegexLimitedBoilerplate ( writer , limitedSupportResult . Item1 , limitedSupportResult . Item2 , limitedSupportResult . Item4 . LanguageVersion ) ;
259+ EmitRegexLimitedBoilerplate ( writer , method , limitedReason , method . CompilationData . LanguageVersion ) ;
247260 writer . WriteLine ( ) ;
248261 }
249- }
250- else if ( result is ValueTuple < RegexMethod , string , Dictionary < string , string [ ] > , CompilationData > regexImpl )
251- {
252- if ( ! regexImpl . Item1 . IsDuplicate )
262+ else if ( runnerFactory is not null )
253263 {
254- EmitRegexDerivedImplementation ( writer , regexImpl . Item1 , regexImpl . Item2 , regexImpl . Item4 . AllowUnsafe ) ;
264+ EmitRegexDerivedImplementation ( writer , method , runnerFactory , method . CompilationData . AllowUnsafe ) ;
255265 writer . WriteLine ( ) ;
256266 }
257267 }
@@ -268,7 +278,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
268278 writer . WriteLine ( $ "{{") ;
269279 writer . Indent ++ ;
270280 bool sawFirst = false ;
271- foreach ( KeyValuePair < string , string [ ] > helper in requiredHelpers . OrderBy ( h => h . Key , StringComparer . Ordinal ) )
281+ foreach ( KeyValuePair < string , ImmutableEquatableArray < string > > helper in requiredHelpers . OrderBy ( h => h . Key , StringComparer . Ordinal ) )
272282 {
273283 if ( sawFirst )
274284 {
0 commit comments