Skip to content

feat: Add analyzer to prefer interpolated strings over string.Format with comprehensive edge case handling (disabled by default)#840

Draft
Copilot wants to merge 26 commits into
mainfrom
copilot/fix-218
Draft

feat: Add analyzer to prefer interpolated strings over string.Format with comprehensive edge case handling (disabled by default)#840
Copilot wants to merge 26 commits into
mainfrom
copilot/fix-218

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Jul 24, 2025

This PR adds a new Roslyn analyzer PreferInterpolatedStringAnalyzer (PH2148) that detects string.Format calls that can be replaced with interpolated strings for better readability and reduced error-proneness.

What it detects

The analyzer identifies string.Format calls with simple placeholders and suggests replacing them with interpolated strings:

// Before (triggers analyzer)
int num = 42;
string str = string.Format("This is number {0}", num);

// After (suggested replacement)
int num = 42;
string str = $"This is number {num}";

Supports format specifiers:

// Before (triggers analyzer)
decimal value = 123.456m;
string str = string.Format("Value: {0:N2}", value);

// After (suggested replacement)
decimal value = 123.456m;
string str = $"Value: {value:N2}";

It also detects unnecessary string.Format calls with no placeholders:

// Before (triggers analyzer)
string str = string.Format("Simple string");

// After (suggested replacement)
string str = "Simple string";

What it safely ignores

The analyzer is conservative and gracefully handles edge cases where conversion isn't appropriate:

  • Non-literal format strings: string.Format(formatVariable, args) - can't analyze statically
  • Escaped braces: string.Format("{{text}}") - correctly handles escaped braces
  • Alignment options: string.Format("Value: {0,10}", value) - interpolated strings don't support this syntax directly
  • IFormatProvider overloads: string.Format(CultureInfo.InvariantCulture, "Value: {0}", value) - culture-specific formatting
  • Interpolated string formats: string.Format($"Hello {name}, value: {{0}}", value) - nested interpolation scenarios

Implementation details

  • Uses semantic analysis (not just syntax) to properly detect string.Format method calls
  • Inherits from DiagnosticAnalyzerBase and uses operation-based analysis for robust detection
  • Enhanced format string parser that correctly identifies placeholders with format specifiers like {0:N2}
  • Comprehensive edge case handling with graceful bailout for unsupported scenarios
  • Uses a [Flags] enum for better API design instead of multiple boolean parameters
  • Optimized performance by moving expensive operations inside conditional blocks
  • Includes comprehensive tests covering positive and negative cases including all edge cases
  • Set to Error severity for consistency with existing string.Format analyzers in the codebase
  • Disabled by default to allow regression testing on large code bases before enabling for all users
  • Compatible with both .NET 8.0 and .NET Standard 2.0

Benefits

  • Improved readability: Interpolated strings are easier to read and understand
  • Reduced errors: Eliminates mismatch between format placeholders and arguments
  • Modern C# syntax: Encourages use of language features introduced in C# 6.0
  • Full format specifier support: Handles complex formatting scenarios like currency, dates, and custom number formats
  • Robust edge case handling: Won't suggest conversions where they're inappropriate or unsupported

The analyzer is disabled by default since there's no CodeFixer yet, making it easier to adopt gradually after regression testing.

Fixes #218.


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 2 commits July 24, 2025 22:46
…ugging)

Co-authored-by: bcollamore <57269455+bcollamore@users.noreply.github.com>
Co-authored-by: bcollamore <57269455+bcollamore@users.noreply.github.com>
Copilot AI changed the title [WIP] Replace string.Format with Interpolated string Add analyzer to prefer interpolated strings over string.Format Jul 24, 2025
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • All analyzers have md file documentation. Please create, using others as a template.
  • This looks like a good candidate for a CodeFixer.
  • Build failures.
  • Let's use DiagnosticId of 2145. 2144 is being claimed by another analyzer in-progress.

Co-authored-by: bcollamore <57269455+bcollamore@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Jul 26, 2025

  • All analyzers have md file documentation. Please create, using others as a template.
  • This looks like a good candidate for a CodeFixer.
  • Build failures.
    ...

I've addressed the feedback:

Build failures: Fixed formatting issues and build now passes successfully
DiagnosticId 2145: Updated from 2144 to 2145 as requested
MD file documentation: Created PH2145.md following existing templates
🔄 CodeFixer: This would be a valuable addition but requires more complex implementation to convert string.Format("Hello {0}", name) to $"Hello {name}". I can implement this as a follow-up if desired.

All tests are passing and the analyzer is working correctly. Commit: dfd325a

Co-authored-by: bcollamore <57269455+bcollamore@users.noreply.github.com>
return false;
}

var hasPlaceholders = formatString.Contains("{") && formatString.Contains("}");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems better to use SyntaxKind.OpenBraceToken and SyntaxKind.CloseBraceToken

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit be6d1b3. I replaced the simple string.Contains("{") and string.Contains("}") logic with a proper format string parser that correctly identifies valid placeholders like {0}, {1}, etc. while ignoring escaped braces {{ and }}.

}

var hasPlaceholders = formatString.Contains("{") && formatString.Contains("}");
if (!hasPlaceholders)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this logic handle the case for when the string contains a curly brace as part of the actual string (and not part of the formattter)? Is there a unit test for this case?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the logic now properly handles escaped braces! In commit be6d1b3, I implemented a character-by-character parser that correctly identifies and ignores escaped braces {{ and }} when counting placeholders. I've also added comprehensive unit tests including NoWarningForEscapedBraces and NoWarningForOnlyEscapedBraces to verify this behavior works correctly.

Comment thread Documentation/Diagnostics/PH2145.md Outdated

The analyzer is conservative and only suggests conversion in safe cases:

- **No placeholders**: `string.Format("Simple string")` - no benefit from conversion
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding "No placeholders", this feels like an easy addition. It could be the same diagnostic with a different error message, e.g., "unnecessary call to string.Format".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great suggestion! Implemented in commit be6d1b3. The analyzer now detects unnecessary string.Format calls with no placeholders and flags them with a different diagnostic message "Remove unnecessary call to string.Format". I've also added tests for this case and updated the documentation to reflect that we now detect both interpolated string opportunities and unnecessary string.Format calls.

Copilot AI and others added 2 commits July 26, 2025 17:00
… braces

Co-authored-by: bcollamore <57269455+bcollamore@users.noreply.github.com>
…ensive tests

Co-authored-by: bcollamore <57269455+bcollamore@users.noreply.github.com>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see build failures

…ces logic

Co-authored-by: bcollamore <57269455+bcollamore@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Jul 27, 2025

see build failures

Fixed the build failures in commit 402fdf4. The issues were:

  • IDE0007 error: Changed out bool to out var to follow codebase style preferences
  • IDE0055 formatting errors: Fixed indentation and corrected mixed CRLF/LF line endings
  • Also fixed a logic bug where strings with only escaped braces (like {{text}}) were incorrectly flagged as unnecessary

All tests now pass (1900/1900) and the build succeeds without errors.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See build failures. (The build pipeline dogfoods the analyzers on ourselves. There was a finding.)

@bcollamore Brian Collamore (bcollamore) changed the title Add analyzer to prefer interpolated strings over string.Format feat: Add analyzer to prefer interpolated strings over string.Format Aug 3, 2025
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's see Enabled by Default to No (in documentation and code). That way, I can regression test it on large code bases before forcing it on everyone. We also don't have a CodeFixer yet, which will make this hard to adopt. (We'll do that later in another PR.)

Co-authored-by: bcollamore <57269455+bcollamore@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Aug 3, 2025

Let's see Enabled by Default to No (in documentation and code). That way, I can regression test it on large code bases before forcing it on everyone. We also don't have a CodeFixer yet, which will mak...

Set analyzer to disabled by default (isEnabledByDefault: false) in both code and documentation. This allows regression testing on large code bases before enabling for everyone, especially since there's no CodeFixer yet. Commit: bc159f9

Copilot AI changed the title feat: Add analyzer to prefer interpolated strings over string.Format feat: Add analyzer to prefer interpolated strings over string.Format (disabled by default) Aug 3, 2025
targetMethod.ContainingType.SpecialType == SpecialType.System_String;
}

private bool CanConvertToInterpolatedString(IInvocationOperation invocation, out bool isUnnecessary)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bool bool, maybe we're in need of a [Flags] enum here...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good suggestion! I've replaced the multiple bool parameters with a [Flags] enum ConversionResult that clearly represents the different conversion states (None, CanConvert, IsUnnecessary). This improves the API design and makes the code more readable. f3ec3ca


// Parse the format string to find actual placeholders, ignoring escaped braces
var placeholderCount = CountFormatPlaceholders(formatString);
var hasAnyBraces = formatString.Contains("{") || formatString.Contains("}");
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line can be move inside the if for performance

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent point! I've moved the hasAnyBraces calculation inside the if (placeholderCount == 0) block since it's only used there. This avoids unnecessary string operations when there are placeholders present. f3ec3ca

public void Test()
{
decimal value = 123.456m;
string str = string.Format(""Value: {0:N2}"", value);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The formatting syntax after : is also supported in InterpolatedString, so why do we consider it a reason to use string.Format() again ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're absolutely right! Interpolated strings fully support format specifiers like {value:N2}. I've updated the analyzer to detect string.Format("Value: {0:N2}", value) as convertible to $"Value: {value:N2}". The format string parser now correctly handles placeholders with format specifiers by splitting on ':' and validating the index part. f3ec3ca

@bcollamore
Copy link
Copy Markdown
Member

Copilot see comment above

…formance

Co-authored-by: bcollamore <57269455+bcollamore@users.noreply.github.com>
Copilot AI changed the title feat: Add analyzer to prefer interpolated strings over string.Format (disabled by default) feat: Add analyzer to prefer interpolated strings over string.Format with format specifier support (disabled by default) Aug 13, 2025
// Found valid placeholder, check if it's a simple numeric placeholder
var content = formatString.Substring(i + 1, j - i - 1);

// Handle format specifiers like {0:N2} by splitting on ':'
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also handle alignment options (denoted with a comma). https://learn.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2008/txafckwd(v=vs.90)#alignment-component In such cases, it's OK to just abort the operation, though currently we'll try to TryParse "0,3" for example. (Unit test)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit f2adad9. The analyzer now detects alignment options like {0,10} and {0,10:N2} and gracefully bails out since interpolated strings don't support this syntax directly. Added unit tests NoWarningForAlignmentOptions and NoWarningForAlignmentWithFormatSpecifier to verify this behavior.

{
return ConversionResult.None;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if string.Format(IFormatPrivder, ...) bail out gracefully (unit test)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit f2adad9. The analyzer now detects IFormatProvider overloads (e.g., string.Format(CultureInfo.InvariantCulture, "Value: {0}", value)) by checking if the first parameter is of type IFormatProvider and gracefully bails out. Added unit test NoWarningForIFormatProviderOverload to verify this behavior.

}

IOperation formatStringArgument = invocation.Arguments[0].Value;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handle string.Format($"blah blah blah"). (unit test)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit f2adad9. The analyzer now detects when the format string itself is an interpolated string (e.g., string.Format($"Hello {name}, value: {{0}}", value)) by checking for OperationKind.InterpolatedString and gracefully bails out. Added unit test NoWarningForInterpolatedStringAsFormat to verify this behavior.

@sonarqubecloud
Copy link
Copy Markdown

@bcollamore
Copy link
Copy Markdown
Member

Copilot see comments above

…lated string formats in PreferInterpolatedStringAnalyzer

Co-authored-by: bcollamore <57269455+bcollamore@users.noreply.github.com>
Copilot AI changed the title feat: Add analyzer to prefer interpolated strings over string.Format with format specifier support (disabled by default) feat: Add analyzer to prefer interpolated strings over string.Format with comprehensive edge case handling (disabled by default) Aug 26, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace string.Format with Interpolated string

3 participants