Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1358,7 +1358,7 @@ Enumerable.Count() potentially enumerates the sequence while a Length/Count prop

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

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.
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)).

|Item|Value|
|-|-|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2789,7 +2789,7 @@
"CA1830": {
"id": "CA1830",
"shortDescription": "Prefer strongly-typed Append and Insert method overloads on StringBuilder",
"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.",
"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)).",
"defaultLevel": "note",
"helpUri": "https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1830",
"properties": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1287,14 +1287,17 @@
<value>Prefer strongly-typed Append and Insert method overloads on StringBuilder</value>
</data>
<data name="PreferTypedStringBuilderAppendOverloadsDescription" xml:space="preserve">
<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>
<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>
</data>
<data name="PreferTypedStringBuilderAppendOverloadsMessage" xml:space="preserve">
<value>Remove the ToString call in order to use a strongly-typed StringBuilder overload</value>
<value>Prefer strongly-typed StringBuilder overload</value>
</data>
<data name="PreferTypedStringBuilderAppendOverloadsRemoveToString" xml:space="preserve">
<value>Remove the ToString call</value>
</data>
<data name="PreferTypedStringBuilderAppendOverloadsReplaceStringConstructor" xml:space="preserve">
<value>Use StringBuilder.Append(char, int) overload</value>
</data>
<data name="PreferStringContainsOverIndexOfDescription" xml:space="preserve">
<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>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,26 +29,59 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
SyntaxNode root = await doc.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
if (root.FindNode(context.Span) is SyntaxNode expression)
{
string title = MicrosoftNetCoreAnalyzersResources.PreferTypedStringBuilderAppendOverloadsRemoveToString;
context.RegisterCodeFix(
CodeAction.Create(title,
async ct =>
{
SemanticModel model = await doc.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
if (model.GetOperationWalkingUpParentChain(expression, cancellationToken) is IArgumentOperation arg &&
arg.Value is IInvocationOperation invoke &&
invoke.Instance?.Syntax is SyntaxNode replacement)
SemanticModel model = await doc.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
var operation = model.GetOperationWalkingUpParentChain(expression, cancellationToken);

// Handle ToString() case
if (operation is IArgumentOperation arg &&
arg.Value is IInvocationOperation invoke &&
invoke.Instance?.Syntax is SyntaxNode replacement)
{
string title = MicrosoftNetCoreAnalyzersResources.PreferTypedStringBuilderAppendOverloadsRemoveToString;
context.RegisterCodeFix(
CodeAction.Create(title,
async ct =>
{
DocumentEditor editor = await DocumentEditor.CreateAsync(doc, ct).ConfigureAwait(false);
editor.ReplaceNode(expression, editor.Generator.Argument(replacement));
return editor.GetChangedDocument();
}
},
equivalenceKey: title),
context.Diagnostics);
}
// Handle new string(char, int) case (only for Append, not Insert)
else if (operation is IArgumentOperation argOp &&
argOp.Value is IObjectCreationOperation objectCreation &&
objectCreation.Arguments.Length == 2 &&
argOp.Parent is IInvocationOperation invocationOp &&
invocationOp.TargetMethod.Name == "Append")
{
string title = MicrosoftNetCoreAnalyzersResources.PreferTypedStringBuilderAppendOverloadsReplaceStringConstructor;
context.RegisterCodeFix(
CodeAction.Create(title,
async ct =>
{
DocumentEditor editor = await DocumentEditor.CreateAsync(doc, ct).ConfigureAwait(false);

return doc;
},
equivalenceKey: title),
context.Diagnostics);
// Get the char and int arguments from the string constructor
var charArgSyntax = objectCreation.Arguments[0].Value.Syntax;
var intArgSyntax = objectCreation.Arguments[1].Value.Syntax;

// Append(new string(c, count)) -> Append(c, count)
SyntaxNode newInvocation = editor.Generator.InvocationExpression(
editor.Generator.MemberAccessExpression(
invocationOp.Instance!.Syntax,
"Append"),
editor.Generator.Argument(charArgSyntax),
editor.Generator.Argument(intArgSyntax));

editor.ReplaceNode(invocationOp.Syntax, newInvocation);
return editor.GetChangedDocument();
},
equivalenceKey: title),
context.Diagnostics);
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,16 @@ public sealed override void Initialize(AnalysisContext context)
s.Parameters[1].Type.SpecialType != SpecialType.System_Object &&
s.Parameters[1].Type.TypeKind != TypeKind.Array);

// Get the Append(char, int) overload for the string constructor pattern.
// Note: There is no Insert(int, char, int) overload, so we only handle Append.
var appendCharIntMethod = stringBuilderType
.GetMembers("Append")
.OfType<IMethodSymbol>()
.FirstOrDefault(s =>
s.Parameters.Length == 2 &&
s.Parameters[0].Type.SpecialType == SpecialType.System_Char &&
s.Parameters[1].Type.SpecialType == SpecialType.System_Int32);

// Get the StringBuilder.Append(string)/Insert(int, string) method, for comparison purposes.
var appendStringMethod = appendMethods.FirstOrDefault(s =>
s.Parameters[0].Type.SpecialType == SpecialType.System_String);
Expand Down Expand Up @@ -97,28 +107,43 @@ public sealed override void Initialize(AnalysisContext context)
return;
}

// We're only interested if the string argument is a "string ToString()" call.
if (invocation.Arguments.Length != stringParamIndex + 1 ||
invocation.Arguments[stringParamIndex] is not IArgumentOperation argument ||
argument.Value is not IInvocationOperation toStringInvoke ||
toStringInvoke.TargetMethod.Name != "ToString" ||
toStringInvoke.Type?.SpecialType != SpecialType.System_String ||
!toStringInvoke.TargetMethod.Parameters.IsEmpty)
invocation.Arguments[stringParamIndex] is not IArgumentOperation argument)
{
return;
}

// We're only interested if the receiver type of that ToString call has a corresponding strongly-typed overload.
IMethodSymbol? stronglyTypedAppend =
(stringParamIndex == 0 ? appendMethods : insertMethods)
.FirstOrDefault(s => s.Parameters[stringParamIndex].Type.Equals(toStringInvoke.TargetMethod.ReceiverType));
if (stronglyTypedAppend is null)
// Check if the string argument is a "string ToString()" call.
if (argument.Value is IInvocationOperation toStringInvoke &&
toStringInvoke.TargetMethod.Name == "ToString" &&
toStringInvoke.Type?.SpecialType == SpecialType.System_String &&
toStringInvoke.TargetMethod.Parameters.IsEmpty)
{
return;
}
// We're only interested if the receiver type of that ToString call has a corresponding strongly-typed overload.
IMethodSymbol? stronglyTypedAppend =
(stringParamIndex == 0 ? appendMethods : insertMethods)
.FirstOrDefault(s => s.Parameters[stringParamIndex].Type.Equals(toStringInvoke.TargetMethod.ReceiverType));
if (stronglyTypedAppend is null)
{
return;
}

// Warn.
operationContext.ReportDiagnostic(toStringInvoke.CreateDiagnostic(Rule));
// Warn.
operationContext.ReportDiagnostic(toStringInvoke.CreateDiagnostic(Rule));
}
// Check if the string argument is a "new string(char, int)" constructor call.
// Note: This optimization only applies to Append, not Insert, as there's no Insert(int, char, int) overload.
else if (stringParamIndex == 0 &&
argument.Value is IObjectCreationOperation objectCreation &&
objectCreation.Type?.SpecialType == SpecialType.System_String &&
objectCreation.Arguments.Length == 2 &&
objectCreation.Arguments[0].Value?.Type?.SpecialType == SpecialType.System_Char &&
objectCreation.Arguments[1].Value?.Type?.SpecialType == SpecialType.System_Int32 &&
appendCharIntMethod is not null)
{
// Warn.
operationContext.ReportDiagnostic(objectCreation.CreateDiagnostic(Rule));
}
}, OperationKind.Invocation);
});
}
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading