Skip to content

Commit 47516e5

Browse files
Copilotbcollamore
andcommitted
Update analyzer to detect string.Format instead of string.Join in interpolated strings
Co-authored-by: bcollamore <57269455+bcollamore@users.noreply.github.com>
1 parent 4541e02 commit 47516e5

5 files changed

Lines changed: 107 additions & 88 deletions

File tree

Documentation/Diagnostics/PH2145.md

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,22 @@
1-
# PH2145: Avoid string.Join in interpolated string
1+
# PH2145: Avoid string.Format in interpolated string
22

33
| Property | Value |
44
|--|--|
55
| Package | [Philips.CodeAnalysis.MaintainabilityAnalyzers](https://www.nuget.org/packages/Philips.CodeAnalysis.MaintainabilityAnalyzers) |
66
| Diagnostic ID | PH2145 |
77
| Category | [Maintainability](../Maintainability.md) |
8-
| Analyzer | [AvoidStringJoinInInterpolatedStringAnalyzer](https://github.com/philips-software/roslyn-analyzers/blob/main/Philips.CodeAnalysis.MaintainabilityAnalyzers/Maintainability/AvoidStringJoinInInterpolatedStringAnalyzer.cs)
8+
| Analyzer | [AvoidStringFormatInInterpolatedStringAnalyzer](https://github.com/philips-software/roslyn-analyzers/blob/main/Philips.CodeAnalysis.MaintainabilityAnalyzers/Maintainability/AvoidStringFormatInInterpolatedStringAnalyzer.cs)
99
| CodeFix | No |
1010
| Severity | Info |
1111
| Enabled By Default | No |
1212

1313
## Introduction
1414

15-
Using `string.Join` within interpolated strings can sometimes be simplified and made more readable by using alternative approaches.
15+
Using `string.Format` within interpolated strings is redundant and can be simplified by using direct interpolation.
1616

1717
## How to solve
1818

19-
Consider simplifying the string construction by using alternative methods such as:
20-
- Direct interpolation with collection elements
21-
- LINQ methods for string building
22-
- Traditional string concatenation if appropriate
19+
Replace `string.Format` calls inside interpolated strings with direct interpolation of the variables.
2320

2421
## Example
2522

@@ -29,26 +26,30 @@ class BadExample
2926
{
3027
public void BadMethod()
3128
{
32-
var items = new[] { "apple", "banana", "cherry" };
33-
var result = $"Items: {string.Join(", ", items)}";
29+
var firstName = "John";
30+
var lastName = "Doe";
31+
var result = $"Hello {string.Format("{0} {1}", firstName, lastName)}";
3432

35-
var reasons = new[] { "Error 1", "Error 2" };
36-
var message = $@"Command failed. Reasons:{string.Join(Environment.NewLine, reasons)}";
33+
var name = "John";
34+
var age = 30;
35+
var message = $"User info: {string.Format("Name: {0}, Age: {1}", name, age)}";
3736
}
3837
}
3938
```
4039

41-
And potential replacement approaches:
40+
And the simplified replacement:
4241
``` cs
4342
class GoodExample
4443
{
4544
public void GoodMethod()
4645
{
47-
var items = new[] { "apple", "banana", "cherry" };
48-
var result = "Items: " + string.Join(", ", items); // Move out of interpolation
46+
var firstName = "John";
47+
var lastName = "Doe";
48+
var result = $"Hello {firstName} {lastName}";
4949

50-
var reasons = new[] { "Error 1", "Error 2" };
51-
var message = "Command failed. Reasons:" + Environment.NewLine + string.Join(Environment.NewLine, reasons);
50+
var name = "John";
51+
var age = 30;
52+
var message = $"User info: Name: {name}, Age: {age}";
5253
}
5354
}
5455
```

Philips.CodeAnalysis.Common/DiagnosticId.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,6 @@ public enum DiagnosticId
137137
AvoidEmptyRegions = 2141,
138138
AvoidCastToString = 2142,
139139
AvoidAssemblyGetEntryAssembly = 2143,
140-
AvoidStringJoinInInterpolatedString = 2145,
140+
AvoidStringFormatInInterpolatedString = 2145,
141141
}
142142
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// © 2024 Koninklijke Philips N.V. See License.md in the project root for license information.
2+
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.CSharp.Syntax;
5+
using Microsoft.CodeAnalysis.Diagnostics;
6+
using Philips.CodeAnalysis.Common;
7+
8+
namespace Philips.CodeAnalysis.MaintainabilityAnalyzers.Maintainability
9+
{
10+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
11+
public class AvoidStringFormatInInterpolatedStringAnalyzer : SingleDiagnosticAnalyzer<InterpolatedStringExpressionSyntax, AvoidStringFormatInInterpolatedStringSyntaxNodeAction>
12+
{
13+
private const string Title = @"Avoid string.Format in interpolated string";
14+
private const string MessageFormat = @"Consider simplifying string.Format usage in interpolated string";
15+
private const string Description = @"Using string.Format within interpolated strings can be simplified by using direct interpolation";
16+
17+
public AvoidStringFormatInInterpolatedStringAnalyzer()
18+
: base(DiagnosticId.AvoidStringFormatInInterpolatedString, Title, MessageFormat, Description, Categories.Maintainability, isEnabled: false)
19+
{ }
20+
}
21+
22+
public class AvoidStringFormatInInterpolatedStringSyntaxNodeAction : SyntaxNodeAction<InterpolatedStringExpressionSyntax>
23+
{
24+
public override void Analyze()
25+
{
26+
// Look for interpolated string expressions
27+
foreach (InterpolatedStringContentSyntax content in Node.Contents)
28+
{
29+
if (content is InterpolationSyntax interpolation && ContainsStringFormatCall(interpolation.Expression))
30+
{
31+
Location location = interpolation.GetLocation();
32+
ReportDiagnostic(location);
33+
}
34+
}
35+
}
36+
37+
private bool ContainsStringFormatCall(ExpressionSyntax expression)
38+
{
39+
// Check if this is a direct string.Format call
40+
if (expression is InvocationExpressionSyntax invocation)
41+
{
42+
// Use semantic analysis to properly identify string.Format
43+
SymbolInfo symbolInfo = Context.SemanticModel.GetSymbolInfo(invocation);
44+
if (symbolInfo.Symbol is IMethodSymbol methodSymbol)
45+
{
46+
return methodSymbol.Name == "Format" &&
47+
methodSymbol.ContainingType?.SpecialType == SpecialType.System_String;
48+
}
49+
}
50+
51+
return false;
52+
}
53+
}
54+
}

Philips.CodeAnalysis.MaintainabilityAnalyzers/Maintainability/AvoidStringJoinInInterpolatedStringAnalyzer.cs

Lines changed: 0 additions & 54 deletions
This file was deleted.

Philips.CodeAnalysis.Test/Maintainability/Maintainability/AvoidStringJoinInInterpolatedStringAnalyzerTest.cs renamed to Philips.CodeAnalysis.Test/Maintainability/Maintainability/AvoidStringFormatInInterpolatedStringAnalyzerTest.cs

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,27 @@
1010
namespace Philips.CodeAnalysis.Test.Maintainability.Maintainability
1111
{
1212
[TestClass]
13-
public class AvoidStringJoinInInterpolatedStringAnalyzerTest : DiagnosticVerifier
13+
public class AvoidStringFormatInInterpolatedStringAnalyzerTest : DiagnosticVerifier
1414
{
1515
protected override DiagnosticAnalyzer GetDiagnosticAnalyzer()
1616
{
17-
return new AvoidStringJoinInInterpolatedStringAnalyzer();
17+
return new AvoidStringFormatInInterpolatedStringAnalyzer();
1818
}
1919

2020
[TestMethod]
2121
[TestCategory(TestDefinitions.UnitTests)]
22-
public async Task StringJoinInInterpolatedStringTriggersWarning()
22+
public async Task StringFormatInInterpolatedStringTriggersWarning()
2323
{
2424
var code = @"
2525
using System;
26-
using System.Collections.Generic;
2726
2827
class Test
2928
{
3029
public void Method()
3130
{
32-
var items = new List<string> { ""a"", ""b"", ""c"" };
33-
var result = $""Items: {string.Join("", "", items)}"";
31+
var firstName = ""John"";
32+
var lastName = ""Doe"";
33+
var result = $""Hello {string.Format(""{0} {1}"", firstName, lastName)}"";
3434
}
3535
}
3636
";
@@ -39,20 +39,18 @@ public void Method()
3939

4040
[TestMethod]
4141
[TestCategory(TestDefinitions.UnitTests)]
42-
public async Task StringJoinWithEnvironmentNewLineTriggersWarning()
42+
public async Task StringFormatWithMultipleArgumentsTriggersWarning()
4343
{
4444
var code = @"
4545
using System;
46-
using System.Collections.Generic;
4746
4847
class Test
4948
{
50-
public string CommandType { get; set; }
51-
public List<string> ReasonList { get; set; }
52-
53-
public string Method()
49+
public void Method()
5450
{
55-
return $@""{CommandType} Reasons:{string.Join(Environment.NewLine, ReasonList)}"";
51+
var name = ""John"";
52+
var age = 30;
53+
var result = $""User info: {string.Format(""Name: {0}, Age: {1}"", name, age)}"";
5654
}
5755
}
5856
";
@@ -61,18 +59,38 @@ public string Method()
6159

6260
[TestMethod]
6361
[TestCategory(TestDefinitions.UnitTests)]
64-
public async Task InterpolatedStringWithoutStringJoinIsOk()
62+
public async Task InterpolatedStringWithoutStringFormatIsOk()
63+
{
64+
var code = @"
65+
using System;
66+
67+
class Test
68+
{
69+
public void Method()
70+
{
71+
var firstName = ""John"";
72+
var lastName = ""Doe"";
73+
var result = $""Hello {firstName} {lastName}"";
74+
}
75+
}
76+
";
77+
await VerifySuccessfulCompilation(code).ConfigureAwait(false);
78+
}
79+
80+
[TestMethod]
81+
[TestCategory(TestDefinitions.UnitTests)]
82+
public async Task StringFormatOutsideInterpolatedStringIsOk()
6583
{
6684
var code = @"
6785
using System;
68-
using System.Collections.Generic;
6986
7087
class Test
7188
{
7289
public void Method()
7390
{
74-
var items = new List<string> { ""a"", ""b"", ""c"" };
75-
var result = $""Items: {items.Count}"";
91+
var firstName = ""John"";
92+
var lastName = ""Doe"";
93+
var result = string.Format(""Hello {0} {1}"", firstName, lastName);
7694
}
7795
}
7896
";

0 commit comments

Comments
 (0)