22// The .NET Foundation licenses this file to you under the MIT license.
33
44using System ;
5+ using System . CodeDom . Compiler ;
56using System . Collections . Generic ;
67using System . Collections . Immutable ;
78using System . Diagnostics ;
89using System . Diagnostics . CodeAnalysis ;
910using System . Diagnostics . Tracing ;
11+ using System . IO ;
1012using System . Linq ;
1113using System . Runtime . CompilerServices ;
1214using System . Text ;
@@ -23,65 +25,239 @@ namespace System.Text.RegularExpressions.Generator
2325 [ Generator ( LanguageNames . CSharp ) ]
2426 public partial class RegexGenerator : IIncrementalGenerator
2527 {
26- public void Initialize ( IncrementalGeneratorInitializationContext context )
28+ /// <summary>Name of the type emitted to contain helpers used by the generated code.</summary>
29+ private const string HelpersTypeName = "Utilities" ;
30+ /// <summary>Namespace containing all the generated code.</summary>
31+ private const string GeneratedNamespace = "System.Text.RegularExpressions.Generated" ;
32+ /// <summary>Code for a [GeneratedCode] attribute to put on the top-level generated members.</summary>
33+ private static readonly string s_generatedCodeAttribute = $ "GeneratedCodeAttribute(\" { typeof ( RegexGenerator ) . Assembly . GetName ( ) . Name } \" , \" { typeof ( RegexGenerator ) . Assembly . GetName ( ) . Version } \" )";
34+ /// <summary>Header comments and usings to include at the top of every generated file.</summary>
35+ private static readonly string [ ] s_headers = new string [ ]
2736 {
28- // To avoid invalidating the generator's output when anything from the compilation
29- // changes, we will extract from it the only thing we care about: whether unsafe
30- // code is allowed.
31- IncrementalValueProvider < bool > allowUnsafeProvider =
32- context . CompilationProvider
33- . Select ( ( x , _ ) => x . Options is CSharpCompilationOptions { AllowUnsafe : true } ) ;
37+ " // <auto-generated/>" ,
38+ "#nullable enable" ,
39+ "#pragma warning disable CS0162 // Unreachable code" ,
40+ "#pragma warning disable CS0164 // Unreferenced label" ,
41+ "#pragma warning disable CS0219 // Variable assigned but never used" ,
42+ } ;
3443
35- // Contains one entry per regex method, either the generated code for that regex method,
36- // a diagnostic to fail with, or null if no action should be taken for that regex.
44+ public void Initialize ( IncrementalGeneratorInitializationContext context )
45+ {
46+ // Produces one entry per generated regex. This may be:
47+ // - Diagnostic in the case of a failure that should end the compilation
48+ // - (RegexMethod regexMethod, string runnerFactoryImplementation, Dictionary<string, string[]> requiredHelpers) in the case of valid regex
49+ // - (RegexMethod regexMethod, string reason, Diagnostic diagnostic) in the case of a limited-support regex
3750 IncrementalValueProvider < ImmutableArray < object ? > > codeOrDiagnostics =
3851 context . SyntaxProvider
3952
40- // Find all MethodDeclarationSyntax nodes attributed with RegexGenerator and gather the required information
53+ // Find all MethodDeclarationSyntax nodes attributed with RegexGenerator and gather the required information.
4154 . CreateSyntaxProvider ( IsSyntaxTargetForGeneration , GetSemanticTargetForGeneration )
4255 . Where ( static m => m is not null )
4356
44- // Pair each with whether unsafe code is allowed
45- . Combine ( allowUnsafeProvider )
46-
47- // Get the resulting code string or error Diagnostic for
48- // each MethodDeclarationSyntax/allow-unsafe-blocks pair
57+ // Generate the RunnerFactory for each regex, if possible. This is where the bulk of the implementation occurs.
4958 . Select ( ( state , _ ) =>
5059 {
51- Debug . Assert ( state . Left is not null ) ;
52- return state . Left is RegexType regexType ? EmitRegexType ( regexType , state . Right ) : state . Left ;
60+ if ( state is not RegexMethod regexMethod )
61+ {
62+ Debug . Assert ( state is Diagnostic ) ;
63+ return state ;
64+ }
65+
66+ // If we're unable to generate a full implementation for this regex, report a diagnostic.
67+ // We'll still output a limited implementation that just caches a new Regex(...).
68+ if ( ! regexMethod . Tree . Root . SupportsCompilation ( out string ? reason ) )
69+ {
70+ return ( regexMethod , reason , Diagnostic . Create ( DiagnosticDescriptors . LimitedSourceGeneration , regexMethod . MethodSyntax . GetLocation ( ) ) ) ;
71+ }
72+
73+ // Generate the core logic for the regex.
74+ Dictionary < string , string [ ] > requiredHelpers = new ( ) ;
75+ var sw = new StringWriter ( ) ;
76+ var writer = new IndentedTextWriter ( sw ) ;
77+ writer . Indent += 3 ;
78+ writer . WriteLine ( ) ;
79+ EmitRegexDerivedTypeRunnerFactory ( writer , regexMethod , requiredHelpers ) ;
80+ writer . Indent -= 3 ;
81+ return ( regexMethod , sw . ToString ( ) , requiredHelpers ) ;
5382 } )
5483 . Collect ( ) ;
5584
85+ // To avoid invalidating every regex's output when anything from the compilation changes,
86+ // we extract from it the only things we care about: whether unsafe code is allowed,
87+ // and a name based on the assembly's name, and only that information is then fed into
88+ // RegisterSourceOutput along with all of the cached generated data from each regex.
89+ IncrementalValueProvider < ( bool AllowUnsafe , string ? AssemblyName ) > compilationDataProvider =
90+ context . CompilationProvider
91+ . Select ( ( x , _ ) => ( x . Options is CSharpCompilationOptions { AllowUnsafe : true } , x . AssemblyName ) ) ;
92+
5693 // When there something to output, take all the generated strings and concatenate them to output,
5794 // and raise all of the created diagnostics.
58- context . RegisterSourceOutput ( codeOrDiagnostics , static ( context , results ) =>
95+ context . RegisterSourceOutput ( codeOrDiagnostics . Combine ( compilationDataProvider ) , static ( context , compilationDataAndResults ) =>
5996 {
60- var code = new List < string > ( s_headers . Length + results . Length ) ;
61-
62- // Add file header and required usings
63- code . AddRange ( s_headers ) ;
97+ ImmutableArray < object ? > results = compilationDataAndResults . Left ;
6498
99+ // Report any top-level diagnostics.
100+ bool allFailures = true ;
65101 foreach ( object ? result in results )
66102 {
67- switch ( result )
103+ if ( result is Diagnostic d )
68104 {
69- case Diagnostic d :
70- context . ReportDiagnostic ( d ) ;
71- break ;
105+ context . ReportDiagnostic ( d ) ;
106+ }
107+ else
108+ {
109+ allFailures = false ;
110+ }
111+ }
112+ if ( allFailures )
113+ {
114+ return ;
115+ }
116+
117+ // At this point we'll be emitting code. Create a writer to hold it all.
118+ var sw = new StringWriter ( ) ;
119+ IndentedTextWriter writer = new ( sw ) ;
120+
121+ // Add file headers and required usings.
122+ foreach ( string header in s_headers )
123+ {
124+ writer . WriteLine ( header ) ;
125+ }
126+ writer . WriteLine ( ) ;
72127
73- case ValueTuple < string , ImmutableArray < Diagnostic > > t :
74- code . Add ( t . Item1 ) ;
75- foreach ( Diagnostic d in t . Item2 )
128+ // For every generated type, we give it an incrementally increasing ID, in order to create
129+ // unique type names even in situations where method names were the same, while also keeping
130+ // the type names short. Note that this is why we only generate the RunnerFactory implementations
131+ // earlier in the pipeline... we want to avoid generating code that relies on the class names
132+ // until we're able to iterate through them linearly keeping track of a deterministic ID
133+ // used to name them. The boilerplate code generation that happens here is minimal when compared to
134+ // the work required to generate the actual matching code for the regex.
135+ int id = 0 ;
136+ string generatedClassName = $ "__{ ComputeStringHash ( compilationDataAndResults . Right . AssemblyName ?? "" ) : x} ";
137+
138+ // If we have any (RegexMethod regexMethod, string generatedName, string reason, Diagnostic diagnostic), these are regexes for which we have
139+ // limited support and need to simply output boilerplate. We need to emit their diagnostics.
140+ // If we have any (RegexMethod regexMethod, string generatedName, string runnerFactoryImplementation, Dictionary<string, string[]> requiredHelpers),
141+ // those are generated implementations to be emitted. We need to gather up their required helpers.
142+ Dictionary < string , string [ ] > requiredHelpers = new ( ) ;
143+ foreach ( object ? result in results )
144+ {
145+ RegexMethod ? regexMethod = null ;
146+ if ( result is ValueTuple < RegexMethod , string , Diagnostic > limitedSupportResult )
147+ {
148+ context . ReportDiagnostic ( limitedSupportResult . Item3 ) ;
149+ regexMethod = limitedSupportResult . Item1 ;
150+ }
151+ else if ( result is ValueTuple < RegexMethod , string , Dictionary < string , string [ ] > > regexImpl )
152+ {
153+ foreach ( KeyValuePair < string , string [ ] > helper in regexImpl . Item3 )
154+ {
155+ if ( ! requiredHelpers . ContainsKey ( helper . Key ) )
76156 {
77- context . ReportDiagnostic ( d ) ;
157+ requiredHelpers . Add ( helper . Key , helper . Value ) ;
78158 }
79- break ;
159+ }
160+
161+ regexMethod = regexImpl . Item1 ;
162+ }
163+
164+ if ( regexMethod is not null )
165+ {
166+ regexMethod . GeneratedId = id ++ ;
167+ EmitRegexPartialMethod ( regexMethod , writer , generatedClassName ) ;
168+ writer . WriteLine ( ) ;
169+ }
170+ }
171+
172+ // At this point we've emitted all the partial method definitions, but we still need to emit the actual regex-derived implementations.
173+ // These are all emitted inside of our generated class.
174+ // TODO https://github.com/dotnet/csharplang/issues/5529:
175+ // When C# provides a mechanism for shielding generated code from the rest of the project, it should be employed
176+ // here for the generated class. At that point, the generated class wrapper can be removed, and all of the types
177+ // generated inside of it (one for each regex as well as the helpers type) should be shielded.
178+
179+ writer . WriteLine ( $ "namespace { GeneratedNamespace } ") ;
180+ writer . WriteLine ( $ "{{") ;
181+
182+ // We emit usings here now that we're inside of a namespace block and are no longer emitting code into
183+ // a user's partial type. We can now rely on binding rules mapping to these usings and don't need to
184+ // use global-qualified names for the rest of the implementation.
185+ writer . WriteLine ( $ " using System;") ;
186+ writer . WriteLine ( $ " using System.CodeDom.Compiler;") ;
187+ writer . WriteLine ( $ " using System.Collections;") ;
188+ writer . WriteLine ( $ " using System.ComponentModel;") ;
189+ writer . WriteLine ( $ " using System.Globalization;") ;
190+ writer . WriteLine ( $ " using System.Runtime.CompilerServices;") ;
191+ writer . WriteLine ( $ " using System.Text.RegularExpressions;") ;
192+ writer . WriteLine ( $ " using System.Threading;") ;
193+ writer . WriteLine ( $ "" ) ;
194+ if ( compilationDataAndResults . Right . AllowUnsafe )
195+ {
196+ writer . WriteLine ( $ " [SkipLocalsInit]") ;
197+ }
198+ writer . WriteLine ( $ " [{ s_generatedCodeAttribute } ]") ;
199+ writer . WriteLine ( $ " [EditorBrowsable(EditorBrowsableState.Never)]") ;
200+ writer . WriteLine ( $ " internal static class { generatedClassName } ") ;
201+ writer . WriteLine ( $ " {{") ;
202+
203+ // Emit each Regex-derived type.
204+ writer . Indent += 2 ;
205+ foreach ( object ? result in results )
206+ {
207+ if ( result is ValueTuple < RegexMethod , string , Diagnostic > limitedSupportResult )
208+ {
209+ EmitRegexLimitedBoilerplate ( writer , limitedSupportResult . Item1 , limitedSupportResult . Item1 . GeneratedId , limitedSupportResult . Item2 ) ;
210+ writer . WriteLine ( ) ;
211+ }
212+ else if ( result is ValueTuple < RegexMethod , string , Dictionary < string , string [ ] > > regexImpl )
213+ {
214+ EmitRegexDerivedImplementation ( writer , regexImpl . Item1 , regexImpl . Item1 . GeneratedId , regexImpl . Item2 ) ;
215+ writer . WriteLine ( ) ;
216+ }
217+ }
218+ writer . Indent -= 2 ;
219+
220+ // If any of the Regex-derived types asked for helper methods, emit those now.
221+ if ( requiredHelpers . Count != 0 )
222+ {
223+ writer . Indent += 2 ;
224+ writer . WriteLine ( $ "private static class { HelpersTypeName } ") ;
225+ writer . WriteLine ( $ "{{") ;
226+ writer . Indent ++ ;
227+ foreach ( KeyValuePair < string , string [ ] > helper in requiredHelpers )
228+ {
229+ foreach ( string value in helper . Value )
230+ {
231+ writer . WriteLine ( value ) ;
232+ }
233+ writer . WriteLine ( ) ;
80234 }
235+ writer . Indent -- ;
236+ writer . WriteLine ( $ "}}") ;
237+ writer . Indent -= 2 ;
81238 }
82239
83- context . AddSource ( "RegexGenerator.g.cs" , string . Join ( Environment . NewLine , code ) ) ;
240+ writer . WriteLine ( $ " }}") ;
241+ writer . WriteLine ( $ "}}") ;
242+
243+ // Save out the source
244+ context . AddSource ( "RegexGenerator.g.cs" , sw . ToString ( ) ) ;
84245 } ) ;
85246 }
247+
248+ /// <summary>Computes a hash of the string.</summary>
249+ /// <remarks>
250+ /// Currently an FNV-1a hash function. The actual algorithm used doesn't matter; just something
251+ /// simple to create a deterministic, pseudo-random value that's based on input text.
252+ /// </remarks>
253+ private static uint ComputeStringHash ( string s )
254+ {
255+ uint hashCode = 2166136261 ;
256+ foreach ( char c in s )
257+ {
258+ hashCode = ( c ^ hashCode ) * 16777619 ;
259+ }
260+ return hashCode ;
261+ }
86262 }
87263}
0 commit comments