Skip to content

Commit

Permalink
Add CompositeFormat analyzer
Browse files Browse the repository at this point in the history
  • Loading branch information
stephentoub committed Jun 7, 2023
1 parent 2b6ab8d commit 66c5085
Show file tree
Hide file tree
Showing 22 changed files with 969 additions and 5 deletions.
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,230 @@
// 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"/>
/// </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 const string StringIndexPropertyName = "StringIndex";

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

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 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 member body.</summary>
private static bool IsStringLiteralOrStaticReference(IOperation operation, bool allowLiteral = false)
{
if (operation.Type.SpecialType != SpecialType.System_String)
{
return false;
}

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

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, allowLiteral: true))
{
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
Original file line number Diff line number Diff line change
Expand Up @@ -2748,6 +2748,21 @@ Erweiterungen und benutzerdefinierte Konvertierungen werden bei generischen Type
<target state="translated">"ThrowIfCancellationRequested()" verwenden</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">Die Verwendung von konkreten Typen vermeidet den Mehraufwand für virtuelle Aufrufe oder Schnittstellenaufrufe und ermöglicht das Inlining.</target>
Expand Down Expand Up @@ -2883,6 +2898,21 @@ Erweiterungen und benutzerdefinierte Konvertierungen werden bei generischen Type
<target state="translated">Indexer verwenden</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">Verwenden Sie eine unveränderliche Version</target>
Expand Down
Loading

0 comments on commit 66c5085

Please sign in to comment.