Skip to content

Commit 0b7952e

Browse files
committed
Add CompositeFormat analyzer
1 parent 2b6ab8d commit 0b7952e

23 files changed

+766
-5
lines changed

src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ CA1858 | Performance | Info | UseStartsWithInsteadOfIndexOfComparisonWithZero, [
1414
CA1859 | Performance | Info | UseConcreteTypeAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1859)
1515
CA1860 | Performance | Info | PreferLengthCountIsEmptyOverAnyAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1860)
1616
CA1861 | Performance | Info | AvoidConstArrays, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1861)
17+
CA1862 | Performance | Info | UseCompositeFormatAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1862)
1718
CA2021 | Reliability | Warning | DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2021)
1819

1920
### Removed Rules

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2054,4 +2054,13 @@ Widening and user defined conversions are not supported with generic types.</val
20542054
<data name="PreferIsEmptyOverAnyMessage" xml:space="preserve">
20552055
<value>Prefer an 'IsEmpty' check rather than using 'Any()', both for clarity and for performance</value>
20562056
</data>
2057+
<data name="UseCompositeFormatTitle" xml:space="preserve">
2058+
<value>Use 'CompositeFormat'</value>
2059+
</data>
2060+
<data name="UseCompositeFormatMessage" xml:space="preserve">
2061+
<value>Cache a 'CompositeFormat' for repeated use in this formatting operation</value>
2062+
</data>
2063+
<data name="UseCompositeFormatDescription" xml:space="preserve">
2064+
<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>
2065+
</data>
20572066
</root>
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.
2+
3+
using System;
4+
using System.Collections.Immutable;
5+
using System.Linq;
6+
using Analyzer.Utilities;
7+
using Analyzer.Utilities.Extensions;
8+
using Microsoft.CodeAnalysis;
9+
using Microsoft.CodeAnalysis.Diagnostics;
10+
using Microsoft.CodeAnalysis.Operations;
11+
12+
namespace Microsoft.NetCore.Analyzers.Performance
13+
{
14+
using static MicrosoftNetCoreAnalyzersResources;
15+
16+
/// <summary>
17+
/// CA1862: <inheritdoc cref="UseCompositeFormatTitle"/>
18+
/// </summary>
19+
/// <remarks>
20+
/// Roslyn already provides a refactoring for finding string.Format calls with literal string formats
21+
/// and converting them to use string interpolation. This analyzer instead focuses on non-literal / const
22+
/// arguments.
23+
/// </remarks>
24+
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
25+
public sealed class UseCompositeFormatAnalyzer : DiagnosticAnalyzer
26+
{
27+
internal static readonly DiagnosticDescriptor UseCompositeFormatRule = DiagnosticDescriptorHelper.Create("CA1862",
28+
CreateLocalizableResourceString(nameof(UseCompositeFormatTitle)),
29+
CreateLocalizableResourceString(nameof(UseCompositeFormatMessage)),
30+
DiagnosticCategory.Performance,
31+
RuleLevel.IdeSuggestion,
32+
CreateLocalizableResourceString(nameof(UseCompositeFormatDescription)),
33+
isPortedFxCopRule: false,
34+
isDataflowRule: false);
35+
36+
internal const string StringIndexPropertyName = "StringIndex";
37+
38+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
39+
ImmutableArray.Create(UseCompositeFormatRule);
40+
41+
public override void Initialize(AnalysisContext context)
42+
{
43+
context.EnableConcurrentExecution();
44+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
45+
context.RegisterCompilationStartAction(compilationContext =>
46+
{
47+
INamedTypeSymbol stringType = compilationContext.Compilation.GetSpecialType(SpecialType.System_String);
48+
49+
// Get the types for CompositeFormat, IFormatProvider, and StringBuilder. If we can't, bail.
50+
if (!compilationContext.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemTextCompositeFormat, out INamedTypeSymbol? compositeFormatType) ||
51+
!compilationContext.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemIFormatProvider, out INamedTypeSymbol? formatProviderType) ||
52+
!compilationContext.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemTextStringBuilder, out INamedTypeSymbol? stringBuilderType))
53+
{
54+
return;
55+
}
56+
57+
// Process all calls to string.Format, assuming we can find all the members we'd use as replacements.
58+
IMethodSymbol[] formatCompositeMethods = stringType.GetMembers("Format").OfType<IMethodSymbol>()
59+
.Where(m => m.IsStatic &&
60+
m.Parameters.Length >= 3 &&
61+
SymbolEqualityComparer.Default.Equals(m.Parameters[0].Type, formatProviderType) &&
62+
SymbolEqualityComparer.Default.Equals(m.Parameters[1].Type, compositeFormatType)).ToArray();
63+
if (HasAllCompositeFormatMethods(formatCompositeMethods))
64+
{
65+
compilationContext.RegisterOperationAction(
66+
CreateAnalysisAction(isStatic: true, stringType, "Format", formatProviderType), OperationKind.Invocation);
67+
}
68+
69+
// Process all calls to StringBuilder.AppendFormat, assuming we can find all the members we'd use as replacements.
70+
IMethodSymbol[] appendFormatCompositeMethods = stringBuilderType.GetMembers("AppendFormat").OfType<IMethodSymbol>()
71+
.Where(m => !m.IsStatic &&
72+
m.Parameters.Length >= 3 &&
73+
SymbolEqualityComparer.Default.Equals(m.Parameters[0].Type, formatProviderType) &&
74+
SymbolEqualityComparer.Default.Equals(m.Parameters[1].Type, compositeFormatType)).ToArray();
75+
if (HasAllCompositeFormatMethods(appendFormatCompositeMethods))
76+
{
77+
compilationContext.RegisterOperationAction(
78+
CreateAnalysisAction(isStatic: false, stringBuilderType, "AppendFormat", formatProviderType), OperationKind.Invocation);
79+
}
80+
});
81+
}
82+
83+
/// <summary>Creates a delegate to register with RegisterOperationAction and that flags all of the relevate format string parameters that warrant replacing.</summary>
84+
/// <param name="isStatic">Whether the target methods are static; true for string.Format, false for StringBuilder.AppendFormat.</param>
85+
/// <param name="containingType">The symbol for the containing type, either for string or StringBuilder.</param>
86+
/// <param name="methodName">The name of the target method, either "Format" or "AppendFormat".</param>
87+
/// <param name="formatProviderType">The symbol for IFormatProvider.</param>
88+
/// <returns></returns>
89+
private static Action<OperationAnalysisContext> CreateAnalysisAction(bool isStatic, ITypeSymbol containingType, string methodName, ITypeSymbol formatProviderType)
90+
{
91+
return operationContext =>
92+
{
93+
IInvocationOperation invocation = (IInvocationOperation)operationContext.Operation;
94+
IMethodSymbol targetMethod = invocation.TargetMethod;
95+
96+
// Much match the specified method shape
97+
if (targetMethod.IsStatic != isStatic ||
98+
!SymbolEqualityComparer.Default.Equals(targetMethod.ContainingType, containingType) ||
99+
targetMethod.Name != methodName)
100+
{
101+
return;
102+
}
103+
104+
// Must accept a string format rather than CompositFormat format
105+
int stringIndex;
106+
ImmutableArray<IParameterSymbol> parameters = targetMethod.Parameters;
107+
if (parameters.Length >= 1 && parameters[0].Type.SpecialType == SpecialType.System_String)
108+
{
109+
stringIndex = 0;
110+
}
111+
else if (parameters.Length >= 2 &&
112+
parameters[1].Type.SpecialType == SpecialType.System_String &&
113+
SymbolEqualityComparer.Default.Equals(parameters[0].Type, formatProviderType))
114+
{
115+
stringIndex = 1;
116+
}
117+
else
118+
{
119+
return;
120+
}
121+
122+
// Get the argument for the format string
123+
if (!invocation.Arguments.TryGetArgumentForParameterAtIndex(stringIndex, out IArgumentOperation? arg))
124+
{
125+
return;
126+
}
127+
128+
// If the argument contains anything that references local state, we can't recommend extracting that out
129+
// into a statically-cached CompositeFormat. We instead stick to the easy cases which should also be the
130+
// most common, e.g. literals, static references, etc.
131+
IOperation stringArg = arg.Value.WalkDownConversion();
132+
if (IsStringLiteralOrStaticReference(stringArg))
133+
{
134+
// If the expression is a static reference, we can replace just the format string argument
135+
// with a CompositeFormat, so report the diagnostic on just that argument.
136+
operationContext.ReportDiagnostic(stringArg.CreateDiagnostic(
137+
UseCompositeFormatRule,
138+
properties: ImmutableDictionary<string, string?>.Empty.Add(StringIndexPropertyName, stringIndex.ToString())));
139+
}
140+
};
141+
}
142+
143+
/// <summary>Determines whether the expression is something trivially lifted out of the member body.</summary>
144+
private static bool IsStringLiteralOrStaticReference(IOperation operation, bool allowLiteral = false)
145+
{
146+
if (operation.Type.SpecialType != SpecialType.System_String)
147+
{
148+
return false;
149+
}
150+
151+
if (operation.Kind == OperationKind.Literal)
152+
{
153+
return allowLiteral;
154+
}
155+
156+
if (operation.Kind == OperationKind.FieldReference)
157+
{
158+
return ((IFieldReferenceOperation)operation).Field.IsStatic;
159+
}
160+
161+
if (operation.Kind == OperationKind.PropertyReference)
162+
{
163+
return ((IPropertyReferenceOperation)operation).Property.IsStatic;
164+
}
165+
166+
if (operation.Kind == OperationKind.Invocation)
167+
{
168+
IInvocationOperation invocation = (IInvocationOperation)operation;
169+
if (invocation.TargetMethod.IsStatic)
170+
{
171+
foreach (IArgumentOperation? arg in invocation.Arguments)
172+
{
173+
if (!arg.ConstantValue.HasValue && !IsStringLiteralOrStaticReference(arg.Value, allowLiteral: true))
174+
{
175+
return false;
176+
}
177+
}
178+
179+
return true;
180+
}
181+
}
182+
183+
return false;
184+
}
185+
186+
/// <summary>Validates that all of the required CompositeFormat-based methods exist in the specified set.</summary>
187+
private static bool HasAllCompositeFormatMethods(IMethodSymbol[] methods)
188+
{
189+
// (IFormatProvider, CompositeFormat, T1)
190+
if (!methods.Any(m => m.IsGenericMethod &&
191+
m.Parameters.Length == 3 &&
192+
m.TypeParameters.Length == 1 &&
193+
SymbolEqualityComparer.Default.Equals(m.TypeParameters[0], m.Parameters[2].Type)))
194+
{
195+
return false;
196+
}
197+
198+
// (IFormatProvider, CompositeFormat, T1, T2)
199+
if (!methods.Any(m => m.IsGenericMethod &&
200+
m.Parameters.Length == 4 &&
201+
m.TypeParameters.Length == 2 &&
202+
SymbolEqualityComparer.Default.Equals(m.TypeParameters[0], m.Parameters[2].Type) &&
203+
SymbolEqualityComparer.Default.Equals(m.TypeParameters[1], m.Parameters[3].Type)))
204+
{
205+
return false;
206+
}
207+
208+
// (IFormatProvider, CompositeFormat, T1, T2, T3)
209+
if (!methods.Any(m => m.IsGenericMethod &&
210+
m.Parameters.Length == 5 &&
211+
m.TypeParameters.Length == 3 &&
212+
SymbolEqualityComparer.Default.Equals(m.TypeParameters[0], m.Parameters[2].Type) &&
213+
SymbolEqualityComparer.Default.Equals(m.TypeParameters[1], m.Parameters[3].Type) &&
214+
SymbolEqualityComparer.Default.Equals(m.TypeParameters[2], m.Parameters[4].Type)))
215+
{
216+
return false;
217+
}
218+
219+
// (IFormatProvider, CompositeFormat, object[])
220+
if (!methods.Any(m => m.Parameters.Length == 3 &&
221+
m.Parameters[2].Type.Kind == SymbolKind.ArrayType))
222+
{
223+
return false;
224+
}
225+
226+
// All relevant methods exist.
227+
return true;
228+
}
229+
}
230+
}

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2748,6 +2748,21 @@ Obecné přetypování (IL unbox.any) používané sekvencí vrácenou metodou E
27482748
<target state="translated">Použijte ThrowIfCancellationRequested</target>
27492749
<note />
27502750
</trans-unit>
2751+
<trans-unit id="UseCompositeFormatDescription">
2752+
<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>
2753+
<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>
2754+
<note />
2755+
</trans-unit>
2756+
<trans-unit id="UseCompositeFormatMessage">
2757+
<source>Cache a 'CompositeFormat' for repeated use in this formatting operation</source>
2758+
<target state="new">Cache a 'CompositeFormat' for repeated use in this formatting operation</target>
2759+
<note />
2760+
</trans-unit>
2761+
<trans-unit id="UseCompositeFormatTitle">
2762+
<source>Use 'CompositeFormat'</source>
2763+
<target state="new">Use 'CompositeFormat'</target>
2764+
<note />
2765+
</trans-unit>
27512766
<trans-unit id="UseConcreteTypeDescription">
27522767
<source>Using concrete types avoids virtual or interface call overhead and enables inlining.</source>
27532768
<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>

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2748,6 +2748,21 @@ Erweiterungen und benutzerdefinierte Konvertierungen werden bei generischen Type
27482748
<target state="translated">"ThrowIfCancellationRequested()" verwenden</target>
27492749
<note />
27502750
</trans-unit>
2751+
<trans-unit id="UseCompositeFormatDescription">
2752+
<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>
2753+
<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>
2754+
<note />
2755+
</trans-unit>
2756+
<trans-unit id="UseCompositeFormatMessage">
2757+
<source>Cache a 'CompositeFormat' for repeated use in this formatting operation</source>
2758+
<target state="new">Cache a 'CompositeFormat' for repeated use in this formatting operation</target>
2759+
<note />
2760+
</trans-unit>
2761+
<trans-unit id="UseCompositeFormatTitle">
2762+
<source>Use 'CompositeFormat'</source>
2763+
<target state="new">Use 'CompositeFormat'</target>
2764+
<note />
2765+
</trans-unit>
27512766
<trans-unit id="UseConcreteTypeDescription">
27522767
<source>Using concrete types avoids virtual or interface call overhead and enables inlining.</source>
27532768
<target state="translated">Die Verwendung von konkreten Typen vermeidet den Mehraufwand für virtuelle Aufrufe oder Schnittstellenaufrufe und ermöglicht das Inlining.</target>

src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2748,6 +2748,21 @@ La ampliación y las conversiones definidas por el usuario no se admiten con tip
27482748
<target state="translated">Usar "ThrowIfCancellationRequested"</target>
27492749
<note />
27502750
</trans-unit>
2751+
<trans-unit id="UseCompositeFormatDescription">
2752+
<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>
2753+
<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>
2754+
<note />
2755+
</trans-unit>
2756+
<trans-unit id="UseCompositeFormatMessage">
2757+
<source>Cache a 'CompositeFormat' for repeated use in this formatting operation</source>
2758+
<target state="new">Cache a 'CompositeFormat' for repeated use in this formatting operation</target>
2759+
<note />
2760+
</trans-unit>
2761+
<trans-unit id="UseCompositeFormatTitle">
2762+
<source>Use 'CompositeFormat'</source>
2763+
<target state="new">Use 'CompositeFormat'</target>
2764+
<note />
2765+
</trans-unit>
27512766
<trans-unit id="UseConcreteTypeDescription">
27522767
<source>Using concrete types avoids virtual or interface call overhead and enables inlining.</source>
27532768
<target state="translated">El uso de tipos concretos evita la sobrecarga de llamadas virtuales o de interfaz y habilita la inserción.</target>

src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2748,6 +2748,21 @@ Les conversions étendues et définies par l’utilisateur ne sont pas prises en
27482748
<target state="translated">Appeler ThrowIfCancellationRequested()</target>
27492749
<note />
27502750
</trans-unit>
2751+
<trans-unit id="UseCompositeFormatDescription">
2752+
<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>
2753+
<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>
2754+
<note />
2755+
</trans-unit>
2756+
<trans-unit id="UseCompositeFormatMessage">
2757+
<source>Cache a 'CompositeFormat' for repeated use in this formatting operation</source>
2758+
<target state="new">Cache a 'CompositeFormat' for repeated use in this formatting operation</target>
2759+
<note />
2760+
</trans-unit>
2761+
<trans-unit id="UseCompositeFormatTitle">
2762+
<source>Use 'CompositeFormat'</source>
2763+
<target state="new">Use 'CompositeFormat'</target>
2764+
<note />
2765+
</trans-unit>
27512766
<trans-unit id="UseConcreteTypeDescription">
27522767
<source>Using concrete types avoids virtual or interface call overhead and enables inlining.</source>
27532768
<target state="translated">L’utilisation de types concrets évite la surcharge des appels virtuels ou d’interface et active l’inlining.</target>

0 commit comments

Comments
 (0)