Skip to content
Closed
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
2 changes: 2 additions & 0 deletions src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ CA1858 | Performance | Info | UseStartsWithInsteadOfIndexOfComparisonWithZero, [
CA1859 | Performance | Info | UseConcreteTypeAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1859)
CA1860 | Performance | Info | PreferLengthCountIsEmptyOverAnyAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1860)
CA1861 | Performance | Info | AvoidConstArrays, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1861)
CA1862 | Performance | Info | UseCompositeFormatAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1862)
CA1863 | Performance | Hidden | UseCompositeFormatAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1863)
CA2021 | Reliability | Warning | DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2021)

### Removed Rules
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2054,4 +2054,22 @@ Widening and user defined conversions are not supported with generic types.</val
<data name="PreferIsEmptyOverAnyMessage" xml:space="preserve">
<value>Prefer an 'IsEmpty' check rather than using 'Any()', both for clarity and for performance</value>
</data>
<data name="UseCompositeFormatTitle" xml:space="preserve">
<value>Use 'CompositeFormat'</value>
</data>
<data name="UseCompositeFormatMessage" xml:space="preserve">
<value>Cache a 'CompositeFormat' for repeated use in this formatting operation</value>
</data>
<data name="UseCompositeFormatDescription" xml:space="preserve">
<value>Cache and use a 'CompositeFormat' instance as the argument to this formatting operation, rather than passing in the original format string. This reduces the cost of the formatting operation.</value>
</data>
<data name="UseInterpolatedStringTitle" xml:space="preserve">
<value>Use an interpolated string</value>
</data>
<data name="UseInterpolatedStringMessage" xml:space="preserve">
<value>Use an interpolated string to perform the operation more efficiently</value>
</data>
<data name="UseInterpolatedStringDescription" xml:space="preserve">
<value>Using an interpolated string is both more concise and more efficient than performing the whole formatting operation at run-time.</value>
</data>
</root>
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

using System;
using System.Collections.Immutable;
using System.Linq;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Microsoft.NetCore.Analyzers.Performance
{
using static MicrosoftNetCoreAnalyzersResources;

/// <summary>
/// CA1862: <inheritdoc cref="UseCompositeFormatTitle"/>
/// CA1863: <inheritdoc cref="UseInterpolatedStringTitle"/>
/// </summary>
/// <remarks>
/// Roslyn already provides a refactoring for finding string.Format calls with literal string formats
/// and converting them to use string interpolation. This analyzer instead focuses on non-literal / const
/// arguments.
/// </remarks>
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
public sealed class UseCompositeFormatAnalyzer : DiagnosticAnalyzer
{
internal static readonly DiagnosticDescriptor UseCompositeFormatRule = DiagnosticDescriptorHelper.Create("CA1862",
CreateLocalizableResourceString(nameof(UseCompositeFormatTitle)),
CreateLocalizableResourceString(nameof(UseCompositeFormatMessage)),
DiagnosticCategory.Performance,
RuleLevel.IdeSuggestion,
CreateLocalizableResourceString(nameof(UseCompositeFormatDescription)),
isPortedFxCopRule: false,
isDataflowRule: false);

internal static readonly DiagnosticDescriptor UseInterpolatedStringRule = DiagnosticDescriptorHelper.Create("CA1863",
CreateLocalizableResourceString(nameof(UseInterpolatedStringTitle)),
CreateLocalizableResourceString(nameof(UseInterpolatedStringMessage)),
DiagnosticCategory.Performance,
RuleLevel.IdeHidden_BulkConfigurable,
CreateLocalizableResourceString(nameof(UseInterpolatedStringDescription)),
isPortedFxCopRule: false,
isDataflowRule: false);

internal const string StringIndexPropertyName = "StringIndex";

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
ImmutableArray.Create(UseCompositeFormatRule, UseInterpolatedStringRule);

public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.RegisterCompilationStartAction(compilationContext =>
{
INamedTypeSymbol stringType = compilationContext.Compilation.GetSpecialType(SpecialType.System_String);

// Get the types for CompositeFormat, IFormatProvider, and StringBuilder. If we can't, bail.
if (!compilationContext.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemTextCompositeFormat, out INamedTypeSymbol? compositeFormatType) ||
!compilationContext.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemIFormatProvider, out INamedTypeSymbol? formatProviderType) ||
!compilationContext.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemTextStringBuilder, out INamedTypeSymbol? stringBuilderType))
{
return;
}

// Process all calls to string.Format, assuming we can find all the members we'd use as replacements.
IMethodSymbol[] formatCompositeMethods = stringType.GetMembers("Format").OfType<IMethodSymbol>()
.Where(m => m.IsStatic &&
m.Parameters.Length >= 3 &&
SymbolEqualityComparer.Default.Equals(m.Parameters[0].Type, formatProviderType) &&
SymbolEqualityComparer.Default.Equals(m.Parameters[1].Type, compositeFormatType)).ToArray();
if (HasAllCompositeFormatMethods(formatCompositeMethods))
{
compilationContext.RegisterOperationAction(
CreateAnalysisAction(isStatic: true, stringType, "Format", formatProviderType), OperationKind.Invocation);
}

// Process all calls to StringBuilder.AppendFormat, assuming we can find all the members we'd use as replacements.
IMethodSymbol[] appendFormatCompositeMethods = stringBuilderType.GetMembers("AppendFormat").OfType<IMethodSymbol>()
.Where(m => !m.IsStatic &&
m.Parameters.Length >= 3 &&
SymbolEqualityComparer.Default.Equals(m.Parameters[0].Type, formatProviderType) &&
SymbolEqualityComparer.Default.Equals(m.Parameters[1].Type, compositeFormatType)).ToArray();
if (HasAllCompositeFormatMethods(appendFormatCompositeMethods))
{
compilationContext.RegisterOperationAction(
CreateAnalysisAction(isStatic: false, stringBuilderType, "AppendFormat", formatProviderType), OperationKind.Invocation);
}
});
}

/// <summary>Creates a delegate to register with RegisterOperationAction and that flags all of the relevate format string parameters that warrant replacing.</summary>
/// <param name="isStatic">Whether the target methods are static; true for string.Format, false for StringBuilder.AppendFormat.</param>
/// <param name="containingType">The symbol for the containing type, either for string or StringBuilder.</param>
/// <param name="methodName">The name of the target method, either "Format" or "AppendFormat".</param>
/// <param name="formatProviderType">The symbol for IFormatProvider.</param>
/// <returns></returns>
private static Action<OperationAnalysisContext> CreateAnalysisAction(bool isStatic, ITypeSymbol containingType, string methodName, ITypeSymbol formatProviderType)
{
return operationContext =>
{
IInvocationOperation invocation = (IInvocationOperation)operationContext.Operation;
IMethodSymbol targetMethod = invocation.TargetMethod;

// Much match the specified method shape
if (targetMethod.IsStatic != isStatic ||
!SymbolEqualityComparer.Default.Equals(targetMethod.ContainingType, containingType) ||
targetMethod.Name != methodName)
{
return;
}

// Must accept a string format rather than CompositFormat format
int stringIndex;
ImmutableArray<IParameterSymbol> parameters = targetMethod.Parameters;
if (parameters.Length >= 1 && parameters[0].Type.SpecialType == SpecialType.System_String)
{
stringIndex = 0;
}
else if (parameters.Length >= 2 &&
parameters[1].Type.SpecialType == SpecialType.System_String &&
SymbolEqualityComparer.Default.Equals(parameters[0].Type, formatProviderType))
{
stringIndex = 1;
}
else
{
return;
}

// Get the argument for the format string
if (!invocation.Arguments.TryGetArgumentForParameterAtIndex(stringIndex, out IArgumentOperation? arg))
{
return;
}

// If the argument contains anything that references local state, we can't recommend extracting that out
// into a statically-cached CompositeFormat. We instead stick to the easy cases which should also be the
// most common, e.g. literals, static references, etc.
IOperation stringArg = arg.Value.WalkDownConversion();
if (IsStringLiteralOrStaticReference(stringArg))
{
if (stringArg.Kind == OperationKind.Literal)
{
// If the expression is a literal, the best route is to make it an interpolated string.
// That entails changing the entire method call, so report the diagnostic on the whole invocation.
operationContext.ReportDiagnostic(invocation.CreateDiagnostic(UseInterpolatedStringRule));
}
else
{
// If the expression is a static reference, we can replace just the format string argument
// with a CompositeFormat, so report the diagnostic on just that argument.
operationContext.ReportDiagnostic(stringArg.CreateDiagnostic(
UseCompositeFormatRule,
properties: ImmutableDictionary<string, string?>.Empty.Add(StringIndexPropertyName, stringIndex.ToString())));
}
}
};
}

/// <summary>Determines whether the expression is something trivially lifted out of the nenber body.</summary>
private static bool IsStringLiteralOrStaticReference(IOperation operation)
{
if (operation.Type.SpecialType != SpecialType.System_String)
{
return false;
}

if (operation.Kind == OperationKind.Literal)
{
return true;
}

if (operation.Kind == OperationKind.FieldReference)
{
return ((IFieldReferenceOperation)operation).Field.IsStatic;
}

if (operation.Kind == OperationKind.PropertyReference)
{
return ((IPropertyReferenceOperation)operation).Property.IsStatic;
}

if (operation.Kind == OperationKind.Invocation)
{
IInvocationOperation invocation = (IInvocationOperation)operation;
if (invocation.TargetMethod.IsStatic)
{
foreach (IArgumentOperation? arg in invocation.Arguments)
{
if (!arg.ConstantValue.HasValue && !IsStringLiteralOrStaticReference(arg.Value))
{
return false;
}
}

return true;
}
}

return false;
}

/// <summary>Validates that all of the required CompositeFormat-based methods exist in the specified set.</summary>
private static bool HasAllCompositeFormatMethods(IMethodSymbol[] methods)
{
// (IFormatProvider, CompositeFormat, T1)
if (!methods.Any(m => m.IsGenericMethod &&
m.Parameters.Length == 3 &&
m.TypeParameters.Length == 1 &&
SymbolEqualityComparer.Default.Equals(m.TypeParameters[0], m.Parameters[2].Type)))
{
return false;
}

// (IFormatProvider, CompositeFormat, T1, T2)
if (!methods.Any(m => m.IsGenericMethod &&
m.Parameters.Length == 4 &&
m.TypeParameters.Length == 2 &&
SymbolEqualityComparer.Default.Equals(m.TypeParameters[0], m.Parameters[2].Type) &&
SymbolEqualityComparer.Default.Equals(m.TypeParameters[1], m.Parameters[3].Type)))
{
return false;
}

// (IFormatProvider, CompositeFormat, T1, T2, T3)
if (!methods.Any(m => m.IsGenericMethod &&
m.Parameters.Length == 5 &&
m.TypeParameters.Length == 3 &&
SymbolEqualityComparer.Default.Equals(m.TypeParameters[0], m.Parameters[2].Type) &&
SymbolEqualityComparer.Default.Equals(m.TypeParameters[1], m.Parameters[3].Type) &&
SymbolEqualityComparer.Default.Equals(m.TypeParameters[2], m.Parameters[4].Type)))
{
return false;
}

// (IFormatProvider, CompositeFormat, object[])
if (!methods.Any(m => m.Parameters.Length == 3 &&
m.Parameters[2].Type.Kind == SymbolKind.ArrayType))
{
return false;
}

// All relevant methods exist.
return true;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2748,6 +2748,21 @@ Obecné přetypování (IL unbox.any) používané sekvencí vrácenou metodou E
<target state="translated">Použijte ThrowIfCancellationRequested</target>
<note />
</trans-unit>
<trans-unit id="UseCompositeFormatDescription">
<source>Cache and use a 'CompositeFormat' instance as the argument to this formatting operation, rather than passing in the original format string. This reduces the cost of the formatting operation.</source>
<target state="new">Cache and use a 'CompositeFormat' instance as the argument to this formatting operation, rather than passing in the original format string. This reduces the cost of the formatting operation.</target>
<note />
</trans-unit>
<trans-unit id="UseCompositeFormatMessage">
<source>Cache a 'CompositeFormat' for repeated use in this formatting operation</source>
<target state="new">Cache a 'CompositeFormat' for repeated use in this formatting operation</target>
<note />
</trans-unit>
<trans-unit id="UseCompositeFormatTitle">
<source>Use 'CompositeFormat'</source>
<target state="new">Use 'CompositeFormat'</target>
<note />
</trans-unit>
<trans-unit id="UseConcreteTypeDescription">
<source>Using concrete types avoids virtual or interface call overhead and enables inlining.</source>
<target state="translated">Použití konkrétních typů zabraňuje režii virtuálního volání nebo volání rozhraní a umožňuje vkládání.</target>
Expand Down Expand Up @@ -2883,6 +2898,21 @@ Obecné přetypování (IL unbox.any) používané sekvencí vrácenou metodou E
<target state="translated">Použít indexer</target>
<note />
</trans-unit>
<trans-unit id="UseInterpolatedStringDescription">
<source>Using an interpolated string is both more concise and more efficient than performing the whole formatting operation at run-time.</source>
<target state="new">Using an interpolated string is both more concise and more efficient than performing the whole formatting operation at run-time.</target>
<note />
</trans-unit>
<trans-unit id="UseInterpolatedStringMessage">
<source>Use an interpolated string to perform the operation more efficiently</source>
<target state="new">Use an interpolated string to perform the operation more efficiently</target>
<note />
</trans-unit>
<trans-unit id="UseInterpolatedStringTitle">
<source>Use an interpolated string</source>
<target state="new">Use an interpolated string</target>
<note />
</trans-unit>
<trans-unit id="UseInvariantVersion">
<source>Use an invariant version</source>
<target state="translated">Použít neutrální verzi</target>
Expand Down
Loading