diff --git a/docs/rules/ECS0002.md b/docs/rules/ECS0002.md
index 2767ee5..211ef32 100644
--- a/docs/rules/ECS0002.md
+++ b/docs/rules/ECS0002.md
@@ -1,4 +1,4 @@
-# PRC001: Prefer readonly over const
+# ECS0002: Prefer readonly over const
## Cause
Using `const` fields for constants that do not need to be available at compile time.
diff --git a/docs/rules/ECS0004.md b/docs/rules/ECS0004.md
new file mode 100644
index 0000000..ab7509b
--- /dev/null
+++ b/docs/rules/ECS0004.md
@@ -0,0 +1,59 @@
+# ECS0004: Replace string.Format with interpolated string
+
+This rule is described in detail in [Effective C#: 50 Specific Ways to Improve your C#](https://www.oreilly.com/library/view/effective-c-50/9780134579290/).
+
+## Cause
+
+Using `string.Format` for formatting strings instead of using interpolated strings.
+
+## Rule description
+
+This rule identifies the usage of `string.Format` method and suggests replacing it with C# interpolated strings. Interpolated strings enhance code readability and reduce the likelihood of runtime errors associated with mismatched format arguments.
+
+## How to fix violations
+
+Replace the usage of `string.Format` with an interpolated string. Interpolated strings are more readable and less error-prone compared to `string.Format`.
+
+## When to suppress warnings
+
+Suppress this warning if you have a specific reason to use `string.Format`, such as when dynamically constructing the format string at runtime or when maintaining compatibility with older code bases that rely on `string.Format`.
+
+## Example of a violation
+
+### Description
+
+Using `string.Format` to format a string.
+
+### Code
+
+```csharp
+class Program
+{
+ void Main()
+ {
+ var str = string.Format("Hello, {0}!", "world");
+ }
+}
+
+## Example of how to fix
+
+### Description
+
+Replacing `string.Format` with an interpolated string.
+
+### Code
+
+```csharp
+class Program
+{
+ void Main()
+ {
+ var world = "world";
+ var str = $"Hello, {world}!";
+ }
+}
+```
+
+## Related rules
+
+[ECS0009: Minimize boxing and unboxing](./ECS0009.md)
\ No newline at end of file
diff --git a/src/EffectiveCSharp.Analyzers/AnalyzerReleases.Unshipped.md b/src/EffectiveCSharp.Analyzers/AnalyzerReleases.Unshipped.md
index 835f816..d7dbce4 100644
--- a/src/EffectiveCSharp.Analyzers/AnalyzerReleases.Unshipped.md
+++ b/src/EffectiveCSharp.Analyzers/AnalyzerReleases.Unshipped.md
@@ -6,6 +6,7 @@ Rule ID | Category | Severity | Notes
--------|----------|----------|-------
ECS0001 | Style | Info | PreferImplicitlyTypedLocalVariablesAnalyzer, [Documentation](https://github.com/rjmurillo/EffectiveCSharp.Analyzers/blob/e7c151c721c3039011356d6012838f46e4b60a21/docs/ECS0001.md)
ECS0002 | Maintainability | Info | PreferReadonlyOverConstAnalyzer, [Documentation](https://github.com/rjmurillo/EffectiveCSharp.Analyzers/blob/10c2d53afd688efe5a59097f76cb4edf33f6a474/docs/ECS0002.md)
+ECS0004 | Style | Info | ReplaceStringFormatAnalyzer, [Documentation](https://github.com/rjmurillo/EffectiveCSharp.Analyzers5da647e447fad4eb0a9e3db287e1d16cce316114/docs/ECS0004.md)
ECS0006 | Refactoring | Info | AvoidStringlyTypedApisAnalyzer, [Documentation](https://github.com/rjmurillo/EffectiveCSharp.Analyzers6213cba8473dac61d6132e205550884eae1c94bf/docs/ECS0006.md)
ECS0009 | Performance | Info | MinimizeBoxingUnboxingAnalyzer, [Documentation](https://github.com/rjmurillo/EffectiveCSharp.Analyzers/blob/6213cba8473dac61d6132e205550884eae1c94bf/docs/ECS0009.md)
ECS1000 | Performance | Info | SpanAnalyzer, [Documentation](https://github.com/rjmurillo/EffectiveCSharp.Analyzers/blob/d00a4cc9f61e7d5b392894aad859e46c43a5611c/docs/ECS1000.md)
\ No newline at end of file
diff --git a/src/EffectiveCSharp.Analyzers/Common/ITypeSymbolExtensions.cs b/src/EffectiveCSharp.Analyzers/Common/ITypeSymbolExtensions.cs
index b7e4098..29cc78a 100644
--- a/src/EffectiveCSharp.Analyzers/Common/ITypeSymbolExtensions.cs
+++ b/src/EffectiveCSharp.Analyzers/Common/ITypeSymbolExtensions.cs
@@ -10,4 +10,14 @@ or SpecialType.System_Single
or SpecialType.System_Double
or SpecialType.System_Decimal;
}
+
+ ///
+ /// Determines if the is .
+ ///
+ /// The type.
+ /// Return true if the type is ; otherwise, false.
+ internal static bool IsString(this ITypeSymbol typeSymbol)
+ {
+ return typeSymbol?.SpecialType == SpecialType.System_String;
+ }
}
diff --git a/src/EffectiveCSharp.Analyzers/DiagnosticIds.cs b/src/EffectiveCSharp.Analyzers/DiagnosticIds.cs
index 0ac6baf..4474242 100644
--- a/src/EffectiveCSharp.Analyzers/DiagnosticIds.cs
+++ b/src/EffectiveCSharp.Analyzers/DiagnosticIds.cs
@@ -4,6 +4,7 @@ internal static class DiagnosticIds
{
internal const string PreferImplicitlyTypedLocalVariables = "ECS0001";
internal const string PreferReadonlyOverConst = "ECS0002";
+ internal const string ReplaceStringFormatWithInterpolatedString = "ECS0004";
internal const string AvoidStringlyTypedApis = "ECS0006";
internal const string MinimizeBoxingUnboxing = "ECS0009";
internal const string BeAwareOfValueTypeCopyInReferenceTypes = "ECS0009";
diff --git a/src/EffectiveCSharp.Analyzers/PreferExplicitTypesOnNumbersAnalyzer.cs b/src/EffectiveCSharp.Analyzers/PreferExplicitTypesOnNumbersAnalyzer.cs
index f6e4ac0..9e446e7 100644
--- a/src/EffectiveCSharp.Analyzers/PreferExplicitTypesOnNumbersAnalyzer.cs
+++ b/src/EffectiveCSharp.Analyzers/PreferExplicitTypesOnNumbersAnalyzer.cs
@@ -12,11 +12,11 @@ public class PreferExplicitTypesOnNumbersAnalyzer : DiagnosticAnalyzer
private static readonly DiagnosticDescriptor Rule = new(
id: Id,
title: "Prefer implicitly typed local variables",
- description: "Use var to declare local variables for better readability and efficiency, except for built-in numeric types where explicit typing prevents potential conversion issues.",
messageFormat: "Use explicit type instead of 'var' for numeric variables",
category: "Style",
defaultSeverity: DiagnosticSeverity.Info,
isEnabledByDefault: true,
+ description: "Use var to declare local variables for better readability and efficiency, except for built-in numeric types where explicit typing prevents potential conversion issues.",
helpLinkUri: $"https://github.com/rjmurillo/EffectiveCSharp.Analyzers/blob/{ThisAssembly.GitCommitId}/docs/{Id}.md");
///
diff --git a/src/EffectiveCSharp.Analyzers/ReplaceStringFormatAnalyzer.cs b/src/EffectiveCSharp.Analyzers/ReplaceStringFormatAnalyzer.cs
new file mode 100644
index 0000000..765e672
--- /dev/null
+++ b/src/EffectiveCSharp.Analyzers/ReplaceStringFormatAnalyzer.cs
@@ -0,0 +1,95 @@
+using System.Text.RegularExpressions;
+
+namespace EffectiveCSharp.Analyzers;
+
+///
+/// A for Effective C# Item #4 - Replace string.Format with interpolated string.
+///
+///
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public class ReplaceStringFormatAnalyzer : DiagnosticAnalyzer
+{
+ private const string Category = "Style";
+ private const string Description = "Replace string.Format with interpolated string.";
+ private const string DiagnosticId = DiagnosticIds.ReplaceStringFormatWithInterpolatedString;
+ private const string MessageFormat = "Replace '{0}' with interpolated string";
+ private const string Title = "Replace string.Format with interpolated string";
+
+ // We can't use source generators
+ private static readonly Regex PlaceholderRegex = new(
+ @"\{.*?\}",
+ RegexOptions.Compiled,
+ TimeSpan.FromSeconds(1));
+
+ private static readonly DiagnosticDescriptor Rule = new(
+ DiagnosticId,
+ Title,
+ MessageFormat,
+ Category,
+ DiagnosticSeverity.Info,
+ isEnabledByDefault: true,
+ description: Description,
+ helpLinkUri: $"https://github.com/rjmurillo/EffectiveCSharp.Analyzers{ThisAssembly.GitCommitId}/docs/{DiagnosticId}.md");
+
+ ///
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule);
+
+ ///
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+ context.RegisterCompilationStartAction(compilationContext =>
+ {
+ CSharpParseOptions? parseOptions = compilationContext.Compilation.SyntaxTrees.FirstOrDefault()?.Options as CSharpParseOptions;
+ if (parseOptions != null && parseOptions.LanguageVersion < LanguageVersion.CSharp10)
+ {
+ return;
+ }
+
+ compilationContext.RegisterSyntaxNodeAction(AnalyzeSyntaxNode, SyntaxKind.InvocationExpression);
+ });
+ }
+
+ private static void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context)
+ {
+ InvocationExpressionSyntax invocationExpr = (InvocationExpressionSyntax)context.Node;
+
+ SymbolInfo info = context.SemanticModel.GetSymbolInfo(invocationExpr, context.CancellationToken);
+ ISymbol? symbol = info.Symbol;
+
+ if (symbol is not { Name: "Format" }
+ || !symbol.IsStatic
+ || !symbol.ContainingType.IsString())
+ {
+ return;
+ }
+
+ SeparatedSyntaxList argumentList = invocationExpr.ArgumentList.Arguments;
+
+ if (argumentList.Count < 2)
+ {
+ return;
+ }
+
+ if (argumentList[0].Expression is not LiteralExpressionSyntax formatArgument
+ || !formatArgument.IsKind(SyntaxKind.StringLiteralExpression))
+ {
+ return;
+ }
+
+ string formatString = formatArgument.Token.ValueText;
+ if (!ContainsPlaceholders(formatString))
+ {
+ return;
+ }
+
+ Diagnostic diagnostic = invocationExpr.GetLocation().CreateDiagnostic(Rule, invocationExpr.ToString());
+ context.ReportDiagnostic(diagnostic);
+ }
+
+ private static bool ContainsPlaceholders(string formatString)
+ {
+ return PlaceholderRegex.IsMatch(formatString);
+ }
+}
diff --git a/src/EffectiveCSharp.Analyzers/ReplaceStringFormatCodeFixProvider.cs b/src/EffectiveCSharp.Analyzers/ReplaceStringFormatCodeFixProvider.cs
new file mode 100644
index 0000000..79c25e2
--- /dev/null
+++ b/src/EffectiveCSharp.Analyzers/ReplaceStringFormatCodeFixProvider.cs
@@ -0,0 +1,90 @@
+using System.Text.RegularExpressions;
+
+namespace EffectiveCSharp.Analyzers;
+
+///
+/// A that provides a code fix for the .
+///
+///
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ReplaceStringFormatCodeFixProvider))]
+[Shared]
+public class ReplaceStringFormatCodeFixProvider : CodeFixProvider
+{
+ private const string Title = "Replace with interpolated string";
+
+ ///
+ public override sealed ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(DiagnosticIds.ReplaceStringFormatWithInterpolatedString);
+
+ ///
+ public override sealed FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
+
+ ///
+ public override sealed async Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ Diagnostic diagnostic = context.Diagnostics[0];
+ TextSpan diagnosticSpan = diagnostic.Location.SourceSpan;
+
+ SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+ InvocationExpressionSyntax? invocationExpr = root?.FindNode(diagnosticSpan).FirstAncestorOrSelf();
+
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ title: Title,
+ createChangedSolution: c => ReplaceWithInterpolatedStringAsync(context.Document, invocationExpr, c),
+ equivalenceKey: Title),
+ diagnostic);
+ }
+
+ private static bool NeedsParentheses(ExpressionSyntax expression)
+ {
+ // Check if the expression is complex and needs to be wrapped in parentheses
+ return expression is BinaryExpressionSyntax or ConditionalExpressionSyntax or AssignmentExpressionSyntax or CastExpressionSyntax;
+ }
+
+ private static string CreateInterpolatedString(string formatString, ArgumentSyntax[] arguments)
+ {
+ string result = formatString;
+
+ for (int i = 0; i < arguments.Length; i++)
+ {
+ string argumentText = arguments[i].ToString();
+
+ // Wrap in parentheses if the argument is a complex expression
+ if (NeedsParentheses(arguments[i].Expression))
+ {
+ argumentText = $"({argumentText})";
+ }
+
+ result = Regex.Replace(result, $@"\{{{i}(.*?)\}}", $"{{{argumentText}$1}}", RegexOptions.Compiled, TimeSpan.FromSeconds(1));
+ }
+
+ return $"$\"{result}\"";
+ }
+
+ private static async Task ReplaceWithInterpolatedStringAsync(Document document, InvocationExpressionSyntax? invocationExpr, CancellationToken cancellationToken)
+ {
+ if (invocationExpr == null)
+ {
+ return document.Project.Solution;
+ }
+
+ LiteralExpressionSyntax? formatStringLiteral = invocationExpr.ArgumentList.Arguments.First().Expression as LiteralExpressionSyntax;
+ string? formatString = formatStringLiteral?.Token.ValueText;
+
+ if (string.IsNullOrEmpty(formatString))
+ {
+ return document.Project.Solution;
+ }
+
+ ArgumentSyntax[] arguments = invocationExpr.ArgumentList.Arguments.Skip(1).ToArray();
+
+ // Replace format placeholders with corresponding arguments in an interpolated string format
+ string interpolatedString = CreateInterpolatedString(formatString!, arguments);
+ SyntaxNode? root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
+ SyntaxNode? newRoot = root?.ReplaceNode(invocationExpr, SyntaxFactory.ParseExpression(interpolatedString));
+
+ return newRoot != null
+ ? document.WithSyntaxRoot(newRoot).Project.Solution
+ : document.Project.Solution;
+ }
+}
diff --git a/tests/EffectiveCSharp.Analyzers.Benchmarks/Ecs0004Benchmarks.cs b/tests/EffectiveCSharp.Analyzers.Benchmarks/Ecs0004Benchmarks.cs
new file mode 100644
index 0000000..3941a77
--- /dev/null
+++ b/tests/EffectiveCSharp.Analyzers.Benchmarks/Ecs0004Benchmarks.cs
@@ -0,0 +1,70 @@
+namespace EffectiveCSharp.Analyzers.Benchmarks;
+
+[InProcess]
+[MemoryDiagnoser]
+public class Ecs0004Benchmarks
+{
+ private static CompilationWithAnalyzers? BaselineCompilation { get; set; }
+
+ private static CompilationWithAnalyzers? TestCompilation { get; set; }
+
+ [IterationSetup]
+ [SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits", Justification = "Async setup not supported in BenchmarkDotNet.See https://github.com/dotnet/BenchmarkDotNet/issues/2442.")]
+ public static void SetupCompilation()
+ {
+ List<(string Name, string Content)> sources = [];
+ for (int index = 0; index < Constants.NumberOfCodeFiles; index++)
+ {
+ string name = $"TypeName{index}";
+ sources.Add((name, @$"
+using System;
+
+public class {name}
+{{
+ public void Main()
+ {{
+ var str = string.Format(""The value of pi is {{0}}"", Math.PI.ToString(""F2""));
+ }}
+}}
+"));
+ }
+
+ (BaselineCompilation, TestCompilation) =
+ BenchmarkCSharpCompilationFactory
+ .CreateAsync(sources.ToArray())
+ .GetAwaiter()
+ .GetResult();
+ }
+
+ [Benchmark]
+ public async Task Ecs0004WithDiagnostics()
+ {
+ ImmutableArray diagnostics =
+ (await TestCompilation!
+ .GetAnalysisResultAsync(CancellationToken.None)
+ .ConfigureAwait(false))
+ .AssertValidAnalysisResult()
+ .GetAllDiagnostics();
+
+ if (diagnostics.Length != Constants.NumberOfCodeFiles)
+ {
+ throw new InvalidOperationException($"Expected '{Constants.NumberOfCodeFiles:N0}' analyzer diagnostics but found '{diagnostics.Length}'");
+ }
+ }
+
+ [Benchmark(Baseline = true)]
+ public async Task Ecs0004Baseline()
+ {
+ ImmutableArray diagnostics =
+ (await BaselineCompilation!
+ .GetAnalysisResultAsync(CancellationToken.None)
+ .ConfigureAwait(false))
+ .AssertValidAnalysisResult()
+ .GetAllDiagnostics();
+
+ if (diagnostics.Length != 0)
+ {
+ throw new InvalidOperationException($"Expected no analyzer diagnostics but found '{diagnostics.Length}'");
+ }
+ }
+}
diff --git a/tests/EffectiveCSharp.Analyzers.Tests/Helpers/ReferenceAssemblyCatalog.cs b/tests/EffectiveCSharp.Analyzers.Tests/Helpers/ReferenceAssemblyCatalog.cs
index e7eb419..7377223 100644
--- a/tests/EffectiveCSharp.Analyzers.Tests/Helpers/ReferenceAssemblyCatalog.cs
+++ b/tests/EffectiveCSharp.Analyzers.Tests/Helpers/ReferenceAssemblyCatalog.cs
@@ -61,4 +61,6 @@ internal static class ReferenceAssemblyCatalog
{ Net80, ReferenceAssemblies.Net.Net80 },
{ Net90, ReferenceAssemblies.Net.Net90 },
};
+
+ public static IReadOnlySet DotNetCore { get; } = new HashSet([Net60, Net80, Net90], StringComparer.Ordinal);
}
diff --git a/tests/EffectiveCSharp.Analyzers.Tests/Helpers/TestDataExtensions.cs b/tests/EffectiveCSharp.Analyzers.Tests/Helpers/TestDataExtensions.cs
index a7c79f0..87935dd 100644
--- a/tests/EffectiveCSharp.Analyzers.Tests/Helpers/TestDataExtensions.cs
+++ b/tests/EffectiveCSharp.Analyzers.Tests/Helpers/TestDataExtensions.cs
@@ -2,9 +2,10 @@
internal static class TestDataExtensions
{
- public static TheoryData WithReferenceAssemblyGroups(this TheoryData data)
+ internal static TheoryData WithReferenceAssemblyGroups(this TheoryData data, Predicate? predicate = null)
{
TheoryData retVal = [];
+ predicate ??= _ => true;
foreach (object[]? theoryDataItem in data)
{
@@ -12,7 +13,10 @@ public static TheoryData WithReferenceAssemblyGroups(this Theory
{
foreach (string referenceAssembly in ReferenceAssemblyCatalog.Catalog.Keys)
{
- retVal.Add(referenceAssembly, (string)entry);
+ if (predicate(referenceAssembly))
+ {
+ retVal.Add(referenceAssembly, (string)entry);
+ }
}
}
}
diff --git a/tests/EffectiveCSharp.Analyzers.Tests/ReplaceStringFormatTests.cs b/tests/EffectiveCSharp.Analyzers.Tests/ReplaceStringFormatTests.cs
new file mode 100644
index 0000000..1a0227d
--- /dev/null
+++ b/tests/EffectiveCSharp.Analyzers.Tests/ReplaceStringFormatTests.cs
@@ -0,0 +1,151 @@
+using CodeFixVerifier = EffectiveCSharp.Analyzers.Tests.Helpers.AnalyzerAndCodeFixVerifier;
+using Verifier = EffectiveCSharp.Analyzers.Tests.Helpers.AnalyzerVerifier;
+
+namespace EffectiveCSharp.Analyzers.Tests;
+
+#pragma warning disable IDE0028 // We cannot simply object creation on TheoryData because we need to convert from object[] to string, the way it is now is cleaner
+#pragma warning disable SA1204 // Static elements should appear before instance elements - we suppress to keep theorydata next to theory
+
+public class ReplaceStringFormatTests
+{
+ public static TheoryData TestData()
+ {
+ TheoryData data = new()
+ {
+ """
+ var str = {|ECS0004:string.Format("Hello, {0}!", "world")|};
+ """,
+ """
+ var str = string.Format("Hello, world!", "world");
+ """,
+ """
+ var str = {|ECS0004:string.Format("The value of pi is {0}", Math.PI)|};
+ """,
+ """
+ var str = {|ECS0004:string.Format("The value of pi is {0:F2}", Math.PI)|};
+ """,
+ """
+ var str = {|ECS0004:string.Format("The value of pi is {0}", Math.PI.ToString())|};
+ """,
+ """
+ var str = {|ECS0004:string.Format("The value of pi is {0}", Math.PI.ToString("F2"))|};
+ """,
+
+ // The code generated by the string interpolation will call a formatting
+ // method whose argument is a params array of objects (until .NET 9)
+ // The Math.PI property is a double, which is a value type. In order to
+ // coerce that double to be an Object for the params, it will be boxed.
+ """
+ Console.WriteLine($"The value of pi is {Math.PI}");
+ """,
+
+ // You should use expressions to convert arguments to strings to avoid
+ // the box
+ """
+ Console.WriteLine($"The value of pi is {Math.PI.ToString()}");
+ """,
+ """
+ Console.WriteLine($"The value of pi is {Math.PI.ToString("F2")}");
+ """,
+ """
+ Console.WriteLine($"The value of pi is {Math.PI:F2}");
+ """,
+ """
+ bool round = false;
+ Console.WriteLine($@"The value of pi is {(round ? Math.PI.ToString() : Math.PI.ToString("F2"))}");
+ """,
+ };
+
+#pragma warning disable MA0002 // IEqualityComparer or IComparer is missing
+ return data.WithReferenceAssemblyGroups(p => ReferenceAssemblyCatalog.DotNetCore.Contains(p));
+#pragma warning restore MA0002 // IEqualityComparer or IComparer is missing
+ }
+
+ [Theory]
+ [MemberData(nameof(TestData))]
+ public async Task Analyzer(string referenceAssemblyGroup, string source)
+ {
+ await Verifier.VerifyAnalyzerAsync(
+ $$"""
+ public class Program
+ {
+ void Main()
+ {
+ {{source}}
+ }
+ }
+ """,
+ referenceAssemblyGroup);
+ }
+
+ public static TheoryData CodeFixTestData()
+#pragma warning restore SA1204 // Static elements should appear before instance elements
+ {
+ TheoryData data = new()
+ {
+ {
+ """
+ var str = {|ECS0004:string.Format("Hello, {0}!", "world")|};
+ """,
+ """
+ var str = $"Hello, {"world"}!";
+ """
+ },
+ {
+ """
+ var str = {|ECS0004:string.Format("The value of pi is {0:F2}", Math.PI)|};
+ """,
+ """
+ var str = $"The value of pi is {Math.PI:F2}";
+ """
+ },
+ {
+ """
+ var str = {|ECS0004:string.Format("The value of pi is {0}", Math.PI.ToString("F2"))|};
+ """,
+ """
+ var str = $"The value of pi is {Math.PI.ToString("F2")}";
+ """
+ },
+ {
+ """
+ bool round = false;
+ string str = {|ECS0004:string.Format("The value of pi is {0}", round ? Math.PI.ToString() : Math.PI.ToString("F2"))|};
+ """,
+ """
+ bool round = false;
+ string str = $"The value of pi is {(round ? Math.PI.ToString() : Math.PI.ToString("F2"))}";
+ """
+ },
+ };
+
+ return data;
+ }
+
+ [Theory]
+ [MemberData(nameof(CodeFixTestData))]
+ public async Task CodeFix(string source, string @fixed)
+ {
+ string testCode = $$"""
+ public class Program
+ {
+ void Main()
+ {
+ {{source}}
+ }
+ }
+ """;
+
+ string fixedCode = $$"""
+ public class Program
+ {
+ void Main()
+ {
+ {{@fixed}}
+ }
+ }
+ """;
+
+ await CodeFixVerifier.VerifyCodeFixAsync(testCode, fixedCode, ReferenceAssemblyCatalog.Latest);
+ }
+}
diff --git a/tests/EffectiveCSharp.Analyzers.Tests/SquiggleCop.Baseline.yaml b/tests/EffectiveCSharp.Analyzers.Tests/SquiggleCop.Baseline.yaml
index 44cdc77..7f5640c 100644
--- a/tests/EffectiveCSharp.Analyzers.Tests/SquiggleCop.Baseline.yaml
+++ b/tests/EffectiveCSharp.Analyzers.Tests/SquiggleCop.Baseline.yaml
@@ -421,7 +421,7 @@
- {Id: IDE2005, Title: Blank line not allowed after conditional expression token, Category: Style, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false}
- {Id: IDE2006, Title: Blank line not allowed after arrow expression clause token, Category: Style, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false}
- {Id: MA0001, Title: StringComparison is missing, Category: Usage, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false}
-- {Id: MA0002, Title: IEqualityComparer or IComparer is missing, Category: Usage, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false}
+- {Id: MA0002, Title: IEqualityComparer or IComparer is missing, Category: Usage, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: true}
- {Id: MA0003, Title: Add parameter name to improve readability, Category: Style, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false}
- {Id: MA0004, Title: Use Task.ConfigureAwait, Category: Usage, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false}
- {Id: MA0005, Title: Use Array.Empty(), Category: Performance, DefaultSeverity: Warning, IsEnabledByDefault: true, EffectiveSeverities: [Error], IsEverSuppressed: false}
@@ -690,8 +690,8 @@
- {Id: RCS1114FadeOut, Title: Remove redundant delegate creation, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false}
- {Id: RCS1118, Title: Mark local variable as const, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false}
- {Id: RCS1123, Title: Add parentheses when necessary, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false}
-- {Id: RCS1124, Title: Inline local variable, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false}
-- {Id: RCS1124FadeOut, Title: Inline local variable, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false}
+- {Id: RCS1124, Title: Inline local variable, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: true}
+- {Id: RCS1124FadeOut, Title: Inline local variable, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: true}
- {Id: RCS1126, Title: Add braces to if-else, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: false, EffectiveSeverities: [None], IsEverSuppressed: true}
- {Id: RCS1128, Title: Use coalesce expression, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: false}
- {Id: RCS1129, Title: Remove redundant field initialization, Category: Roslynator, DefaultSeverity: Note, IsEnabledByDefault: true, EffectiveSeverities: [Note], IsEverSuppressed: true}