diff --git a/src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.Analyzer.cs b/src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.Analyzer.cs new file mode 100644 index 00000000000000..731237799bf2c1 --- /dev/null +++ b/src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.Analyzer.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace System.Text.RegularExpressions.Generator +{ + public partial class RegexGenerator + { + /// + /// Provides diagnostics for invalid usage of the . + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class Analyzer : DiagnosticAnalyzer + { + public override ImmutableArray SupportedDiagnostics => [ + DiagnosticDescriptors.InvalidGeneratedRegexAttribute, + DiagnosticDescriptors.MultipleGeneratedRegexAttributes, + DiagnosticDescriptors.InvalidRegexArguments, + DiagnosticDescriptors.RegexMemberMustHaveValidSignature, + DiagnosticDescriptors.LimitedSourceGeneration, + ]; + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + + context.RegisterCompilationStartAction(context => + { + var generatedRegexAttributeSymbol = context.Compilation.GetTypeByMetadataName(GeneratedRegexAttributeName); + if (generatedRegexAttributeSymbol is null) + { + return; + } + context.RegisterSymbolAction(context => + { + if (context.Symbol is + IMethodSymbol { PartialDefinitionPart: not null } or IPropertySymbol { PartialDefinitionPart: not null }) + { + // The analyzer will also see the source-generated parts, so we must take care to emit diagnostics only for + // the partial definition that the user wrote. + return; + } + var attributes = context.Symbol.GetAttributes().Where(x => SymbolEqualityComparer.Default.Equals(x.AttributeClass, generatedRegexAttributeSymbol)).ToImmutableArray(); + if (attributes.Length == 0) + { + return; + } + if (context.Symbol.DeclaringSyntaxReferences.Length == 0) + { + return; + } + var onReportDiagnostic = context.ReportDiagnostic; + var targetNode = context.Symbol.DeclaringSyntaxReferences[0].GetSyntax(context.CancellationToken); + var regexPatternAndSyntax = ParseGeneratedRegexAttribute(targetNode, context.Symbol, context.Compilation, attributes, onReportDiagnostic); + if (regexPatternAndSyntax is null) + { + return; + } + _ = GetRegexMethod(regexPatternAndSyntax, targetNode, onReportDiagnostic); + }, SymbolKind.Method, SymbolKind.Property); + }); + } + } + } +} diff --git a/src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.Emitter.cs b/src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.Emitter.cs index 6bfe7c83424146..a0c4d90206962d 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.Emitter.cs +++ b/src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.Emitter.cs @@ -114,11 +114,11 @@ private static void EmitRegexPartialMethod(RegexMethod regexMethod, IndentedText } /// Emits the Regex-derived type for a method where we're unable to generate custom code. - private static void EmitRegexLimitedBoilerplate( - IndentedTextWriter writer, RegexMethod rm, string reason, LanguageVersion langVer) + private static void EmitRegexLimitedBoilerplate(IndentedTextWriter writer, RegexMethod rm) { + Debug.Assert(!rm.SupportsCodeGeneration); string visibility; - if (langVer >= LanguageVersion.CSharp11) + if (rm.CompilationData.LanguageVersion >= LanguageVersion.CSharp11) { visibility = "file"; writer.WriteLine($"/// Caches a instance for the {rm.MemberName} method."); @@ -128,7 +128,7 @@ private static void EmitRegexLimitedBoilerplate( visibility = "internal"; writer.WriteLine($"/// This class supports generated regexes and should not be used by other code directly."); } - writer.WriteLine($"/// A custom Regex-derived type could not be generated because {reason}."); + writer.WriteLine($"/// A custom Regex-derived type could not be generated because {rm.CodeGenerationUnsupportedReason}."); writer.WriteLine($"[{s_generatedCodeAttribute}]"); writer.WriteLine($"{visibility} sealed class {rm.GeneratedName} : Regex"); writer.WriteLine($"{{"); @@ -148,12 +148,11 @@ private static void EmitRegexLimitedBoilerplate( private const string HasDefaultTimeoutFieldName = "s_hasTimeout"; /// Emits the Regex-derived type for a method whose RunnerFactory implementation was generated into . - private static void EmitRegexDerivedImplementation( - IndentedTextWriter writer, RegexMethod rm, string runnerFactoryImplementation, bool allowUnsafe) + private static void EmitRegexDerivedImplementation(IndentedTextWriter writer, RegexMethod rm, string runnerFactoryImplementation) { writer.WriteLine($"/// Custom -derived type for the {rm.MemberName} method."); writer.WriteLine($"[{s_generatedCodeAttribute}]"); - if (allowUnsafe) + if (rm.CompilationData.AllowUnsafe) { writer.WriteLine($"[SkipLocalsInit]"); } @@ -229,8 +228,9 @@ static void AppendHashtableContents(IndentedTextWriter writer, IEnumerableEmits the code for the RunnerFactory. This is the actual logic for the regular expression. - private static void EmitRegexDerivedTypeRunnerFactory(IndentedTextWriter writer, RegexMethod rm, Dictionary requiredHelpers, bool checkOverflow) + private static void EmitRegexDerivedTypeRunnerFactory(IndentedTextWriter writer, RegexMethod rm, Dictionary requiredHelpers) { + bool checkOverflow = rm.CompilationData.CheckOverflow; void EnterCheckOverflow() { if (checkOverflow) diff --git a/src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.Parser.cs b/src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.Parser.cs index 301382d1f2bfe8..f9d8b5c5da4be3 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.Parser.cs +++ b/src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.Parser.cs @@ -3,6 +3,7 @@ using System.Collections.Immutable; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Threading; @@ -20,25 +21,25 @@ public partial class RegexGenerator private const string GeneratedRegexAttributeName = "System.Text.RegularExpressions.GeneratedRegexAttribute"; /// - /// Returns null if nothing to do, a if there's an error to report, - /// or if the type was analyzed successfully. + /// Analyzes a symbol with a [GeneratedRegex] attribute applied to it, and returns a + /// if valid, or returns and optionally + /// reports a if invalid. /// - private static object? GetRegexMethodDataOrFailureDiagnostic( - GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken) + private static RegexPatternAndSyntax? ParseGeneratedRegexAttribute( + SyntaxNode targetNode, ISymbol regexMemberSymbol, Compilation compilation, ImmutableArray boundAttributes, + Action? onReportDiagnostic) { - if (context.TargetNode is IndexerDeclarationSyntax or AccessorDeclarationSyntax) + if (targetNode is IndexerDeclarationSyntax or AccessorDeclarationSyntax) { // We allow these to be used as a target node for the sole purpose // of being able to flag invalid use when [GeneratedRegex] is applied incorrectly. // Otherwise, if the ForAttributeWithMetadataName call excluded these, [GeneratedRegex] // could be applied to them and we wouldn't be able to issue a diagnostic. - return Diagnostic.Create(DiagnosticDescriptors.RegexMemberMustHaveValidSignature, context.TargetNode.GetLocation()); + return ReportDiagnostic(DiagnosticDescriptors.RegexMemberMustHaveValidSignature, targetNode?.GetLocation()); } - var memberSyntax = (MemberDeclarationSyntax)context.TargetNode; - SemanticModel sm = context.SemanticModel; + var memberSyntax = (MemberDeclarationSyntax)targetNode; - Compilation compilation = sm.Compilation; INamedTypeSymbol? regexSymbol = compilation.GetBestTypeByMetadataName(RegexName); if (regexSymbol is null) @@ -53,28 +54,26 @@ public partial class RegexGenerator return null; } - ISymbol? regexMemberSymbol = context.TargetSymbol is IMethodSymbol or IPropertySymbol ? context.TargetSymbol : null; - if (regexMemberSymbol is null) + if (regexMemberSymbol is not (IMethodSymbol or IPropertySymbol)) { return null; } - ImmutableArray boundAttributes = context.Attributes; if (boundAttributes.Length != 1) { - return Diagnostic.Create(DiagnosticDescriptors.MultipleGeneratedRegexAttributes, memberSyntax.GetLocation()); + return ReportDiagnostic(DiagnosticDescriptors.MultipleGeneratedRegexAttributes, memberSyntax.GetLocation()); } AttributeData generatedRegexAttr = boundAttributes[0]; if (generatedRegexAttr.ConstructorArguments.Any(ca => ca.Kind == TypedConstantKind.Error)) { - return Diagnostic.Create(DiagnosticDescriptors.InvalidGeneratedRegexAttribute, memberSyntax.GetLocation()); + return ReportDiagnostic(DiagnosticDescriptors.InvalidGeneratedRegexAttribute, memberSyntax.GetLocation()); } ImmutableArray items = generatedRegexAttr.ConstructorArguments; if (items.Length is 0 or > 4) { - return Diagnostic.Create(DiagnosticDescriptors.InvalidGeneratedRegexAttribute, memberSyntax.GetLocation()); + return ReportDiagnostic(DiagnosticDescriptors.InvalidGeneratedRegexAttribute, memberSyntax.GetLocation()); } string? pattern = items[0].Value as string; @@ -106,7 +105,7 @@ public partial class RegexGenerator if (pattern is null || cultureName is null) { - return Diagnostic.Create(DiagnosticDescriptors.InvalidRegexArguments, memberSyntax.GetLocation(), "(null)"); + return ReportDiagnostic(DiagnosticDescriptors.InvalidRegexArguments, targetNode.GetLocation(), "(null)"); } bool nullableRegex; @@ -118,7 +117,7 @@ public partial class RegexGenerator regexMethodSymbol.Arity != 0 || !SymbolEqualityComparer.Default.Equals(regexMethodSymbol.ReturnType, regexSymbol)) { - return Diagnostic.Create(DiagnosticDescriptors.RegexMemberMustHaveValidSignature, memberSyntax.GetLocation()); + return ReportDiagnostic(DiagnosticDescriptors.RegexMemberMustHaveValidSignature, targetNode.GetLocation()); } nullableRegex = regexMethodSymbol.ReturnNullableAnnotation == NullableAnnotation.Annotated; @@ -127,12 +126,12 @@ public partial class RegexGenerator { Debug.Assert(regexMemberSymbol is IPropertySymbol); IPropertySymbol regexPropertySymbol = (IPropertySymbol)regexMemberSymbol; - if (!memberSyntax.Modifiers.Any(SyntaxKind.PartialKeyword) || // TODO: Switch to using regexPropertySymbol.IsPartialDefinition when available + if (!regexPropertySymbol.IsPartialDefinition || regexPropertySymbol.IsAbstract || regexPropertySymbol.SetMethod is not null || !SymbolEqualityComparer.Default.Equals(regexPropertySymbol.Type, regexSymbol)) { - return Diagnostic.Create(DiagnosticDescriptors.RegexMemberMustHaveValidSignature, memberSyntax.GetLocation()); + return ReportDiagnostic(DiagnosticDescriptors.RegexMemberMustHaveValidSignature, targetNode.GetLocation()); } nullableRegex = regexPropertySymbol.NullableAnnotation == NullableAnnotation.Annotated; @@ -154,7 +153,7 @@ regexPropertySymbol.SetMethod is not null || } catch (Exception e) { - return Diagnostic.Create(DiagnosticDescriptors.InvalidRegexArguments, memberSyntax.GetLocation(), e.Message); + return ReportDiagnostic(DiagnosticDescriptors.InvalidRegexArguments, targetNode.GetLocation(), e.Message); } if ((regexOptionsWithPatternOptions & RegexOptions.IgnoreCase) != 0 && !string.IsNullOrEmpty(cultureName)) @@ -162,7 +161,7 @@ regexPropertySymbol.SetMethod is not null || if ((regexOptions & RegexOptions.CultureInvariant) != 0) { // User passed in both a culture name and set RegexOptions.CultureInvariant which causes an explicit conflict. - return Diagnostic.Create(DiagnosticDescriptors.InvalidRegexArguments, memberSyntax.GetLocation(), "cultureName"); + return ReportDiagnostic(DiagnosticDescriptors.InvalidRegexArguments, targetNode.GetLocation(), "cultureName"); } try @@ -171,7 +170,7 @@ regexPropertySymbol.SetMethod is not null || } catch (CultureNotFoundException) { - return Diagnostic.Create(DiagnosticDescriptors.InvalidRegexArguments, memberSyntax.GetLocation(), "cultureName"); + return ReportDiagnostic(DiagnosticDescriptors.InvalidRegexArguments, targetNode.GetLocation(), "cultureName"); } } @@ -190,13 +189,13 @@ regexPropertySymbol.SetMethod is not null || RegexOptions.AnyNewLine; if ((regexOptions & ~SupportedOptions) != 0) { - return Diagnostic.Create(DiagnosticDescriptors.InvalidRegexArguments, memberSyntax.GetLocation(), "options"); + return ReportDiagnostic(DiagnosticDescriptors.InvalidRegexArguments, targetNode.GetLocation(), "options"); } // Validate the timeout if (matchTimeout is 0 or < -1) { - return Diagnostic.Create(DiagnosticDescriptors.InvalidRegexArguments, memberSyntax.GetLocation(), "matchTimeout"); + return ReportDiagnostic(DiagnosticDescriptors.InvalidRegexArguments, targetNode.GetLocation(), "matchTimeout"); } // Determine the namespace the class is declared in, if any @@ -215,7 +214,6 @@ regexPropertySymbol.SetMethod is not null || var result = new RegexPatternAndSyntax( regexType, IsProperty: regexMemberSymbol is IPropertySymbol, - memberSyntax.GetLocation(), regexMemberSymbol.Name, memberSyntax.Modifiers.ToString(), nullableRegex, @@ -247,16 +245,80 @@ SyntaxKind.StructDeclaration or SyntaxKind.RecordDeclaration or SyntaxKind.RecordStructDeclaration or SyntaxKind.InterfaceDeclaration; + + RegexPatternAndSyntax? ReportDiagnostic(DiagnosticDescriptor descriptor, Location? location, params object?[]? messageArgs) + { + onReportDiagnostic?.Invoke(Diagnostic.Create(descriptor, location, messageArgs)); + return null; + } + } + + /// + /// Analyzes the given to create a + /// that can be used to emit the source code for the regex. + /// + private static RegexMethod? GetRegexMethod(RegexPatternAndSyntax? patternAndSyntax, SyntaxNode? targetNode, Action? onReportDiagnostic) + { + try + { + // make sure Compiled is included to get all optimizations applied to it + RegexTree regexTree = RegexParser.Parse(patternAndSyntax!.Pattern, patternAndSyntax.Options | RegexOptions.Compiled, patternAndSyntax.Culture); + AnalysisResults analysis = RegexTreeAnalyzer.Analyze(regexTree); + + // If we're unable to generate a full implementation for this regex, report a diagnostic. + // We'll still output a limited implementation that just caches a new Regex(...). + bool supportsCodeGeneration = SupportsCodeGeneration(regexTree, patternAndSyntax.CompilationData.LanguageVersion, out string? reason); + if (!supportsCodeGeneration) + { + onReportDiagnostic?.Invoke(Diagnostic.Create(DiagnosticDescriptors.LimitedSourceGeneration, targetNode?.GetLocation(), reason)); + } + + return new RegexMethod(patternAndSyntax, regexTree, analysis, reason); + } + catch (Exception e) + { + onReportDiagnostic?.Invoke(Diagnostic.Create(DiagnosticDescriptors.InvalidRegexArguments, targetNode?.GetLocation(), e.Message)); + return null; + } } /// Data about a regex directly from the GeneratedRegex attribute. - internal sealed record RegexPatternAndSyntax(RegexType DeclaringType, bool IsProperty, Location DiagnosticLocation, string MemberName, string Modifiers, bool NullableRegex, string Pattern, RegexOptions Options, int? MatchTimeout, CultureInfo Culture, CompilationData CompilationData); + internal sealed record RegexPatternAndSyntax(RegexType DeclaringType, bool IsProperty, string MemberName, string Modifiers, bool NullableRegex, string Pattern, RegexOptions Options, int? MatchTimeout, CultureInfo Culture, CompilationData CompilationData) + { + /// + /// Returns a value that is equal for all instances that + /// will end up generating the same regex code. + /// + public (string Pattern, RegexOptions Options, int? MatchTimeout) GetEquivalenceKey() => (Pattern, Options, MatchTimeout); + } /// Data about a regex, including a fully parsed RegexTree and subsequent analysis. - internal sealed record RegexMethod(RegexType DeclaringType, bool IsProperty, string MemberName, string Modifiers, bool NullableRegex, string Pattern, RegexOptions Options, int? MatchTimeout, RegexTree Tree, AnalysisResults Analysis, CompilationData CompilationData) + internal sealed class RegexMethod(RegexPatternAndSyntax patternAndSyntax, RegexTree tree, AnalysisResults analysis, string? codeGenerationUnsupportedReason) { + public RegexPatternAndSyntax PatternAndSyntax { get; } = patternAndSyntax; + public RegexTree Tree { get; } = tree; + public AnalysisResults Analysis { get; } = analysis; + public string? CodeGenerationUnsupportedReason { get; } = codeGenerationUnsupportedReason; + + public RegexType DeclaringType => PatternAndSyntax.DeclaringType; + public bool IsProperty => PatternAndSyntax.IsProperty; + public string MemberName => PatternAndSyntax.MemberName; + public string Modifiers => PatternAndSyntax.Modifiers; + public bool NullableRegex => PatternAndSyntax.NullableRegex; + public string Pattern => PatternAndSyntax.Pattern; + public RegexOptions Options => PatternAndSyntax.Options; + public int? MatchTimeout => PatternAndSyntax.MatchTimeout; + public CultureInfo Culture => PatternAndSyntax.Culture; + public CompilationData CompilationData => PatternAndSyntax.CompilationData; + [MemberNotNullWhen(false, nameof(CodeGenerationUnsupportedReason))] + public bool SupportsCodeGeneration => CodeGenerationUnsupportedReason is null; + public string? GeneratedName { get; set; } public bool IsDuplicate { get; set; } + + // All properties of a RegexMethod depend on the RegexPatternAndSyntax, so we can only consider that for equality. + public override bool Equals(object? obj) => obj is RegexMethod other && PatternAndSyntax.Equals(other.PatternAndSyntax); + public override int GetHashCode() => PatternAndSyntax.GetHashCode(); } /// A type holding a regex method. diff --git a/src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.cs b/src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.cs index 69174c2a508efd..27933a3a70d3e1 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.cs +++ b/src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.cs @@ -42,16 +42,23 @@ public partial class RegexGenerator : IIncrementalGenerator internal record struct CompilationData(bool AllowUnsafe, bool CheckOverflow, LanguageVersion LanguageVersion); + internal readonly struct RuntimeRegexData(RegexMethod method, string? runnerFactoryImplementation, Dictionary? requiredHelpers) : IEquatable + { + public RegexMethod RegexMethod { get; } = method; + public string? RunnerFactoryImplementation { get; } = runnerFactoryImplementation; + public Dictionary? RequiredHelpers { get; } = requiredHelpers; + + // All fields of a RuntimeRegexData depend on the RegexMethod, so we can just consider that for equality. + public bool Equals(RuntimeRegexData other) => RegexMethod.Equals(other.RegexMethod); + + public override bool Equals(object? obj) => obj is RuntimeRegexData other && Equals(other); + + public override int GetHashCode() => RegexMethod.GetHashCode(); + } + public void Initialize(IncrementalGeneratorInitializationContext context) { - // Produces one entry per generated regex. This may be: - // - Diagnostic in the case of a failure that should end the compilation - // - (RegexMethod regexMethod, string runnerFactoryImplementation, Dictionary requiredHelpers) in the case of valid regex - // - (RegexMethod regexMethod, string reason, Diagnostic diagnostic) in the case of a limited-support regex - // - // Location is threaded separately from the records so that it doesn't participate in - // record equality — this allows the incremental pipeline to cache results by value. - IncrementalValueProvider<(ImmutableArray Results, ImmutableArray Diagnostics)> collected = + IncrementalValueProvider> sourceModel = context.SyntaxProvider // Find all MethodDeclarationSyntax nodes attributed with GeneratedRegex and gather the required information. @@ -65,100 +72,37 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .ForAttributeWithMetadataName( GeneratedRegexAttributeName, (node, _) => node is MethodDeclarationSyntax or PropertyDeclarationSyntax or IndexerDeclarationSyntax or AccessorDeclarationSyntax, - GetRegexMethodDataOrFailureDiagnostic) + (context, _) => ParseGeneratedRegexAttribute(context.TargetNode, context.TargetSymbol, context.SemanticModel.Compilation, context.Attributes, null)) // Filter out any parsing errors that resulted in null objects being returned. .Where(static m => m is not null) - // The input here will either be a Diagnostic (in the case of something erroneous detected in GetRegexMethodDataOrFailureDiagnostic) - // or it will be a RegexPatternAndSyntax containing all of the successfully parsed data from the attribute/method. - // This step parses the regex tree and checks whether full code generation is supported. - // The DiagnosticLocation is consumed here for diagnostic creation and not propagated further. - .Select((methodOrDiagnostic, _) => - { - if (methodOrDiagnostic is RegexPatternAndSyntax method) - { - try - { - RegexTree regexTree = RegexParser.Parse(method.Pattern, method.Options | RegexOptions.Compiled, method.Culture); // make sure Compiled is included to get all optimizations applied to it - AnalysisResults analysis = RegexTreeAnalyzer.Analyze(regexTree); - RegexMethod regexMethod = new(method.DeclaringType, method.IsProperty, method.MemberName, method.Modifiers, method.NullableRegex, method.Pattern, method.Options, method.MatchTimeout, regexTree, analysis, method.CompilationData); - - // If we're unable to generate a full implementation for this regex, report a diagnostic. - // We'll still output a limited implementation that just caches a new Regex(...). - if (!SupportsCodeGeneration(regexMethod, regexMethod.CompilationData.LanguageVersion, out string? reason)) - { - return (object)(regexMethod, reason, Diagnostic.Create(DiagnosticDescriptors.LimitedSourceGeneration, method.DiagnosticLocation), regexMethod.CompilationData); - } + // For each successfully parsed [GeneratedRegex] attribute, parse the regex and construct a RegexMethod object. + .Select((x, _) => GetRegexMethod(x, null, null)) - return regexMethod; - } - catch (Exception e) - { - return Diagnostic.Create(DiagnosticDescriptors.InvalidRegexArguments, method.DiagnosticLocation, e.Message); - } - } - - return methodOrDiagnostic; - }) + // Filter out any regexes that failed to parse and resulted in null objects being returned. + .Where(static m => m is not null) // Generate the RunnerFactory for each regex, if possible. This is where the bulk of the implementation occurs. - .Select((state, _) => + .Select((regexMethod, _) => { - if (state is not RegexMethod regexMethod) + if (!regexMethod!.SupportsCodeGeneration) { - Debug.Assert(state is Diagnostic or ValueTuple); - return state; + return new RuntimeRegexData(regexMethod, null, null); } - // Generate the core logic for the regex. Dictionary requiredHelpers = new(); var sw = new StringWriter(); var writer = new IndentedTextWriter(sw); writer.Indent += 2; writer.WriteLine(); - EmitRegexDerivedTypeRunnerFactory(writer, regexMethod, requiredHelpers, regexMethod.CompilationData.CheckOverflow); + EmitRegexDerivedTypeRunnerFactory(writer, regexMethod!, requiredHelpers); writer.Indent -= 2; - return (regexMethod, sw.ToString(), requiredHelpers, regexMethod.CompilationData); + return new RuntimeRegexData(regexMethod, sw.ToString(), requiredHelpers); }) - // Combine all of the generated text outputs into a single batch, then split - // the source model from diagnostics so they can be emitted independently. - .Collect() - .Select(static (results, _) => - { - ImmutableArray.Builder? diagnostics = null; - ImmutableArray.Builder? filteredResults = null; - - foreach (object result in results) - { - if (result is Diagnostic d) - { - (diagnostics ??= ImmutableArray.CreateBuilder()).Add(d); - } - else if (result is ValueTuple limitedSupportResult) - { - (diagnostics ??= ImmutableArray.CreateBuilder()).Add(limitedSupportResult.Item3); - (filteredResults ??= ImmutableArray.CreateBuilder()).Add( - (limitedSupportResult.Item1, limitedSupportResult.Item2, limitedSupportResult.Item4)); - } - else - { - (filteredResults ??= ImmutableArray.CreateBuilder()).Add(result); - } - } - - return ( - Results: filteredResults?.ToImmutable() ?? ImmutableArray.Empty, - Diagnostics: diagnostics?.ToImmutable() ?? ImmutableArray.Empty); - }); - - // Project to just the source model, discarding diagnostics. - // ObjectImmutableArraySequenceEqualityComparer applies element-wise equality over - // the heterogeneous result array, enabling Roslyn's incremental pipeline to skip - // re-emitting source when the model has not changed. - IncrementalValueProvider> sourceModel = - collected.Select(static (t, _) => t.Results).WithComparer(new ObjectImmutableArraySequenceEqualityComparer()); + // Combine all of the generated text outputs into a single batch. + .Collect(); context.RegisterSourceOutput(sourceModel, static (context, results) => { @@ -197,44 +141,36 @@ public void Initialize(IncrementalGeneratorInitializationContext context) // If we have any (RegexMethod regexMethod, string runnerFactoryImplementation, Dictionary requiredHelpers, CompilationData compilationData), // those are generated implementations to be emitted. We need to gather up their required helpers. Dictionary requiredHelpers = new(); - foreach (object? result in results) + foreach (var result in results) { - RegexMethod? regexMethod = null; - if (result is ValueTuple limitedSupportResult) - { - regexMethod = limitedSupportResult.Item1; - } - else if (result is ValueTuple, CompilationData> regexImpl) + context.CancellationToken.ThrowIfCancellationRequested(); + RegexMethod regexMethod = result.RegexMethod; + if (result.RequiredHelpers is not null) { - foreach (KeyValuePair helper in regexImpl.Item3) + foreach (KeyValuePair helper in result.RequiredHelpers) { if (!requiredHelpers.ContainsKey(helper.Key)) { requiredHelpers.Add(helper.Key, helper.Value); } } - - regexMethod = regexImpl.Item1; } - if (regexMethod is not null) + var key = regexMethod.PatternAndSyntax.GetEquivalenceKey(); + if (emittedExpressions.TryGetValue(key, out RegexMethod? implementation)) { - var key = (regexMethod.Pattern, regexMethod.Options, regexMethod.MatchTimeout); - if (emittedExpressions.TryGetValue(key, out RegexMethod? implementation)) - { - regexMethod.IsDuplicate = true; - regexMethod.GeneratedName = implementation.GeneratedName; - } - else - { - regexMethod.IsDuplicate = false; - regexMethod.GeneratedName = $"{regexMethod.MemberName}_{id++}"; - emittedExpressions.Add(key, regexMethod); - } - - EmitRegexPartialMethod(regexMethod, writer); - writer.WriteLine(); + regexMethod.IsDuplicate = true; + regexMethod.GeneratedName = implementation.GeneratedName; + } + else + { + regexMethod.IsDuplicate = false; + regexMethod.GeneratedName = $"{regexMethod.PatternAndSyntax.MemberName}_{id++}"; + emittedExpressions.Add(key, regexMethod); } + + EmitRegexPartialMethod(regexMethod, writer); + writer.WriteLine(); } // At this point we've emitted all the partial method definitions, but we still need to emit the actual regex-derived implementations. @@ -259,21 +195,21 @@ public void Initialize(IncrementalGeneratorInitializationContext context) // Emit each Regex-derived type. writer.Indent++; - foreach (object? result in results) + foreach (var result in results) { - if (result is ValueTuple limitedSupportResult) + context.CancellationToken.ThrowIfCancellationRequested(); + var regexMethod = result.RegexMethod; + if (!regexMethod.IsDuplicate) { - if (!limitedSupportResult.Item1.IsDuplicate) + if (regexMethod.SupportsCodeGeneration) { - EmitRegexLimitedBoilerplate(writer, limitedSupportResult.Item1, limitedSupportResult.Item2, limitedSupportResult.Item3.LanguageVersion); + Debug.Assert(result.RunnerFactoryImplementation is not null); + EmitRegexDerivedImplementation(writer, regexMethod, result.RunnerFactoryImplementation); writer.WriteLine(); } - } - else if (result is ValueTuple, CompilationData> regexImpl) - { - if (!regexImpl.Item1.IsDuplicate) + else { - EmitRegexDerivedImplementation(writer, regexImpl.Item1, regexImpl.Item2, regexImpl.Item4.AllowUnsafe); + EmitRegexLimitedBoilerplate(writer, regexMethod); writer.WriteLine(); } } @@ -292,6 +228,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) bool sawFirst = false; foreach (KeyValuePair helper in requiredHelpers.OrderBy(h => h.Key, StringComparer.Ordinal)) { + context.CancellationToken.ThrowIfCancellationRequested(); if (sawFirst) { writer.WriteLine(); @@ -313,30 +250,14 @@ public void Initialize(IncrementalGeneratorInitializationContext context) // Save out the source context.AddSource("RegexGenerator.g.cs", sw.ToString()); }); - - // Project to just the diagnostics, discarding the model. ImmutableArray does not - // implement value equality, so Roslyn's incremental pipeline uses reference equality — - // the callback fires on every compilation change. This is by design: diagnostic emission - // is cheap, and we need fresh SourceLocation instances that are pragma-suppressible - // (cf. https://github.com/dotnet/runtime/issues/92509). - IncrementalValueProvider> diagnosticResults = - collected.Select(static (t, _) => t.Diagnostics); - - context.RegisterSourceOutput(diagnosticResults, static (context, diagnostics) => - { - foreach (Diagnostic diagnostic in diagnostics) - { - context.ReportDiagnostic(diagnostic); - } - }); } /// Determines whether the passed in node supports C# code generation. /// - // It also provides a human-readable string to explain the reason. It will be emitted by the source generator - // as a comment into the C# code, hence there's no need to localize. + /// It also provides a human-readable string to explain the reason. It will be emitted by the source generator + /// as a comment into the C# code, hence there's no need to localize. /// - private static bool SupportsCodeGeneration(RegexMethod method, LanguageVersion languageVersion, [NotNullWhen(false)] out string? reason) + private static bool SupportsCodeGeneration(RegexTree tree, LanguageVersion languageVersion, [NotNullWhen(false)] out string? reason) { if (languageVersion < LanguageVersion.CSharp11) { @@ -344,7 +265,7 @@ private static bool SupportsCodeGeneration(RegexMethod method, LanguageVersion l return false; } - RegexNode node = method.Tree.Root; + RegexNode node = tree.Root; if (!node.SupportsCompilation(out reason)) { @@ -389,38 +310,5 @@ static bool HasCaseInsensitiveBackReferences(RegexNode node) return false; } } - - private sealed class ObjectImmutableArraySequenceEqualityComparer : IEqualityComparer> - { - public bool Equals(ImmutableArray left, ImmutableArray right) - { - if (left.Length != right.Length) - { - return false; - } - - for (int i = 0; i < left.Length; i++) - { - bool areEqual = left[i] is { } leftElem - ? leftElem.Equals(right[i]) - : right[i] is null; - - if (!areEqual) - { - return false; - } - } - - return true; - } - - public int GetHashCode([DisallowNull] ImmutableArray obj) - { - int hash = 0; - for (int i = 0; i < obj.Length; i++) - hash = (hash, obj[i]).GetHashCode(); - return hash; - } - } } } diff --git a/src/libraries/System.Text.RegularExpressions/gen/System.Text.RegularExpressions.Generator.csproj b/src/libraries/System.Text.RegularExpressions/gen/System.Text.RegularExpressions.Generator.csproj index 642d6d750443fc..aa44d3572fa82d 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/System.Text.RegularExpressions.Generator.csproj +++ b/src/libraries/System.Text.RegularExpressions/gen/System.Text.RegularExpressions.Generator.csproj @@ -15,6 +15,7 @@ true cs true + true diff --git a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexGeneratorHelper.netcoreapp.cs b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexGeneratorHelper.netcoreapp.cs index 0a0b82888ef5de..e3ccbbd0cfe94b 100644 --- a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexGeneratorHelper.netcoreapp.cs +++ b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexGeneratorHelper.netcoreapp.cs @@ -16,6 +16,7 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Emit; using Microsoft.CodeAnalysis.Text; using Xunit; @@ -69,7 +70,7 @@ internal static byte[] CreateAssemblyImage(string source, string assemblyName) throw new InvalidOperationException(); } - internal static async Task<(Compilation, GeneratorDriverRunResult)> RunGeneratorCore( + private static async Task<(Compilation, GeneratorDriverRunResult)> RunGeneratorCore( string code, LanguageVersion langVersion = LanguageVersion.Preview, MetadataReference[]? additionalRefs = null, bool allowUnsafe = false, bool checkOverflow = true, CancellationToken cancellationToken = default) { var proj = new AdhocWorkspace() @@ -89,30 +90,35 @@ internal static byte[] CreateAssemblyImage(string source, string assemblyName) var generator = new RegexGenerator(); CSharpGeneratorDriver cgd = CSharpGeneratorDriver.Create(new[] { generator.AsSourceGenerator() }, parseOptions: CSharpParseOptions.Default.WithLanguageVersion(langVersion)); GeneratorDriver gd = cgd.RunGenerators(comp!, cancellationToken); - return (comp, gd.GetRunResult()); + var runResult = gd.GetRunResult(); + Assert.Empty(runResult.Diagnostics); // The analyzer is the one that produces diagnostics. + return (comp, runResult); } internal static async Task> RunGenerator( string code, bool compile = false, LanguageVersion langVersion = LanguageVersion.Preview, MetadataReference[]? additionalRefs = null, bool allowUnsafe = false, bool checkOverflow = true, CancellationToken cancellationToken = default) { (Compilation comp, GeneratorDriverRunResult generatorResults) = await RunGeneratorCore(code, langVersion, additionalRefs, allowUnsafe, checkOverflow, cancellationToken); + comp = comp.AddSyntaxTrees(generatorResults.GeneratedTrees); + var withAnalyzers = comp.WithAnalyzers([new RegexGenerator.Analyzer()]); + // Launch the analyzer and await it after we call Emit, in order for them to run concurrently. + var analyzerDiagnosticsTask = withAnalyzers.GetAnalyzerDiagnosticsAsync(cancellationToken).ConfigureAwait(false); if (!compile) { - return generatorResults.Diagnostics; + return await analyzerDiagnosticsTask; } - comp = comp.AddSyntaxTrees(generatorResults.GeneratedTrees.ToArray()); - EmitResult results = comp.Emit(Stream.Null, cancellationToken: cancellationToken); - ImmutableArray generatorDiagnostics = generatorResults.Diagnostics.RemoveAll(d => d.Severity <= DiagnosticSeverity.Hidden); + EmitResult results = withAnalyzers.Compilation.Emit(Stream.Null, cancellationToken: cancellationToken); + ImmutableArray analyzerDiagnostics = await analyzerDiagnosticsTask; ImmutableArray resultsDiagnostics = results.Diagnostics.RemoveAll(d => d.Severity <= DiagnosticSeverity.Hidden); if (!results.Success || resultsDiagnostics.Length != 0) { throw new ArgumentException( - string.Join(Environment.NewLine, resultsDiagnostics.Concat(generatorDiagnostics)) + Environment.NewLine + + string.Join(Environment.NewLine, resultsDiagnostics.Concat(analyzerDiagnostics)) + Environment.NewLine + string.Join(Environment.NewLine, generatorResults.GeneratedTrees.Select(t => t.ToString()))); } - return generatorResults.Diagnostics.Concat(results.Diagnostics).Where(d => d.Severity != DiagnosticSeverity.Hidden).ToArray(); + return analyzerDiagnostics.Concat(results.Diagnostics).Where(d => d.Severity != DiagnosticSeverity.Hidden).ToArray(); } internal static async Task GenerateSourceText( @@ -121,11 +127,6 @@ internal static async Task GenerateSourceText( (Compilation comp, GeneratorDriverRunResult generatorResults) = await RunGeneratorCore(code, langVersion, additionalRefs, allowUnsafe, checkOverflow, cancellationToken); string generatedSource = string.Concat(generatorResults.GeneratedTrees.Select(t => t.ToString())); - if (generatorResults.Diagnostics.Length != 0) - { - throw new ArgumentException(string.Join(Environment.NewLine, generatorResults.Diagnostics) + Environment.NewLine + generatedSource); - } - return generatedSource; } diff --git a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexGeneratorParserTests.cs b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexGeneratorParserTests.cs index a742f1234f76f7..88faacdcec4f2d 100644 --- a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexGeneratorParserTests.cs +++ b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexGeneratorParserTests.cs @@ -404,7 +404,7 @@ partial class C public async Task Diagnostic_HasPragmaSuppressibleLocation() { // SYSLIB1044 (LimitedSourceGeneration) is emitted for case-insensitive backreferences. - string code = """ + IReadOnlyList diagnostics = await RegexGeneratorHelper.RunGenerator(""" #pragma warning disable SYSLIB1044 using System.Text.RegularExpressions; partial class C @@ -412,12 +412,9 @@ partial class C [GeneratedRegex("(a)\\1", RegexOptions.IgnoreCase)] private static partial Regex Method(); } - """; + """); - (Compilation comp, GeneratorDriverRunResult result) = await RegexGeneratorHelper.RunGeneratorCore(code); - var effective = CompilationWithAnalyzers.GetEffectiveDiagnostics(result.Diagnostics, comp); - Diagnostic diagnostic = Assert.Single(effective, d => d.Id == "SYSLIB1044"); - Assert.True(diagnostic.IsSuppressed); + Assert.Empty(diagnostics); } [Fact]