Skip to content

Commit 2bf492b

Browse files
authored
Enable GeneratedRegex on partial properties (dotnet#102977)
* Enable GeneratedRegex on partial properties * Address PR feedback * Suppress API compat warning
1 parent c286a8e commit 2bf492b

23 files changed

+527
-127
lines changed

src/libraries/System.Text.RegularExpressions/gen/DiagnosticDescriptors.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,10 @@ internal static class DiagnosticDescriptors
3737
isEnabledByDefault: true,
3838
customTags: WellKnownDiagnosticTags.NotConfigurable);
3939

40-
public static DiagnosticDescriptor RegexMethodMustHaveValidSignature { get; } = DiagnosticDescriptorHelper.Create(
40+
public static DiagnosticDescriptor RegexMemberMustHaveValidSignature { get; } = DiagnosticDescriptorHelper.Create(
4141
id: "SYSLIB1043",
4242
title: new LocalizableResourceString(nameof(SR.InvalidGeneratedRegexAttributeTitle), SR.ResourceManager, typeof(FxResources.System.Text.RegularExpressions.Generator.SR)),
43-
messageFormat: new LocalizableResourceString(nameof(SR.RegexMethodMustHaveValidSignatureMessage), SR.ResourceManager, typeof(FxResources.System.Text.RegularExpressions.Generator.SR)),
43+
messageFormat: new LocalizableResourceString(nameof(SR.RegexMemberMustHaveValidSignatureMessage), SR.ResourceManager, typeof(FxResources.System.Text.RegularExpressions.Generator.SR)),
4444
category: Category,
4545
DiagnosticSeverity.Error,
4646
isEnabledByDefault: true,

src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.Emitter.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,12 @@ private static void EmitRegexPartialMethod(RegexMethod regexMethod, IndentedText
7171
writer.WriteLine($"/// </code>");
7272
writer.WriteLine($"/// </remarks>");
7373
writer.WriteLine($"[global::System.CodeDom.Compiler.{s_generatedCodeAttribute}]");
74-
writer.WriteLine($"{regexMethod.Modifiers} global::System.Text.RegularExpressions.Regex {regexMethod.MethodName}() => global::{GeneratedNamespace}.{regexMethod.GeneratedName}.Instance;");
74+
writer.Write($"{regexMethod.Modifiers} global::System.Text.RegularExpressions.Regex{(regexMethod.NullableRegex ? "?" : "")} {regexMethod.MemberName}");
75+
if (!regexMethod.IsProperty)
76+
{
77+
writer.Write("()");
78+
}
79+
writer.WriteLine($" => global::{GeneratedNamespace}.{regexMethod.GeneratedName}.Instance;");
7580

7681
// Unwind all scopes
7782
while (writer.Indent != 0)
@@ -89,7 +94,7 @@ private static void EmitRegexLimitedBoilerplate(
8994
if (langVer >= LanguageVersion.CSharp11)
9095
{
9196
visibility = "file";
92-
writer.WriteLine($"/// <summary>Caches a <see cref=\"Regex\"/> instance for the {rm.MethodName} method.</summary>");
97+
writer.WriteLine($"/// <summary>Caches a <see cref=\"Regex\"/> instance for the {rm.MemberName} method.</summary>");
9398
}
9499
else
95100
{
@@ -119,7 +124,7 @@ private static void EmitRegexLimitedBoilerplate(
119124
private static void EmitRegexDerivedImplementation(
120125
IndentedTextWriter writer, RegexMethod rm, string runnerFactoryImplementation, bool allowUnsafe)
121126
{
122-
writer.WriteLine($"/// <summary>Custom <see cref=\"Regex\"/>-derived type for the {rm.MethodName} method.</summary>");
127+
writer.WriteLine($"/// <summary>Custom <see cref=\"Regex\"/>-derived type for the {rm.MemberName} method.</summary>");
123128
writer.WriteLine($"[{s_generatedCodeAttribute}]");
124129
if (allowUnsafe)
125130
{

src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.Parser.cs

Lines changed: 59 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Collections.Immutable;
5+
using System.Diagnostics;
56
using System.Globalization;
67
using System.Linq;
78
using System.Threading;
@@ -25,7 +26,16 @@ public partial class RegexGenerator
2526
private static object? GetRegexMethodDataOrFailureDiagnostic(
2627
GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken)
2728
{
28-
var methodSyntax = (MethodDeclarationSyntax)context.TargetNode;
29+
if (context.TargetNode is IndexerDeclarationSyntax or AccessorDeclarationSyntax)
30+
{
31+
// We allow these to be used as a target node for the sole purpose
32+
// of being able to flag invalid use when [GeneratedRegex] is applied incorrectly.
33+
// Otherwise, if the ForAttributeWithMetadataName call excluded these, [GeneratedRegex]
34+
// could be applied to them and we wouldn't be able to issue a diagnostic.
35+
return new DiagnosticData(DiagnosticDescriptors.RegexMemberMustHaveValidSignature, GetComparableLocation(context.TargetNode));
36+
}
37+
38+
var memberSyntax = (MemberDeclarationSyntax)context.TargetNode;
2939
SemanticModel sm = context.SemanticModel;
3040

3141
Compilation compilation = sm.Compilation;
@@ -37,34 +47,34 @@ public partial class RegexGenerator
3747
return null;
3848
}
3949

40-
TypeDeclarationSyntax? typeDec = methodSyntax.Parent as TypeDeclarationSyntax;
50+
TypeDeclarationSyntax? typeDec = memberSyntax.Parent as TypeDeclarationSyntax;
4151
if (typeDec is null)
4252
{
4353
return null;
4454
}
4555

46-
IMethodSymbol? regexMethodSymbol = context.TargetSymbol as IMethodSymbol;
47-
if (regexMethodSymbol is null)
56+
ISymbol? regexMemberSymbol = context.TargetSymbol is IMethodSymbol or IPropertySymbol ? context.TargetSymbol : null;
57+
if (regexMemberSymbol is null)
4858
{
4959
return null;
5060
}
5161

5262
ImmutableArray<AttributeData> boundAttributes = context.Attributes;
5363
if (boundAttributes.Length != 1)
5464
{
55-
return new DiagnosticData(DiagnosticDescriptors.MultipleGeneratedRegexAttributes, GetComparableLocation(methodSyntax));
65+
return new DiagnosticData(DiagnosticDescriptors.MultipleGeneratedRegexAttributes, GetComparableLocation(memberSyntax));
5666
}
5767
AttributeData generatedRegexAttr = boundAttributes[0];
5868

5969
if (generatedRegexAttr.ConstructorArguments.Any(ca => ca.Kind == TypedConstantKind.Error))
6070
{
61-
return new DiagnosticData(DiagnosticDescriptors.InvalidGeneratedRegexAttribute, GetComparableLocation(methodSyntax));
71+
return new DiagnosticData(DiagnosticDescriptors.InvalidGeneratedRegexAttribute, GetComparableLocation(memberSyntax));
6272
}
6373

6474
ImmutableArray<TypedConstant> items = generatedRegexAttr.ConstructorArguments;
6575
if (items.Length is 0 or > 4)
6676
{
67-
return new DiagnosticData(DiagnosticDescriptors.InvalidGeneratedRegexAttribute, GetComparableLocation(methodSyntax));
77+
return new DiagnosticData(DiagnosticDescriptors.InvalidGeneratedRegexAttribute, GetComparableLocation(memberSyntax));
6878
}
6979

7080
string? pattern = items[0].Value as string;
@@ -96,16 +106,36 @@ public partial class RegexGenerator
96106

97107
if (pattern is null || cultureName is null)
98108
{
99-
return new DiagnosticData(DiagnosticDescriptors.InvalidRegexArguments, GetComparableLocation(methodSyntax), "(null)");
109+
return new DiagnosticData(DiagnosticDescriptors.InvalidRegexArguments, GetComparableLocation(memberSyntax), "(null)");
100110
}
101111

102-
if (!regexMethodSymbol.IsPartialDefinition ||
103-
regexMethodSymbol.IsAbstract ||
104-
regexMethodSymbol.Parameters.Length != 0 ||
105-
regexMethodSymbol.Arity != 0 ||
106-
!SymbolEqualityComparer.Default.Equals(regexMethodSymbol.ReturnType, regexSymbol))
112+
bool nullableRegex;
113+
if (regexMemberSymbol is IMethodSymbol regexMethodSymbol)
107114
{
108-
return new DiagnosticData(DiagnosticDescriptors.RegexMethodMustHaveValidSignature, GetComparableLocation(methodSyntax));
115+
if (!regexMethodSymbol.IsPartialDefinition ||
116+
regexMethodSymbol.IsAbstract ||
117+
regexMethodSymbol.Parameters.Length != 0 ||
118+
regexMethodSymbol.Arity != 0 ||
119+
!SymbolEqualityComparer.Default.Equals(regexMethodSymbol.ReturnType, regexSymbol))
120+
{
121+
return new DiagnosticData(DiagnosticDescriptors.RegexMemberMustHaveValidSignature, GetComparableLocation(memberSyntax));
122+
}
123+
124+
nullableRegex = regexMethodSymbol.ReturnNullableAnnotation == NullableAnnotation.Annotated;
125+
}
126+
else
127+
{
128+
Debug.Assert(regexMemberSymbol is IPropertySymbol);
129+
IPropertySymbol regexPropertySymbol = (IPropertySymbol)regexMemberSymbol;
130+
if (!memberSyntax.Modifiers.Any(SyntaxKind.PartialKeyword) || // TODO: Switch to using regexPropertySymbol.IsPartialDefinition when available
131+
regexPropertySymbol.IsAbstract ||
132+
regexPropertySymbol.SetMethod is not null ||
133+
!SymbolEqualityComparer.Default.Equals(regexPropertySymbol.Type, regexSymbol))
134+
{
135+
return new DiagnosticData(DiagnosticDescriptors.RegexMemberMustHaveValidSignature, GetComparableLocation(memberSyntax));
136+
}
137+
138+
nullableRegex = regexPropertySymbol.NullableAnnotation == NullableAnnotation.Annotated;
109139
}
110140

111141
RegexOptions regexOptions = options is not null ? (RegexOptions)options : RegexOptions.None;
@@ -124,15 +154,15 @@ public partial class RegexGenerator
124154
}
125155
catch (Exception e)
126156
{
127-
return new DiagnosticData(DiagnosticDescriptors.InvalidRegexArguments, GetComparableLocation(methodSyntax), e.Message);
157+
return new DiagnosticData(DiagnosticDescriptors.InvalidRegexArguments, GetComparableLocation(memberSyntax), e.Message);
128158
}
129159

130160
if ((regexOptionsWithPatternOptions & RegexOptions.IgnoreCase) != 0 && !string.IsNullOrEmpty(cultureName))
131161
{
132162
if ((regexOptions & RegexOptions.CultureInvariant) != 0)
133163
{
134164
// User passed in both a culture name and set RegexOptions.CultureInvariant which causes an explicit conflict.
135-
return new DiagnosticData(DiagnosticDescriptors.InvalidRegexArguments, GetComparableLocation(methodSyntax), "cultureName");
165+
return new DiagnosticData(DiagnosticDescriptors.InvalidRegexArguments, GetComparableLocation(memberSyntax), "cultureName");
136166
}
137167

138168
try
@@ -141,7 +171,7 @@ public partial class RegexGenerator
141171
}
142172
catch (CultureNotFoundException)
143173
{
144-
return new DiagnosticData(DiagnosticDescriptors.InvalidRegexArguments, GetComparableLocation(methodSyntax), "cultureName");
174+
return new DiagnosticData(DiagnosticDescriptors.InvalidRegexArguments, GetComparableLocation(memberSyntax), "cultureName");
145175
}
146176
}
147177

@@ -159,17 +189,17 @@ public partial class RegexGenerator
159189
RegexOptions.Singleline;
160190
if ((regexOptions & ~SupportedOptions) != 0)
161191
{
162-
return new DiagnosticData(DiagnosticDescriptors.InvalidRegexArguments, GetComparableLocation(methodSyntax), "options");
192+
return new DiagnosticData(DiagnosticDescriptors.InvalidRegexArguments, GetComparableLocation(memberSyntax), "options");
163193
}
164194

165195
// Validate the timeout
166196
if (matchTimeout is 0 or < -1)
167197
{
168-
return new DiagnosticData(DiagnosticDescriptors.InvalidRegexArguments, GetComparableLocation(methodSyntax), "matchTimeout");
198+
return new DiagnosticData(DiagnosticDescriptors.InvalidRegexArguments, GetComparableLocation(memberSyntax), "matchTimeout");
169199
}
170200

171201
// Determine the namespace the class is declared in, if any
172-
string? ns = regexMethodSymbol.ContainingType?.ContainingNamespace?.ToDisplayString(
202+
string? ns = regexMemberSymbol.ContainingType?.ContainingNamespace?.ToDisplayString(
173203
SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted));
174204

175205
var regexType = new RegexType(
@@ -183,9 +213,11 @@ public partial class RegexGenerator
183213

184214
var result = new RegexPatternAndSyntax(
185215
regexType,
186-
GetComparableLocation(methodSyntax),
187-
regexMethodSymbol.Name,
188-
methodSyntax.Modifiers.ToString(),
216+
IsProperty: regexMemberSymbol is IPropertySymbol,
217+
GetComparableLocation(memberSyntax),
218+
regexMemberSymbol.Name,
219+
memberSyntax.Modifiers.ToString(),
220+
nullableRegex,
189221
pattern,
190222
regexOptions,
191223
matchTimeout,
@@ -217,18 +249,18 @@ SyntaxKind.RecordStructDeclaration or
217249

218250
// Get a Location object that doesn't store a reference to the compilation.
219251
// That allows it to compare equally across compilations.
220-
static Location GetComparableLocation(MethodDeclarationSyntax method)
252+
static Location GetComparableLocation(SyntaxNode syntax)
221253
{
222-
var location = method.GetLocation();
254+
var location = syntax.GetLocation();
223255
return Location.Create(location.SourceTree?.FilePath ?? string.Empty, location.SourceSpan, location.GetLineSpan().Span);
224256
}
225257
}
226258

227259
/// <summary>Data about a regex directly from the GeneratedRegex attribute.</summary>
228-
internal sealed record RegexPatternAndSyntax(RegexType DeclaringType, Location DiagnosticLocation, string MethodName, string Modifiers, string Pattern, RegexOptions Options, int? MatchTimeout, CultureInfo Culture, CompilationData CompilationData);
260+
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);
229261

230262
/// <summary>Data about a regex, including a fully parsed RegexTree and subsequent analysis.</summary>
231-
internal sealed record RegexMethod(RegexType DeclaringType, Location DiagnosticLocation, string MethodName, string Modifiers, string Pattern, RegexOptions Options, int? MatchTimeout, RegexTree Tree, AnalysisResults Analysis, CompilationData CompilationData)
263+
internal sealed record RegexMethod(RegexType DeclaringType, bool IsProperty, Location DiagnosticLocation, string MemberName, string Modifiers, bool NullableRegex, string Pattern, RegexOptions Options, int? MatchTimeout, RegexTree Tree, AnalysisResults Analysis, CompilationData CompilationData)
232264
{
233265
public string? GeneratedName { get; set; }
234266
public bool IsDuplicate { get; set; }

src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
5757
// if there are no changes.
5858
.ForAttributeWithMetadataName(
5959
GeneratedRegexAttributeName,
60-
(node, _) => node is MethodDeclarationSyntax,
60+
(node, _) => node is MethodDeclarationSyntax or PropertyDeclarationSyntax or IndexerDeclarationSyntax or AccessorDeclarationSyntax,
6161
GetRegexMethodDataOrFailureDiagnostic)
6262

6363
// Filter out any parsing errors that resulted in null objects being returned.
@@ -73,7 +73,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
7373
{
7474
RegexTree regexTree = RegexParser.Parse(method.Pattern, method.Options | RegexOptions.Compiled, method.Culture); // make sure Compiled is included to get all optimizations applied to it
7575
AnalysisResults analysis = RegexTreeAnalyzer.Analyze(regexTree);
76-
return new RegexMethod(method.DeclaringType, method.DiagnosticLocation, method.MethodName, method.Modifiers, method.Pattern, method.Options, method.MatchTimeout, regexTree, analysis, method.CompilationData);
76+
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);
7777
}
7878
catch (Exception e)
7979
{
@@ -201,7 +201,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
201201
else
202202
{
203203
regexMethod.IsDuplicate = false;
204-
regexMethod.GeneratedName = $"{regexMethod.MethodName}_{id++}";
204+
regexMethod.GeneratedName = $"{regexMethod.MemberName}_{id++}";
205205
emittedExpressions.Add(key, regexMethod);
206206
}
207207

src/libraries/System.Text.RegularExpressions/gen/Resources/Strings.resx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,8 @@
131131
<data name="InvalidRegexArgumentsMessage" xml:space="preserve">
132132
<value>The specified regex is invalid. '{0}'</value>
133133
</data>
134-
<data name="RegexMethodMustHaveValidSignatureMessage" xml:space="preserve">
135-
<value>GeneratedRegexAttribute method must be partial, parameterless, non-generic, non-abstract, and return Regex.</value>
134+
<data name="RegexMemberMustHaveValidSignatureMessage" xml:space="preserve">
135+
<value>GeneratedRegexAttribute method or property must be partial, parameterless, non-generic, non-abstract, and return Regex. If a property, it must also be get-only.</value>
136136
</data>
137137
<data name="LimitedSourceGenerationTitle" xml:space="preserve">
138138
<value>Regex generator limitation reached.</value>

src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.cs.xlf

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,9 +192,9 @@
192192
<target state="translated">Vypršel časový limit modulu Regex při pokusu o porovnání vzoru se vstupním řetězcem. K tomu může dojít z celé řady důvodů, mezi které patří velká velikost vstupních dat nebo nadměrné zpětné navracení způsobené vloženými kvantifikátory, zpětnými odkazy a dalšími faktory.</target>
193193
<note />
194194
</trans-unit>
195-
<trans-unit id="RegexMethodMustHaveValidSignatureMessage">
196-
<source>GeneratedRegexAttribute method must be partial, parameterless, non-generic, non-abstract, and return Regex.</source>
197-
<target state="translated">Metoda GeneratedRegexAttribute musí být částečná, bez parametrů, neobecná, neabstraktní a návratová metoda Regex.</target>
195+
<trans-unit id="RegexMemberMustHaveValidSignatureMessage">
196+
<source>GeneratedRegexAttribute method or property must be partial, parameterless, non-generic, non-abstract, and return Regex. If a property, it must also be get-only.</source>
197+
<target state="needs-review-translation">Metoda GeneratedRegexAttribute musí být částečná, bez parametrů, neobecná, neabstraktní a návratová metoda Regex.</target>
198198
<note />
199199
</trans-unit>
200200
<trans-unit id="ReversedCharacterRange">

0 commit comments

Comments
 (0)