Skip to content

Commit ba0db14

Browse files
Copilotstephentoub
andauthored
Add CA1830 analyzer for StringBuilder.Append(new string(char, int)) (#51215)
Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: stephentoub <[email protected]>
1 parent 86e86be commit ba0db14

19 files changed

+289
-86
lines changed

src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1358,7 +1358,7 @@ Enumerable.Count() potentially enumerates the sequence while a Length/Count prop
13581358

13591359
## [CA1830](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1830): Prefer strongly-typed Append and Insert method overloads on StringBuilder
13601360

1361-
StringBuilder.Append and StringBuilder.Insert provide overloads for multiple types beyond System.String. When possible, prefer the strongly-typed overloads over using ToString() and the string-based overload.
1361+
StringBuilder.Append and StringBuilder.Insert provide overloads for multiple types beyond System.String. When possible, prefer the strongly-typed overloads over using ToString() and the string-based overload. Additionally, prefer Append(char, int) over Append(new string(char, int)).
13621362

13631363
|Item|Value|
13641364
|-|-|

src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers.sarif

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2789,7 +2789,7 @@
27892789
"CA1830": {
27902790
"id": "CA1830",
27912791
"shortDescription": "Prefer strongly-typed Append and Insert method overloads on StringBuilder",
2792-
"fullDescription": "StringBuilder.Append and StringBuilder.Insert provide overloads for multiple types beyond System.String. When possible, prefer the strongly-typed overloads over using ToString() and the string-based overload.",
2792+
"fullDescription": "StringBuilder.Append and StringBuilder.Insert provide overloads for multiple types beyond System.String. When possible, prefer the strongly-typed overloads over using ToString() and the string-based overload. Additionally, prefer Append(char, int) over Append(new string(char, int)).",
27932793
"defaultLevel": "note",
27942794
"helpUri": "https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1830",
27952795
"properties": {

src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1287,14 +1287,17 @@
12871287
<value>Prefer strongly-typed Append and Insert method overloads on StringBuilder</value>
12881288
</data>
12891289
<data name="PreferTypedStringBuilderAppendOverloadsDescription" xml:space="preserve">
1290-
<value>StringBuilder.Append and StringBuilder.Insert provide overloads for multiple types beyond System.String. When possible, prefer the strongly-typed overloads over using ToString() and the string-based overload.</value>
1290+
<value>StringBuilder.Append and StringBuilder.Insert provide overloads for multiple types beyond System.String. When possible, prefer the strongly-typed overloads over using ToString() and the string-based overload. Additionally, prefer Append(char, int) over Append(new string(char, int)).</value>
12911291
</data>
12921292
<data name="PreferTypedStringBuilderAppendOverloadsMessage" xml:space="preserve">
1293-
<value>Remove the ToString call in order to use a strongly-typed StringBuilder overload</value>
1293+
<value>Prefer strongly-typed StringBuilder overload</value>
12941294
</data>
12951295
<data name="PreferTypedStringBuilderAppendOverloadsRemoveToString" xml:space="preserve">
12961296
<value>Remove the ToString call</value>
12971297
</data>
1298+
<data name="PreferTypedStringBuilderAppendOverloadsReplaceStringConstructor" xml:space="preserve">
1299+
<value>Use StringBuilder.Append(char, int) overload</value>
1300+
</data>
12981301
<data name="PreferStringContainsOverIndexOfDescription" xml:space="preserve">
12991302
<value>Calls to 'string.IndexOf' where the result is used to check for the presence/absence of a substring can be replaced by 'string.Contains'.</value>
13001303
</data>

src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/Runtime/PreferTypedStringBuilderAppendOverloads.Fixer.cs

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,26 +29,59 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
2929
SyntaxNode root = await doc.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
3030
if (root.FindNode(context.Span) is SyntaxNode expression)
3131
{
32-
string title = MicrosoftNetCoreAnalyzersResources.PreferTypedStringBuilderAppendOverloadsRemoveToString;
33-
context.RegisterCodeFix(
34-
CodeAction.Create(title,
35-
async ct =>
36-
{
37-
SemanticModel model = await doc.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
38-
if (model.GetOperationWalkingUpParentChain(expression, cancellationToken) is IArgumentOperation arg &&
39-
arg.Value is IInvocationOperation invoke &&
40-
invoke.Instance?.Syntax is SyntaxNode replacement)
32+
SemanticModel model = await doc.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
33+
var operation = model.GetOperationWalkingUpParentChain(expression, cancellationToken);
34+
35+
// Handle ToString() case
36+
if (operation is IArgumentOperation arg &&
37+
arg.Value is IInvocationOperation invoke &&
38+
invoke.Instance?.Syntax is SyntaxNode replacement)
39+
{
40+
string title = MicrosoftNetCoreAnalyzersResources.PreferTypedStringBuilderAppendOverloadsRemoveToString;
41+
context.RegisterCodeFix(
42+
CodeAction.Create(title,
43+
async ct =>
4144
{
4245
DocumentEditor editor = await DocumentEditor.CreateAsync(doc, ct).ConfigureAwait(false);
4346
editor.ReplaceNode(expression, editor.Generator.Argument(replacement));
4447
return editor.GetChangedDocument();
45-
}
48+
},
49+
equivalenceKey: title),
50+
context.Diagnostics);
51+
}
52+
// Handle new string(char, int) case (only for Append, not Insert)
53+
else if (operation is IArgumentOperation argOp &&
54+
argOp.Value is IObjectCreationOperation objectCreation &&
55+
objectCreation.Arguments.Length == 2 &&
56+
argOp.Parent is IInvocationOperation invocationOp &&
57+
invocationOp.TargetMethod.Name == "Append")
58+
{
59+
string title = MicrosoftNetCoreAnalyzersResources.PreferTypedStringBuilderAppendOverloadsReplaceStringConstructor;
60+
context.RegisterCodeFix(
61+
CodeAction.Create(title,
62+
async ct =>
63+
{
64+
DocumentEditor editor = await DocumentEditor.CreateAsync(doc, ct).ConfigureAwait(false);
4665

47-
return doc;
48-
},
49-
equivalenceKey: title),
50-
context.Diagnostics);
66+
// Get the char and int arguments from the string constructor
67+
var charArgSyntax = objectCreation.Arguments[0].Value.Syntax;
68+
var intArgSyntax = objectCreation.Arguments[1].Value.Syntax;
69+
70+
// Append(new string(c, count)) -> Append(c, count)
71+
SyntaxNode newInvocation = editor.Generator.InvocationExpression(
72+
editor.Generator.MemberAccessExpression(
73+
invocationOp.Instance!.Syntax,
74+
"Append"),
75+
editor.Generator.Argument(charArgSyntax),
76+
editor.Generator.Argument(intArgSyntax));
77+
78+
editor.ReplaceNode(invocationOp.Syntax, newInvocation);
79+
return editor.GetChangedDocument();
80+
},
81+
equivalenceKey: title),
82+
context.Diagnostics);
83+
}
5184
}
5285
}
5386
}
54-
}
87+
}

src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/Runtime/PreferTypedStringBuilderAppendOverloads.cs

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,16 @@ public sealed override void Initialize(AnalysisContext context)
6565
s.Parameters[1].Type.SpecialType != SpecialType.System_Object &&
6666
s.Parameters[1].Type.TypeKind != TypeKind.Array);
6767

68+
// Get the Append(char, int) overload for the string constructor pattern.
69+
// Note: There is no Insert(int, char, int) overload, so we only handle Append.
70+
var appendCharIntMethod = stringBuilderType
71+
.GetMembers("Append")
72+
.OfType<IMethodSymbol>()
73+
.FirstOrDefault(s =>
74+
s.Parameters.Length == 2 &&
75+
s.Parameters[0].Type.SpecialType == SpecialType.System_Char &&
76+
s.Parameters[1].Type.SpecialType == SpecialType.System_Int32);
77+
6878
// Get the StringBuilder.Append(string)/Insert(int, string) method, for comparison purposes.
6979
var appendStringMethod = appendMethods.FirstOrDefault(s =>
7080
s.Parameters[0].Type.SpecialType == SpecialType.System_String);
@@ -97,28 +107,43 @@ public sealed override void Initialize(AnalysisContext context)
97107
return;
98108
}
99109

100-
// We're only interested if the string argument is a "string ToString()" call.
101110
if (invocation.Arguments.Length != stringParamIndex + 1 ||
102-
invocation.Arguments[stringParamIndex] is not IArgumentOperation argument ||
103-
argument.Value is not IInvocationOperation toStringInvoke ||
104-
toStringInvoke.TargetMethod.Name != "ToString" ||
105-
toStringInvoke.Type?.SpecialType != SpecialType.System_String ||
106-
!toStringInvoke.TargetMethod.Parameters.IsEmpty)
111+
invocation.Arguments[stringParamIndex] is not IArgumentOperation argument)
107112
{
108113
return;
109114
}
110115

111-
// We're only interested if the receiver type of that ToString call has a corresponding strongly-typed overload.
112-
IMethodSymbol? stronglyTypedAppend =
113-
(stringParamIndex == 0 ? appendMethods : insertMethods)
114-
.FirstOrDefault(s => s.Parameters[stringParamIndex].Type.Equals(toStringInvoke.TargetMethod.ReceiverType));
115-
if (stronglyTypedAppend is null)
116+
// Check if the string argument is a "string ToString()" call.
117+
if (argument.Value is IInvocationOperation toStringInvoke &&
118+
toStringInvoke.TargetMethod.Name == "ToString" &&
119+
toStringInvoke.Type?.SpecialType == SpecialType.System_String &&
120+
toStringInvoke.TargetMethod.Parameters.IsEmpty)
116121
{
117-
return;
118-
}
122+
// We're only interested if the receiver type of that ToString call has a corresponding strongly-typed overload.
123+
IMethodSymbol? stronglyTypedAppend =
124+
(stringParamIndex == 0 ? appendMethods : insertMethods)
125+
.FirstOrDefault(s => s.Parameters[stringParamIndex].Type.Equals(toStringInvoke.TargetMethod.ReceiverType));
126+
if (stronglyTypedAppend is null)
127+
{
128+
return;
129+
}
119130

120-
// Warn.
121-
operationContext.ReportDiagnostic(toStringInvoke.CreateDiagnostic(Rule));
131+
// Warn.
132+
operationContext.ReportDiagnostic(toStringInvoke.CreateDiagnostic(Rule));
133+
}
134+
// Check if the string argument is a "new string(char, int)" constructor call.
135+
// Note: This optimization only applies to Append, not Insert, as there's no Insert(int, char, int) overload.
136+
else if (stringParamIndex == 0 &&
137+
argument.Value is IObjectCreationOperation objectCreation &&
138+
objectCreation.Type?.SpecialType == SpecialType.System_String &&
139+
objectCreation.Arguments.Length == 2 &&
140+
objectCreation.Arguments[0].Value?.Type?.SpecialType == SpecialType.System_Char &&
141+
objectCreation.Arguments[1].Value?.Type?.SpecialType == SpecialType.System_Int32 &&
142+
appendCharIntMethod is not null)
143+
{
144+
// Warn.
145+
operationContext.ReportDiagnostic(objectCreation.CreateDiagnostic(Rule));
146+
}
122147
}, OperationKind.Invocation);
123148
});
124149
}

src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf

Lines changed: 9 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf

Lines changed: 9 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)