Skip to content

Commit ca22fda

Browse files
danmoseleyCopilot
andcommitted
Fix SYSLIB1045 code fixer generating duplicate MyRegex names across partial class declarations
When the BatchFixer applies multiple fixes concurrently, each fix sees the original compilation and independently picks the same first-available name. This causes CS0756 when two partial declarations of the same class both contain Regex calls that need fixing. The fix scans all partial declarations of the containing type for Regex call sites that would generate new property names, orders them deterministically (by file path, then position), and offsets the generated name by the current node's index in that list. Fixes #77409 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 213a41d commit ca22fda

2 files changed

Lines changed: 260 additions & 0 deletions

File tree

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

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,21 @@ operation is not (IInvocationOperation or IObjectCreationOperation))
121121
{
122122
memberName = $"{DefaultRegexPropertyName}{memberCount++}";
123123
}
124+
125+
// When the BatchFixer applies multiple fixes concurrently, each fix sees the
126+
// original compilation and picks the same first-available name. To avoid
127+
// duplicates, determine this node's position among all Regex call sites in
128+
// the type that would generate new names, and offset accordingly.
129+
int precedingCount = CountPrecedingRegexCallSites(
130+
typeSymbol, compilation, regexSymbol, nodeToFix, cancellationToken);
131+
for (int i = 0; i < precedingCount; i++)
132+
{
133+
memberName = $"{DefaultRegexPropertyName}{memberCount++}";
134+
while (GetAllMembers(typeSymbol).Any(m => m.Name == memberName))
135+
{
136+
memberName = $"{DefaultRegexPropertyName}{memberCount++}";
137+
}
138+
}
124139
}
125140

126141
// Add partial to all ancestors.
@@ -457,5 +472,73 @@ private static IEnumerable<ISymbol> GetAllMembers(INamedTypeSymbol typeSymbol)
457472
}
458473
}
459474
}
475+
476+
/// <summary>
477+
/// Counts how many Regex call sites in the same type (across all partial declarations)
478+
/// appear before the given node in a deterministic order. This ensures that when the
479+
/// BatchFixer applies fixes concurrently against the original compilation, each fix
480+
/// picks a unique generated method name.
481+
/// </summary>
482+
private static int CountPrecedingRegexCallSites(
483+
INamedTypeSymbol typeSymbol, Compilation compilation,
484+
INamedTypeSymbol regexSymbol, SyntaxNode nodeToFix,
485+
CancellationToken cancellationToken)
486+
{
487+
var callSites = new List<(string FilePath, int Position)>();
488+
489+
foreach (SyntaxReference syntaxRef in typeSymbol.DeclaringSyntaxReferences)
490+
{
491+
SyntaxNode declSyntax = syntaxRef.GetSyntax(cancellationToken);
492+
SemanticModel declModel = compilation.GetSemanticModel(syntaxRef.SyntaxTree);
493+
494+
foreach (SyntaxNode descendant in declSyntax.DescendantNodes())
495+
{
496+
if (descendant is not (InvocationExpressionSyntax or ObjectCreationExpressionSyntax or ImplicitObjectCreationExpressionSyntax))
497+
{
498+
continue;
499+
}
500+
501+
// Skip call sites inside nested type declarations — they belong to
502+
// a different type and won't affect this type's generated names.
503+
if (descendant.Ancestors().Any(a => a is TypeDeclarationSyntax && a != declSyntax))
504+
{
505+
continue;
506+
}
507+
508+
IOperation? op = declModel.GetOperation(descendant, cancellationToken);
509+
bool isRegexCall = op switch
510+
{
511+
IInvocationOperation inv => inv.TargetMethod.IsStatic &&
512+
SymbolEqualityComparer.Default.Equals(inv.TargetMethod.ContainingType, regexSymbol),
513+
IObjectCreationOperation create => SymbolEqualityComparer.Default.Equals(create.Type, regexSymbol),
514+
_ => false
515+
};
516+
517+
if (isRegexCall)
518+
{
519+
callSites.Add((syntaxRef.SyntaxTree.FilePath ?? string.Empty, descendant.SpanStart));
520+
}
521+
}
522+
}
523+
524+
if (callSites.Count <= 1)
525+
{
526+
return 0;
527+
}
528+
529+
callSites.Sort((a, b) =>
530+
{
531+
int cmp = StringComparer.Ordinal.Compare(a.FilePath, b.FilePath);
532+
return cmp != 0 ? cmp : a.Position.CompareTo(b.Position);
533+
});
534+
535+
string currentFilePath = nodeToFix.SyntaxTree.FilePath ?? string.Empty;
536+
int currentPosition = nodeToFix.SpanStart;
537+
538+
int index = callSites.FindIndex(c =>
539+
StringComparer.Ordinal.Equals(c.FilePath, currentFilePath) && c.Position == currentPosition);
540+
541+
return index > 0 ? index : 0;
542+
}
460543
}
461544
}

src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/UpgradeToGeneratedRegexAnalyzerTests.cs

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1085,6 +1085,183 @@ public static void Main()
10851085
}.RunAsync();
10861086
}
10871087

1088+
[Fact]
1089+
public async Task CodeFixerGeneratesUniqueNamesAcrossPartialClassDeclarations()
1090+
{
1091+
string test = @"using System.Text.RegularExpressions;
1092+
1093+
internal partial class C
1094+
{
1095+
public static Regex A()
1096+
{
1097+
var r = [|new Regex|](""abc"");
1098+
return r;
1099+
}
1100+
}
1101+
1102+
internal partial class C
1103+
{
1104+
public static Regex B()
1105+
{
1106+
var r = [|new Regex|](""def"");
1107+
return r;
1108+
}
1109+
}
1110+
";
1111+
1112+
string fixedSource = @"using System.Text.RegularExpressions;
1113+
1114+
internal partial class C
1115+
{
1116+
public static Regex A()
1117+
{
1118+
var r = MyRegex;
1119+
return r;
1120+
}
1121+
1122+
[GeneratedRegex(""abc"")]
1123+
private static partial Regex MyRegex { get; }
1124+
}
1125+
1126+
internal partial class C
1127+
{
1128+
public static Regex B()
1129+
{
1130+
var r = MyRegex1;
1131+
return r;
1132+
}
1133+
1134+
[GeneratedRegex(""def"")]
1135+
private static partial Regex MyRegex1 { get; }
1136+
}
1137+
";
1138+
1139+
await VerifyCS.VerifyCodeFixAsync(test, fixedSource);
1140+
}
1141+
1142+
[Fact]
1143+
public async Task CodeFixerGeneratesUniqueNamesAcrossThreePartialClassDeclarations()
1144+
{
1145+
string test = @"using System.Text.RegularExpressions;
1146+
1147+
internal partial class C
1148+
{
1149+
public static Regex A()
1150+
{
1151+
return [|new Regex|](""aaa"");
1152+
}
1153+
}
1154+
1155+
internal partial class C
1156+
{
1157+
public static Regex B()
1158+
{
1159+
return [|new Regex|](""bbb"");
1160+
}
1161+
}
1162+
1163+
internal partial class C
1164+
{
1165+
public static Regex D()
1166+
{
1167+
return [|new Regex|](""ccc"");
1168+
}
1169+
}
1170+
";
1171+
1172+
string fixedSource = @"using System.Text.RegularExpressions;
1173+
1174+
internal partial class C
1175+
{
1176+
public static Regex A()
1177+
{
1178+
return MyRegex;
1179+
}
1180+
1181+
[GeneratedRegex(""aaa"")]
1182+
private static partial Regex MyRegex { get; }
1183+
}
1184+
1185+
internal partial class C
1186+
{
1187+
public static Regex B()
1188+
{
1189+
return MyRegex1;
1190+
}
1191+
1192+
[GeneratedRegex(""bbb"")]
1193+
private static partial Regex MyRegex1 { get; }
1194+
}
1195+
1196+
internal partial class C
1197+
{
1198+
public static Regex D()
1199+
{
1200+
return MyRegex2;
1201+
}
1202+
1203+
[GeneratedRegex(""ccc"")]
1204+
private static partial Regex MyRegex2 { get; }
1205+
}
1206+
";
1207+
1208+
await VerifyCS.VerifyCodeFixAsync(test, fixedSource);
1209+
}
1210+
1211+
[Fact]
1212+
public async Task CodeFixerGeneratesUniqueNamesWhenExistingMemberNamedMyRegex()
1213+
{
1214+
string test = @"using System.Text.RegularExpressions;
1215+
1216+
internal partial class C
1217+
{
1218+
private static void MyRegex() { }
1219+
1220+
public static Regex A()
1221+
{
1222+
return [|new Regex|](""abc"");
1223+
}
1224+
}
1225+
1226+
internal partial class C
1227+
{
1228+
public static Regex B()
1229+
{
1230+
return [|new Regex|](""def"");
1231+
}
1232+
}
1233+
";
1234+
1235+
string fixedSource = @"using System.Text.RegularExpressions;
1236+
1237+
internal partial class C
1238+
{
1239+
private static void MyRegex() { }
1240+
1241+
public static Regex A()
1242+
{
1243+
return MyRegex1;
1244+
}
1245+
1246+
[GeneratedRegex(""abc"")]
1247+
private static partial Regex MyRegex1 { get; }
1248+
}
1249+
1250+
internal partial class C
1251+
{
1252+
public static Regex B()
1253+
{
1254+
return MyRegex2;
1255+
}
1256+
1257+
[GeneratedRegex(""def"")]
1258+
private static partial Regex MyRegex2 { get; }
1259+
}
1260+
";
1261+
1262+
await VerifyCS.VerifyCodeFixAsync(test, fixedSource);
1263+
}
1264+
10881265
[Fact]
10891266
public async Task CodeFixerSupportsNamedParameters()
10901267
{

0 commit comments

Comments
 (0)