Skip to content

Commit 6d82f1b

Browse files
committed
Add CompositeFormat analyzer
1 parent 2b6ab8d commit 6d82f1b

23 files changed

+1022
-5
lines changed

src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ 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)
18+
CA1863 | Performance | Hidden | UseCompositeFormatAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1863)
1719
CA2021 | Reliability | Warning | DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2021)
1820

1921
### Removed Rules

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

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

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

Lines changed: 30 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>
@@ -2883,6 +2898,21 @@ Obecné přetypování (IL unbox.any) používané sekvencí vrácenou metodou E
28832898
<target state="translated">Použít indexer</target>
28842899
<note />
28852900
</trans-unit>
2901+
<trans-unit id="UseInterpolatedStringDescription">
2902+
<source>Using an interpolated string is both more concise and more efficient than performing the whole formatting operation at run-time.</source>
2903+
<target state="new">Using an interpolated string is both more concise and more efficient than performing the whole formatting operation at run-time.</target>
2904+
<note />
2905+
</trans-unit>
2906+
<trans-unit id="UseInterpolatedStringMessage">
2907+
<source>Use an interpolated string to perform the operation more efficiently</source>
2908+
<target state="new">Use an interpolated string to perform the operation more efficiently</target>
2909+
<note />
2910+
</trans-unit>
2911+
<trans-unit id="UseInterpolatedStringTitle">
2912+
<source>Use an interpolated string</source>
2913+
<target state="new">Use an interpolated string</target>
2914+
<note />
2915+
</trans-unit>
28862916
<trans-unit id="UseInvariantVersion">
28872917
<source>Use an invariant version</source>
28882918
<target state="translated">Použít neutrální verzi</target>

0 commit comments

Comments
 (0)