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