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}