From e35c680e88e794ac0afe4c53f50f1bc84fc57bba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Mon, 30 Mar 2026 13:50:42 -0400 Subject: [PATCH 1/6] Add code fixers for MA0088-MA0131 Implement missing code fix providers and tests for MA0088, MA0090, MA0099, MA0105, MA0106, MA0112, MA0127, MA0129, and MA0131. MA0137 already had an existing fixer and is covered by targeted tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...osureWhenUsingConcurrentDictionaryFixer.cs | 390 ++++++++++++++++++ ...oNotUseZeroToInitializeAnEnumValueFixer.cs | 124 ++++++ .../Rules/OptimizeLinqUsageFixer.cs | 25 ++ .../Rules/OptionalParametersAttributeFixer.cs | 83 ++++ .../Rules/RemoveEmptyBlockFixer.cs | 68 +++ .../Rules/TaskInUsingFixer.cs | 45 ++ ...ThrowIfNullWithNonNullableInstanceFixer.cs | 47 +++ .../UseStringEqualsInsteadOfIsPatternFixer.cs | 61 +++ ...WhenAccessingTheKeyAnalyzerTests_MA0105.cs | 31 +- ...WhenAccessingTheKeyAnalyzerTests_MA0106.cs | 31 +- ...oNotUseZeroToInitializeAnEnumValueTests.cs | 32 +- ...qUsageAnalyzerUseCountInsteadOfAnyTests.cs | 34 +- ...lParametersAttributeAnalyzerMA0088Tests.cs | 36 +- .../Rules/RemoveEmptyBlockAnalyzerTests.cs | 75 +++- .../Rules/TaskInUsingAnalyzerTests.cs | 26 +- ...ullWithNonNullableInstanceAnalyzerTests.cs | 22 +- ...ngEqualsInsteadOfIsPatternAnalyzerTests.cs | 32 +- 17 files changed, 1153 insertions(+), 9 deletions(-) create mode 100644 src/Meziantou.Analyzer.CodeFixers/Rules/AvoidClosureWhenUsingConcurrentDictionaryFixer.cs create mode 100644 src/Meziantou.Analyzer.CodeFixers/Rules/DoNotUseZeroToInitializeAnEnumValueFixer.cs create mode 100644 src/Meziantou.Analyzer.CodeFixers/Rules/OptionalParametersAttributeFixer.cs create mode 100644 src/Meziantou.Analyzer.CodeFixers/Rules/RemoveEmptyBlockFixer.cs create mode 100644 src/Meziantou.Analyzer.CodeFixers/Rules/TaskInUsingFixer.cs create mode 100644 src/Meziantou.Analyzer.CodeFixers/Rules/ThrowIfNullWithNonNullableInstanceFixer.cs create mode 100644 src/Meziantou.Analyzer.CodeFixers/Rules/UseStringEqualsInsteadOfIsPatternFixer.cs diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/AvoidClosureWhenUsingConcurrentDictionaryFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/AvoidClosureWhenUsingConcurrentDictionaryFixer.cs new file mode 100644 index 000000000..6092e2c35 --- /dev/null +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/AvoidClosureWhenUsingConcurrentDictionaryFixer.cs @@ -0,0 +1,390 @@ +using System.Collections.Immutable; +using System.Composition; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using Meziantou.Analyzer.Internals; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Operations; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace Meziantou.Analyzer.Rules; + +[ExportCodeFixProvider(LanguageNames.CSharp), Shared] +public sealed class AvoidClosureWhenUsingConcurrentDictionaryFixer : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create( + RuleIdentifiers.AvoidClosureWhenUsingConcurrentDictionary, + RuleIdentifiers.AvoidClosureWhenUsingConcurrentDictionaryByUsingFactoryArg); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var nodeToFix = root?.FindNode(context.Span, getInnermostNodeForTie: true); + if (nodeToFix is null) + return; + + var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); + if (semanticModel is null) + return; + + if (!TryGetAnonymousFunctionOperation(semanticModel, nodeToFix, context.CancellationToken, out var lambdaOperation)) + return; + + if (!TryGetInvocationAndArgument(lambdaOperation, out var invocationOperation, out var lambdaArgument)) + return; + + if (context.Diagnostics.Any(d => d.Id == RuleIdentifiers.AvoidClosureWhenUsingConcurrentDictionary)) + { + context.RegisterCodeFix( + CodeAction.Create( + "Use lambda parameters", + ct => UseLambdaParameters(context.Document, semanticModel, invocationOperation, lambdaOperation, lambdaArgument, ct), + equivalenceKey: "Use lambda parameters"), + context.Diagnostics); + } + + if (context.Diagnostics.Any(d => d.Id == RuleIdentifiers.AvoidClosureWhenUsingConcurrentDictionaryByUsingFactoryArg)) + { + context.RegisterCodeFix( + CodeAction.Create( + "Use factoryArgument overload", + ct => UseFactoryArgumentOverload(context.Document, semanticModel, invocationOperation, lambdaOperation, lambdaArgument, ct), + equivalenceKey: "Use factoryArgument overload"), + context.Diagnostics); + } + } + + private static async Task UseLambdaParameters(Document document, SemanticModel semanticModel, IInvocationOperation invocationOperation, IAnonymousFunctionOperation lambdaOperation, IArgumentOperation lambdaArgument, CancellationToken cancellationToken) + { + var mappings = GetReplacementMappings(invocationOperation, lambdaOperation, lambdaArgument); + if (mappings.Count == 0) + return document; + + var updatedLambda = (AnonymousFunctionExpressionSyntax)lambdaOperation.Syntax; + foreach (var (symbol, parameterName) in mappings) + { + updatedLambda = ReplaceSymbolReferences(updatedLambda, semanticModel, symbol, parameterName); + } + + var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + editor.ReplaceNode(lambdaOperation.Syntax, updatedLambda.WithAdditionalAnnotations(Formatter.Annotation)); + return editor.GetChangedDocument(); + } + + private static async Task UseFactoryArgumentOverload(Document document, SemanticModel semanticModel, IInvocationOperation invocationOperation, IAnonymousFunctionOperation lambdaOperation, IArgumentOperation lambdaArgument, CancellationToken cancellationToken) + { + if (invocationOperation.Syntax is not InvocationExpressionSyntax invocationSyntax) + return document; + + var capturedSymbol = GetCapturedSymbols(semanticModel, lambdaOperation).FirstOrDefault(); + if (capturedSymbol is not ILocalSymbol and not IParameterSymbol) + return document; + + var newInvocation = invocationOperation.TargetMethod.Name switch + { + "GetOrAdd" => CreateGetOrAddInvocationWithFactoryArg(invocationOperation, invocationSyntax, lambdaOperation, capturedSymbol, semanticModel), + "AddOrUpdate" => CreateAddOrUpdateInvocationWithFactoryArg(invocationOperation, invocationSyntax, capturedSymbol, semanticModel), + _ => null, + }; + + if (newInvocation is null) + return document; + + var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + editor.ReplaceNode(invocationSyntax, newInvocation.WithAdditionalAnnotations(Formatter.Annotation)); + return editor.GetChangedDocument(); + } + + private static InvocationExpressionSyntax? CreateGetOrAddInvocationWithFactoryArg(IInvocationOperation invocationOperation, InvocationExpressionSyntax invocationSyntax, IAnonymousFunctionOperation lambdaOperation, ISymbol capturedSymbol, SemanticModel semanticModel) + { + if (invocationOperation.Arguments.Length != 2) + return null; + + var lambda = lambdaOperation.Syntax as AnonymousFunctionExpressionSyntax; + if (lambda is null) + return null; + + var parameterName = GetUniqueParameterName(lambdaOperation.Symbol.Parameters.Select(p => p.Name), "arg"); + var updatedLambda = ReplaceSymbolReferences(lambda, semanticModel, capturedSymbol, parameterName); + updatedLambda = AddParameterToLambda(updatedLambda, parameterName); + if (updatedLambda is null) + return null; + + var arguments = invocationSyntax.ArgumentList.Arguments; + arguments = arguments.Replace(arguments[1], arguments[1].WithExpression(updatedLambda)); + arguments = arguments.Add(Argument(IdentifierName(capturedSymbol.Name))); + return invocationSyntax.WithArgumentList(invocationSyntax.ArgumentList.WithArguments(arguments)); + } + + private static InvocationExpressionSyntax? CreateAddOrUpdateInvocationWithFactoryArg(IInvocationOperation invocationOperation, InvocationExpressionSyntax invocationSyntax, ISymbol capturedSymbol, SemanticModel semanticModel) + { + if (invocationOperation.Arguments.Length != 3) + return null; + + if (!TryGetAnonymousFunctionOperation(invocationOperation.Arguments[1].Value, out var addValueFactoryOperation)) + return null; + + if (!TryGetAnonymousFunctionOperation(invocationOperation.Arguments[2].Value, out var updateValueFactoryOperation)) + return null; + + var addValueFactory = addValueFactoryOperation.Syntax as AnonymousFunctionExpressionSyntax; + var updateValueFactory = updateValueFactoryOperation.Syntax as AnonymousFunctionExpressionSyntax; + if (addValueFactory is null || updateValueFactory is null) + return null; + + var parameterName = GetUniqueParameterName( + addValueFactoryOperation.Symbol.Parameters.Select(p => p.Name).Concat(updateValueFactoryOperation.Symbol.Parameters.Select(p => p.Name)), + "arg"); + + addValueFactory = ReplaceSymbolReferences(addValueFactory, semanticModel, capturedSymbol, parameterName); + addValueFactory = AddParameterToLambda(addValueFactory, parameterName); + if (addValueFactory is null) + return null; + + updateValueFactory = ReplaceSymbolReferences(updateValueFactory, semanticModel, capturedSymbol, parameterName); + updateValueFactory = AddParameterToLambda(updateValueFactory, parameterName); + if (updateValueFactory is null) + return null; + + var arguments = invocationSyntax.ArgumentList.Arguments; + arguments = arguments.Replace(arguments[1], arguments[1].WithExpression(addValueFactory)); + arguments = arguments.Replace(arguments[2], arguments[2].WithExpression(updateValueFactory)); + arguments = arguments.Add(Argument(IdentifierName(capturedSymbol.Name))); + return invocationSyntax.WithArgumentList(invocationSyntax.ArgumentList.WithArguments(arguments)); + } + + private static List<(ISymbol Symbol, string ParameterName)> GetReplacementMappings(IInvocationOperation invocationOperation, IAnonymousFunctionOperation lambdaOperation, IArgumentOperation lambdaArgument) + { + var result = new List<(ISymbol Symbol, string ParameterName)>(); + var lambdaIndex = invocationOperation.Arguments.IndexOf(lambdaArgument); + + if (invocationOperation.TargetMethod.Name is "GetOrAdd") + { + if (invocationOperation.Arguments.Length == 2 && lambdaIndex == 1 && lambdaOperation.Symbol.Parameters.Length >= 1) + { + AddMapping(result, invocationOperation.Arguments[0], lambdaOperation.Symbol.Parameters[0].Name); + } + else if (invocationOperation.Arguments.Length == 3 && lambdaIndex == 1 && lambdaOperation.Symbol.Parameters.Length >= 2) + { + AddMapping(result, invocationOperation.Arguments[0], lambdaOperation.Symbol.Parameters[0].Name); + AddMapping(result, invocationOperation.Arguments[2], lambdaOperation.Symbol.Parameters[1].Name); + } + } + else if (invocationOperation.TargetMethod.Name is "AddOrUpdate") + { + if (invocationOperation.Arguments.Length == 3 && lambdaIndex == 1 && lambdaOperation.Symbol.Parameters.Length >= 1) + { + AddMapping(result, invocationOperation.Arguments[0], lambdaOperation.Symbol.Parameters[0].Name); + } + else if (invocationOperation.Arguments.Length == 3 && lambdaIndex == 2 && lambdaOperation.Symbol.Parameters.Length >= 1) + { + AddMapping(result, invocationOperation.Arguments[0], lambdaOperation.Symbol.Parameters[0].Name); + } + else if (invocationOperation.Arguments.Length == 4 && lambdaIndex == 1 && lambdaOperation.Symbol.Parameters.Length >= 2) + { + AddMapping(result, invocationOperation.Arguments[0], lambdaOperation.Symbol.Parameters[0].Name); + AddMapping(result, invocationOperation.Arguments[3], lambdaOperation.Symbol.Parameters[1].Name); + } + else if (invocationOperation.Arguments.Length == 4 && lambdaIndex == 2 && lambdaOperation.Symbol.Parameters.Length >= 3) + { + AddMapping(result, invocationOperation.Arguments[0], lambdaOperation.Symbol.Parameters[0].Name); + AddMapping(result, invocationOperation.Arguments[3], lambdaOperation.Symbol.Parameters[2].Name); + } + } + + return result; + + static void AddMapping(List<(ISymbol Symbol, string ParameterName)> mappings, IArgumentOperation argument, string parameterName) + { + if (TryGetLocalOrParameterSymbol(argument, out var symbol)) + { + mappings.Add((symbol, parameterName)); + } + } + } + + private static IEnumerable GetCapturedSymbols(SemanticModel semanticModel, IAnonymousFunctionOperation lambdaOperation) + { + var dataFlowNode = GetDataFlowArgument(lambdaOperation.Syntax); + if (dataFlowNode is null) + yield break; + + var dataFlow = semanticModel.AnalyzeDataFlow(dataFlowNode); + var parameters = lambdaOperation.Symbol.Parameters; + + foreach (var symbol in dataFlow.CapturedInside) + { + if (!parameters.Contains(symbol, SymbolEqualityComparer.Default)) + { + yield return symbol; + } + } + } + + private static AnonymousFunctionExpressionSyntax ReplaceSymbolReferences(AnonymousFunctionExpressionSyntax lambda, SemanticModel semanticModel, ISymbol symbolToReplace, string replacementParameterName) + { + var rewriter = new SymbolReferenceRewriter(semanticModel, symbolToReplace, replacementParameterName); + return (AnonymousFunctionExpressionSyntax)rewriter.Visit(lambda)!; + } + + private static AnonymousFunctionExpressionSyntax? AddParameterToLambda(AnonymousFunctionExpressionSyntax lambda, string parameterName) + { + var parameter = Parameter(Identifier(parameterName)); + + if (lambda is ParenthesizedLambdaExpressionSyntax parenthesizedLambda) + { + return parenthesizedLambda.WithParameterList(parenthesizedLambda.ParameterList.WithParameters(parenthesizedLambda.ParameterList.Parameters.Add(parameter))); + } + + if (lambda is SimpleLambdaExpressionSyntax simpleLambda) + { + var parameters = SeparatedList(new[] { simpleLambda.Parameter, parameter }); + + ParenthesizedLambdaExpressionSyntax updatedLambda; + if (simpleLambda.Block is not null) + { + updatedLambda = ParenthesizedLambdaExpression(ParameterList(parameters), simpleLambda.Block); + } + else if (simpleLambda.ExpressionBody is not null) + { + updatedLambda = ParenthesizedLambdaExpression(ParameterList(parameters), simpleLambda.ExpressionBody); + } + else + { + return null; + } + + return updatedLambda.WithAsyncKeyword(simpleLambda.AsyncKeyword); + } + + return null; + } + + private static string GetUniqueParameterName(IEnumerable existingParameterNames, string baseName) + { + var usedNames = new HashSet(existingParameterNames, StringComparer.Ordinal); + if (!usedNames.Contains(baseName)) + return baseName; + + for (var i = 1; ; i++) + { + var candidate = baseName + i.ToString(CultureInfo.InvariantCulture); + if (!usedNames.Contains(candidate)) + return candidate; + } + } + + private static bool TryGetAnonymousFunctionOperation(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken, [NotNullWhen(true)] out IAnonymousFunctionOperation? lambdaOperation) + { + var operation = semanticModel.GetOperation(node, cancellationToken); + if (TryGetAnonymousFunctionOperation(operation, out lambdaOperation)) + return true; + + foreach (var ancestor in node.AncestorsAndSelf()) + { + operation = semanticModel.GetOperation(ancestor, cancellationToken); + if (TryGetAnonymousFunctionOperation(operation, out lambdaOperation)) + return true; + } + + lambdaOperation = null; + return false; + } + + private static bool TryGetAnonymousFunctionOperation(IOperation? operation, [NotNullWhen(true)] out IAnonymousFunctionOperation? lambdaOperation) + { + if (operation is null) + { + lambdaOperation = null; + return false; + } + + operation = operation.UnwrapConversionOperations(); + + if (operation is IAnonymousFunctionOperation anonymousFunctionOperation) + { + lambdaOperation = anonymousFunctionOperation; + return true; + } + + if (operation is IDelegateCreationOperation { Target: IAnonymousFunctionOperation delegateTarget }) + { + lambdaOperation = delegateTarget; + return true; + } + + lambdaOperation = null; + return false; + } + + private static bool TryGetInvocationAndArgument(IAnonymousFunctionOperation lambdaOperation, [NotNullWhen(true)] out IInvocationOperation? invocationOperation, [NotNullWhen(true)] out IArgumentOperation? lambdaArgument) + { + var parentOperation = lambdaOperation.Parent; + if (parentOperation is IDelegateCreationOperation delegateCreationOperation) + { + parentOperation = delegateCreationOperation.Parent; + } + + if (parentOperation is IArgumentOperation argumentOperation && argumentOperation.Parent is IInvocationOperation parentInvocation) + { + invocationOperation = parentInvocation; + lambdaArgument = argumentOperation; + return true; + } + + invocationOperation = null; + lambdaArgument = null; + return false; + } + + private static bool TryGetLocalOrParameterSymbol(IArgumentOperation argumentOperation, [NotNullWhen(true)] out ISymbol? symbol) + { + var operation = argumentOperation.Value.UnwrapConversionOperations(); + if (operation is ILocalReferenceOperation localReferenceOperation) + { + symbol = localReferenceOperation.Local; + return true; + } + + if (operation is IParameterReferenceOperation parameterReferenceOperation) + { + symbol = parameterReferenceOperation.Parameter; + return true; + } + + symbol = null; + return false; + } + + [return: NotNullIfNotNull(nameof(node))] + private static SyntaxNode? GetDataFlowArgument(SyntaxNode? node) + { + if (node is ArrowExpressionClauseSyntax expression) + return expression.Expression; + + return node; + } + + private sealed class SymbolReferenceRewriter(SemanticModel semanticModel, ISymbol symbolToReplace, string replacementParameterName) : CSharpSyntaxRewriter + { + public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node) + { + var symbol = semanticModel.GetSymbolInfo(node).Symbol; + if (symbol is not null && symbol.IsEqualTo(symbolToReplace)) + { + return IdentifierName(replacementParameterName).WithTriviaFrom(node); + } + + return base.VisitIdentifierName(node); + } + } +} diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/DoNotUseZeroToInitializeAnEnumValueFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/DoNotUseZeroToInitializeAnEnumValueFixer.cs new file mode 100644 index 000000000..b6ed7a099 --- /dev/null +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/DoNotUseZeroToInitializeAnEnumValueFixer.cs @@ -0,0 +1,124 @@ +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Operations; +using Microsoft.CodeAnalysis.Simplification; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace Meziantou.Analyzer.Rules; + +[ExportCodeFixProvider(LanguageNames.CSharp), Shared] +public sealed class DoNotUseZeroToInitializeAnEnumValueFixer : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(RuleIdentifiers.DoNotUseZeroToInitializeAnEnumValue); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var nodeToFix = root?.FindNode(context.Span, getInnermostNodeForTie: true); + if (nodeToFix is null) + return; + + var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); + if (semanticModel is null) + return; + + var expressionToFix = nodeToFix as ExpressionSyntax ?? nodeToFix.AncestorsAndSelf().OfType().FirstOrDefault(); + if (expressionToFix is null) + return; + + var enumType = GetTargetEnumType(semanticModel, expressionToFix, context.CancellationToken); + if (enumType is null) + return; + + var zeroEnumField = enumType + .GetMembers() + .OfType() + .Where(field => field.HasConstantValue) + .FirstOrDefault(field => IsZero(field.ConstantValue)); + if (zeroEnumField is null) + return; + + context.RegisterCodeFix( + CodeAction.Create( + $"Use {zeroEnumField.Name}", + ct => UseEnumField(context.Document, expressionToFix, zeroEnumField, ct), + equivalenceKey: "Use enum member"), + context.Diagnostics); + + static bool IsZero(object? value) + { + return value switch + { + sbyte v => v == 0, + byte v => v == 0, + short v => v == 0, + ushort v => v == 0, + int v => v == 0, + uint v => v == 0, + long v => v == 0, + ulong v => v == 0, + _ => false, + }; + } + } + + private static INamedTypeSymbol? GetTargetEnumType(SemanticModel semanticModel, ExpressionSyntax expression, CancellationToken cancellationToken) + { + if (semanticModel.GetTypeInfo(expression, cancellationToken).ConvertedType is INamedTypeSymbol { EnumUnderlyingType: not null } convertedEnumType) + return convertedEnumType; + + if (expression.Parent is EqualsValueClauseSyntax equalsValueClause) + { + if (equalsValueClause.Parent is ParameterSyntax parameterSyntax) + { + if (semanticModel.GetDeclaredSymbol(parameterSyntax, cancellationToken) is IParameterSymbol parameterSymbol && + parameterSymbol.Type is INamedTypeSymbol { EnumUnderlyingType: not null } parameterEnumType) + { + return parameterEnumType; + } + } + else if (equalsValueClause.Parent is VariableDeclaratorSyntax variableDeclaratorSyntax && + semanticModel.GetDeclaredSymbol(variableDeclaratorSyntax, cancellationToken) is ILocalSymbol localSymbol && + localSymbol.Type is INamedTypeSymbol { EnumUnderlyingType: not null } localEnumType) + { + return localEnumType; + } + } + + if (expression.Parent is AssignmentExpressionSyntax assignmentExpression && + assignmentExpression.Right == expression && + semanticModel.GetTypeInfo(assignmentExpression.Left, cancellationToken).Type is INamedTypeSymbol { EnumUnderlyingType: not null } assignmentEnumType) + { + return assignmentEnumType; + } + + if (expression.Parent is ArgumentSyntax argumentSyntax && + semanticModel.GetOperation(argumentSyntax, cancellationToken) is IArgumentOperation { Parameter.Type: INamedTypeSymbol { EnumUnderlyingType: not null } parameterEnumType2 }) + { + return parameterEnumType2; + } + + return null; + } + + private static async Task UseEnumField(Document document, ExpressionSyntax expressionToFix, IFieldSymbol fieldSymbol, CancellationToken cancellationToken) + { + var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + var memberAccess = MemberAccessExpression( + Microsoft.CodeAnalysis.CSharp.SyntaxKind.SimpleMemberAccessExpression, + (NameSyntax)ParseName(fieldSymbol.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", "", StringComparison.Ordinal)), + IdentifierName(fieldSymbol.Name)) + .WithAdditionalAnnotations(Simplifier.Annotation); + + editor.ReplaceNode(expressionToFix, memberAccess); + return editor.GetChangedDocument(); + } +} diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/OptimizeLinqUsageFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/OptimizeLinqUsageFixer.cs index 69f5ba2a8..907e0fcab 100644 --- a/src/Meziantou.Analyzer.CodeFixers/Rules/OptimizeLinqUsageFixer.cs +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/OptimizeLinqUsageFixer.cs @@ -23,6 +23,7 @@ public sealed class OptimizeLinqUsageFixer : CodeFixProvider RuleIdentifiers.DuplicateEnumerable_OrderBy, RuleIdentifiers.OptimizeEnumerable_CombineMethods, RuleIdentifiers.OptimizeEnumerable_Count, + RuleIdentifiers.OptimizeEnumerable_UseCountInsteadOfAny, RuleIdentifiers.OptimizeEnumerable_CastInsteadOfSelect, RuleIdentifiers.OptimizeEnumerable_UseOrder); @@ -42,6 +43,13 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) if (diagnostic is null) return; + if (diagnostic.Id == RuleIdentifiers.OptimizeEnumerable_UseCountInsteadOfAny) + { + const string codeFixTitle = "Optimize linq usage"; + context.RegisterCodeFix(CodeAction.Create(codeFixTitle, ct => UseCountGreaterThanZero(context.Document, nodeToFix, ct), equivalenceKey: codeFixTitle), context.Diagnostics); + return; + } + if (!Enum.TryParse(diagnostic.Properties.GetValueOrDefault("Data", ""), ignoreCase: false, out OptimizeLinqUsageData data) || data is OptimizeLinqUsageData.None) return; @@ -196,6 +204,23 @@ private static async Task UseAny(Document document, Diagnostic diagnos return editor.GetChangedDocument(); } + private static async Task UseCountGreaterThanZero(Document document, SyntaxNode nodeToFix, CancellationToken cancellationToken) + { + var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + if (editor.SemanticModel.GetOperation(nodeToFix, cancellationToken) is not IInvocationOperation invocation) + return document; + + if (invocation.Arguments.Length != 1) + return document; + + var generator = editor.Generator; + var countExpression = generator.MemberAccessExpression(invocation.Arguments[0].Syntax, "Count"); + var newExpression = generator.ValueNotEqualsExpression(countExpression, generator.LiteralExpression(0)); + + editor.ReplaceNode(nodeToFix, newExpression); + return editor.GetChangedDocument(); + } + private static async Task UseTakeAndCount(Document document, Diagnostic diagnostic, CancellationToken cancellationToken) { var countOperationStart = int.Parse(diagnostic.Properties["CountOperationStart"]!, NumberStyles.Integer, CultureInfo.InvariantCulture); diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/OptionalParametersAttributeFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/OptionalParametersAttributeFixer.cs new file mode 100644 index 000000000..476c537a4 --- /dev/null +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/OptionalParametersAttributeFixer.cs @@ -0,0 +1,83 @@ +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using Meziantou.Analyzer.Internals; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace Meziantou.Analyzer.Rules; + +[ExportCodeFixProvider(LanguageNames.CSharp), Shared] +public sealed class OptionalParametersAttributeFixer : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(RuleIdentifiers.DefaultValueShouldNotBeUsedWhenParameterDefaultValueIsMeant); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var nodeToFix = root?.FindNode(context.Span, getInnermostNodeForTie: true); + if (nodeToFix is null) + return; + + var parameter = nodeToFix as ParameterSyntax ?? nodeToFix.AncestorsAndSelf().OfType().FirstOrDefault(); + if (parameter is null) + return; + + var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); + if (semanticModel is null) + return; + + var defaultValueAttributeSymbol = semanticModel.Compilation.GetBestTypeByMetadataName("System.ComponentModel.DefaultValueAttribute"); + var defaultParameterValueAttributeSymbol = semanticModel.Compilation.GetBestTypeByMetadataName("System.Runtime.InteropServices.DefaultParameterValueAttribute"); + if (defaultValueAttributeSymbol is null || defaultParameterValueAttributeSymbol is null) + return; + + var defaultValueAttribute = parameter.AttributeLists + .SelectMany(a => a.Attributes) + .FirstOrDefault(attribute => + { + var attributeSymbol = semanticModel.GetSymbolInfo(attribute, context.CancellationToken).Symbol?.ContainingType; + return attributeSymbol is not null && attributeSymbol.IsEqualTo(defaultValueAttributeSymbol); + }); + + if (defaultValueAttribute is null) + return; + + var hasDefaultParameterValueAttribute = parameter.AttributeLists + .SelectMany(a => a.Attributes) + .Any(attribute => + { + var attributeSymbol = semanticModel.GetSymbolInfo(attribute, context.CancellationToken).Symbol?.ContainingType; + return attributeSymbol is not null && attributeSymbol.IsEqualTo(defaultParameterValueAttributeSymbol); + }); + + if (hasDefaultParameterValueAttribute) + return; + + context.RegisterCodeFix( + CodeAction.Create( + "Add [DefaultParameterValue]", + ct => AddDefaultParameterValueAttribute(context.Document, defaultValueAttribute, ct), + equivalenceKey: "Add [DefaultParameterValue]"), + context.Diagnostics); + } + + private static async Task AddDefaultParameterValueAttribute(Document document, AttributeSyntax defaultValueAttribute, CancellationToken cancellationToken) + { + var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + if (defaultValueAttribute.Parent is not AttributeListSyntax attributeList) + return document; + + var attribute = Attribute(ParseName("DefaultParameterValue"), defaultValueAttribute.ArgumentList) + .WithLeadingTrivia(Space); + + editor.ReplaceNode(attributeList, attributeList.WithAttributes(attributeList.Attributes.Add(attribute))); + return editor.GetChangedDocument(); + } +} diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/RemoveEmptyBlockFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/RemoveEmptyBlockFixer.cs new file mode 100644 index 000000000..64eaf6fbc --- /dev/null +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/RemoveEmptyBlockFixer.cs @@ -0,0 +1,68 @@ +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Formatting; + +namespace Meziantou.Analyzer.Rules; + +[ExportCodeFixProvider(LanguageNames.CSharp), Shared] +public sealed class RemoveEmptyBlockFixer : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(RuleIdentifiers.RemoveEmptyBlock); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var nodeToFix = root?.FindNode(context.Span, getInnermostNodeForTie: true); + if (nodeToFix is null) + return; + + if (nodeToFix is ElseClauseSyntax || nodeToFix.AncestorsAndSelf().OfType().FirstOrDefault() is not null) + { + context.RegisterCodeFix( + CodeAction.Create( + "Remove empty else block", + ct => RemoveElseClause(context.Document, nodeToFix, ct), + equivalenceKey: "Remove empty else block"), + context.Diagnostics); + } + else if (nodeToFix is FinallyClauseSyntax || nodeToFix.AncestorsAndSelf().OfType().FirstOrDefault() is not null) + { + context.RegisterCodeFix( + CodeAction.Create( + "Remove empty finally block", + ct => RemoveFinallyClause(context.Document, nodeToFix, ct), + equivalenceKey: "Remove empty finally block"), + context.Diagnostics); + } + } + + private static async Task RemoveElseClause(Document document, SyntaxNode nodeToFix, CancellationToken cancellationToken) + { + var elseClause = nodeToFix as ElseClauseSyntax ?? nodeToFix.AncestorsAndSelf().OfType().FirstOrDefault(); + if (elseClause?.Parent is not IfStatementSyntax ifStatement) + return document; + + var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + editor.ReplaceNode(ifStatement, ifStatement.WithElse(null).WithAdditionalAnnotations(Formatter.Annotation)); + return editor.GetChangedDocument(); + } + + private static async Task RemoveFinallyClause(Document document, SyntaxNode nodeToFix, CancellationToken cancellationToken) + { + var finallyClause = nodeToFix as FinallyClauseSyntax ?? nodeToFix.AncestorsAndSelf().OfType().FirstOrDefault(); + if (finallyClause?.Parent is not TryStatementSyntax tryStatement) + return document; + + var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + editor.ReplaceNode(tryStatement, tryStatement.WithFinally(null).WithAdditionalAnnotations(Formatter.Annotation)); + return editor.GetChangedDocument(); + } +} diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/TaskInUsingFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/TaskInUsingFixer.cs new file mode 100644 index 000000000..c5edd2ac4 --- /dev/null +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/TaskInUsingFixer.cs @@ -0,0 +1,45 @@ +using System.Collections.Immutable; +using System.Composition; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Formatting; + +namespace Meziantou.Analyzer.Rules; + +[ExportCodeFixProvider(LanguageNames.CSharp), Shared] +public sealed class TaskInUsingFixer : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(RuleIdentifiers.TaskInUsing); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var nodeToFix = root?.FindNode(context.Span, getInnermostNodeForTie: true); + if (nodeToFix is null) + return; + + context.RegisterCodeFix( + CodeAction.Create( + "Await task", + ct => AddAwait(context.Document, nodeToFix, ct), + equivalenceKey: "Await task"), + context.Diagnostics); + } + + private static async Task AddAwait(Document document, SyntaxNode nodeToFix, CancellationToken cancellationToken) + { + var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + var node = nodeToFix.WithoutTrivia(); + + ExpressionSyntax awaitExpression = SyntaxFactory.AwaitExpression((ExpressionSyntax)node).WithTriviaFrom(nodeToFix); + + editor.ReplaceNode(nodeToFix, awaitExpression.WithAdditionalAnnotations(Formatter.Annotation)); + return editor.GetChangedDocument(); + } +} diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/ThrowIfNullWithNonNullableInstanceFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/ThrowIfNullWithNonNullableInstanceFixer.cs new file mode 100644 index 000000000..adad1f931 --- /dev/null +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/ThrowIfNullWithNonNullableInstanceFixer.cs @@ -0,0 +1,47 @@ +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; + +namespace Meziantou.Analyzer.Rules; + +[ExportCodeFixProvider(LanguageNames.CSharp), Shared] +public sealed class ThrowIfNullWithNonNullableInstanceFixer : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(RuleIdentifiers.ThrowIfNullWithNonNullableInstance); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var nodeToFix = root?.FindNode(context.Span, getInnermostNodeForTie: true); + if (nodeToFix is null) + return; + + var invocation = nodeToFix as InvocationExpressionSyntax ?? nodeToFix.AncestorsAndSelf().OfType().FirstOrDefault(); + if (invocation is null) + return; + + if (invocation.Parent is not ExpressionStatementSyntax) + return; + + context.RegisterCodeFix( + CodeAction.Create( + "Remove useless ThrowIfNull", + ct => RemoveInvocation(context.Document, invocation, ct), + equivalenceKey: "Remove useless ThrowIfNull"), + context.Diagnostics); + } + + private static async Task RemoveInvocation(Document document, InvocationExpressionSyntax invocation, CancellationToken cancellationToken) + { + var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + editor.RemoveNode(invocation.Parent!); + return editor.GetChangedDocument(); + } +} diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/UseStringEqualsInsteadOfIsPatternFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/UseStringEqualsInsteadOfIsPatternFixer.cs new file mode 100644 index 000000000..3df0c2af5 --- /dev/null +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/UseStringEqualsInsteadOfIsPatternFixer.cs @@ -0,0 +1,61 @@ +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using Meziantou.Analyzer.Internals; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace Meziantou.Analyzer.Rules; + +[ExportCodeFixProvider(LanguageNames.CSharp), Shared] +public sealed class UseStringEqualsInsteadOfIsPatternFixer : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(RuleIdentifiers.UseStringEqualsInsteadOfIsPattern); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var nodeToFix = root?.FindNode(context.Span, getInnermostNodeForTie: true); + if (nodeToFix is null) + return; + + var isPatternExpression = nodeToFix as IsPatternExpressionSyntax ?? nodeToFix.AncestorsAndSelf().OfType().FirstOrDefault(); + if (isPatternExpression is null) + return; + + if (isPatternExpression.Pattern is not ConstantPatternSyntax { Expression: ExpressionSyntax constantExpression }) + return; + + context.RegisterCodeFix( + CodeAction.Create( + "Use string.Equals", + ct => ReplaceWithStringEquals(context.Document, isPatternExpression, constantExpression, ct), + equivalenceKey: "Use string.Equals"), + context.Diagnostics); + } + + private static async Task ReplaceWithStringEquals(Document document, IsPatternExpressionSyntax isPatternExpression, ExpressionSyntax constantExpression, CancellationToken cancellationToken) + { + var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + var generator = editor.Generator; + + var stringComparisonType = editor.SemanticModel.Compilation.GetBestTypeByMetadataName("System.StringComparison"); + if (stringComparisonType is null) + return document; + + var newExpression = generator.InvocationExpression( + generator.MemberAccessExpression(generator.TypeExpression(SpecialType.System_String), nameof(string.Equals)), + isPatternExpression.Expression, + constantExpression, + ParseExpression("System.StringComparison.Ordinal")); + + editor.ReplaceNode(isPatternExpression, newExpression); + return editor.GetChangedDocument(); + } +} diff --git a/tests/Meziantou.Analyzer.Test/Rules/ConcurrentDictionaryMustPreventClosureWhenAccessingTheKeyAnalyzerTests_MA0105.cs b/tests/Meziantou.Analyzer.Test/Rules/ConcurrentDictionaryMustPreventClosureWhenAccessingTheKeyAnalyzerTests_MA0105.cs index c694b2f85..81874beb6 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/ConcurrentDictionaryMustPreventClosureWhenAccessingTheKeyAnalyzerTests_MA0105.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/ConcurrentDictionaryMustPreventClosureWhenAccessingTheKeyAnalyzerTests_MA0105.cs @@ -10,7 +10,8 @@ private static ProjectBuilder CreateProjectBuilder() { return new ProjectBuilder() .WithTargetFramework(TargetFramework.Net6_0) - .WithAnalyzer(id: "MA0105"); + .WithAnalyzer(id: "MA0105") + .WithCodeFixProvider(); } [Fact] @@ -90,6 +91,34 @@ await CreateProjectBuilder() .ValidateAsync(); } + [Fact] + public async Task GetOrAdd_StringInterpolation_CodeFix() + { + const string SourceCode = """ + using System.Collections.Concurrent; + + var key = 1; + var value = 1; + var dict = new ConcurrentDictionary(); + dict.GetOrAdd(key, [|k => $"{key}"|]); + """; + + const string FixedCode = """ + using System.Collections.Concurrent; + + var key = 1; + var value = 1; + var dict = new ConcurrentDictionary(); + dict.GetOrAdd(key, k => $"{k}"); + """; + + await CreateProjectBuilder() + .WithOutputKind(Microsoft.CodeAnalysis.OutputKind.ConsoleApplication) + .WithSourceCode(SourceCode) + .ShouldFixCodeWith(FixedCode) + .ValidateAsync(); + } + [Fact] public async Task AddOrUpdate_Parameter() { diff --git a/tests/Meziantou.Analyzer.Test/Rules/ConcurrentDictionaryMustPreventClosureWhenAccessingTheKeyAnalyzerTests_MA0106.cs b/tests/Meziantou.Analyzer.Test/Rules/ConcurrentDictionaryMustPreventClosureWhenAccessingTheKeyAnalyzerTests_MA0106.cs index 509e98d85..ab1cac4e2 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/ConcurrentDictionaryMustPreventClosureWhenAccessingTheKeyAnalyzerTests_MA0106.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/ConcurrentDictionaryMustPreventClosureWhenAccessingTheKeyAnalyzerTests_MA0106.cs @@ -10,7 +10,8 @@ private static ProjectBuilder CreateProjectBuilder() { return new ProjectBuilder() .WithTargetFramework(TargetFramework.Net6_0) - .WithAnalyzer(id: "MA0106"); + .WithAnalyzer(id: "MA0106") + .WithCodeFixProvider(); } [Fact] @@ -103,6 +104,34 @@ await CreateProjectBuilder() .ValidateAsync(); } + [Fact] + public async Task GetOrAdd_Closure_CodeFix() + { + const string SourceCode = """ + using System.Collections.Concurrent; + + var key = 1; + var value = 1; + var a = new ConcurrentDictionary(); + a.GetOrAdd(key, [|_ => value|]); + """; + + const string FixedCode = """ + using System.Collections.Concurrent; + + var key = 1; + var value = 1; + var a = new ConcurrentDictionary(); + a.GetOrAdd(key, (_, arg) => arg, value); + """; + + await CreateProjectBuilder() + .WithOutputKind(Microsoft.CodeAnalysis.OutputKind.ConsoleApplication) + .WithSourceCode(SourceCode) + .ShouldFixCodeWith(FixedCode) + .ValidateAsync(); + } + [Fact] public async Task GetOrAdd_ClosureWithLambdaParameter() { diff --git a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseZeroToInitializeAnEnumValueTests.cs b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseZeroToInitializeAnEnumValueTests.cs index 786cea547..6c1578863 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseZeroToInitializeAnEnumValueTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseZeroToInitializeAnEnumValueTests.cs @@ -8,7 +8,8 @@ public class DoNotUseZeroToInitializeAnEnumValueTests private static ProjectBuilder CreateProjectBuilder() { return new ProjectBuilder() - .WithAnalyzer(); + .WithAnalyzer() + .WithCodeFixProvider(); } public static TheoryData GetCombinationZero() @@ -129,6 +130,35 @@ void A() .ValidateAsync(); } + [Fact] + public async Task Assignation_CodeFix() + { + await CreateProjectBuilder() + .WithSourceCode(""" + enum MyEnum { A = 0, B = 1 } + + class Test + { + void A() + { + MyEnum a = [|0|]; + } + } + """) + .ShouldFixCodeWith(""" + enum MyEnum { A = 0, B = 1 } + + class Test + { + void A() + { + MyEnum a = MyEnum.A; + } + } + """) + .ValidateAsync(); + } + [Fact] public async Task Reassignation() { diff --git a/tests/Meziantou.Analyzer.Test/Rules/OptimizeLinqUsageAnalyzerUseCountInsteadOfAnyTests.cs b/tests/Meziantou.Analyzer.Test/Rules/OptimizeLinqUsageAnalyzerUseCountInsteadOfAnyTests.cs index 794f4ffde..d79cf462a 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/OptimizeLinqUsageAnalyzerUseCountInsteadOfAnyTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/OptimizeLinqUsageAnalyzerUseCountInsteadOfAnyTests.cs @@ -8,7 +8,8 @@ public sealed class OptimizeLinqUsageAnalyzerUseCountInsteadOfAnyTests private static ProjectBuilder CreateProjectBuilder() { return new ProjectBuilder() - .WithAnalyzer(id: RuleIdentifiers.OptimizeEnumerable_UseCountInsteadOfAny); + .WithAnalyzer(id: RuleIdentifiers.OptimizeEnumerable_UseCountInsteadOfAny) + .WithCodeFixProvider(); } [Fact] @@ -30,6 +31,37 @@ await CreateProjectBuilder() .ValidateAsync(); } + [Fact] + public async Task Any_List_CodeFix() + { + const string SourceCode = @"using System.Linq; +class Test +{ + public Test() + { + var collection = new System.Collections.Generic.List(); + _ = [|collection.Any()|]; + } +} +"; + + const string FixedCode = @"using System.Linq; +class Test +{ + public Test() + { + var collection = new System.Collections.Generic.List(); + _ = collection.Count != 0; + } +} +"; + + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ShouldFixCodeWith(FixedCode) + .ValidateAsync(); + } + [Fact] public async Task Any_Array() { diff --git a/tests/Meziantou.Analyzer.Test/Rules/OptionalParametersAttributeAnalyzerMA0088Tests.cs b/tests/Meziantou.Analyzer.Test/Rules/OptionalParametersAttributeAnalyzerMA0088Tests.cs index 5a074eecf..77ae7454e 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/OptionalParametersAttributeAnalyzerMA0088Tests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/OptionalParametersAttributeAnalyzerMA0088Tests.cs @@ -8,7 +8,8 @@ public sealed class OptionalParametersAttributeAnalyzerMA0088Tests private static ProjectBuilder CreateProjectBuilder() { return new ProjectBuilder() - .WithAnalyzer(id: "MA0088"); + .WithAnalyzer(id: "MA0088") + .WithCodeFixProvider(); } [Fact] @@ -67,4 +68,37 @@ await CreateProjectBuilder() .WithSourceCode(SourceCode) .ValidateAsync(); } + + [Fact] + public async Task DefaultValue_CodeFix() + { + const string SourceCode = """ + using System.ComponentModel; + using System.Runtime.InteropServices; + + class Test + { + void A([DefaultValue(10)]int [|a|]) + { + } + } + """; + + const string FixedCode = """ + using System.ComponentModel; + using System.Runtime.InteropServices; + + class Test + { + void A([DefaultValue(10), DefaultParameterValue(10)]int a) + { + } + } + """; + + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ShouldFixCodeWith(FixedCode) + .ValidateAsync(); + } } diff --git a/tests/Meziantou.Analyzer.Test/Rules/RemoveEmptyBlockAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/RemoveEmptyBlockAnalyzerTests.cs index ee54eeedc..dea8be0b1 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/RemoveEmptyBlockAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/RemoveEmptyBlockAnalyzerTests.cs @@ -8,7 +8,8 @@ public sealed class RemoveEmptyBlockAnalyzerTests private static ProjectBuilder CreateProjectBuilder() { return new ProjectBuilder() - .WithAnalyzer(); + .WithAnalyzer() + .WithCodeFixProvider(); } [Fact] @@ -33,6 +34,42 @@ await CreateProjectBuilder() .ValidateAsync(); } + [Fact] + public async Task EmptyElseBlock_CodeFix() + { + const string SourceCode = """ + class Test + { + void A(bool condition) + { + if (condition) + { + } + [|else + { + }|] + } + } + """; + + const string FixedCode = """ + class Test + { + void A(bool condition) + { + if (condition) + { + } + } + } + """; + + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ShouldFixCodeWith(FixedCode) + .ValidateAsync(); + } + [Fact] public async Task ElseBlockContainingABlock() { @@ -152,6 +189,42 @@ await CreateProjectBuilder() .ValidateAsync(); } + [Fact] + public async Task EmptyFinallyBlock_CodeFix() + { + const string SourceCode = """ + class Test + { + void A() + { + try + { + } + [|finally + { + }|] + } + } + """; + + const string FixedCode = """ + class Test + { + void A() + { + try + { + } + } + } + """; + + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ShouldFixCodeWith(FixedCode) + .ValidateAsync(); + } + [Fact] public async Task FinallyBlockWithComment() { diff --git a/tests/Meziantou.Analyzer.Test/Rules/TaskInUsingAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/TaskInUsingAnalyzerTests.cs index ecef195ec..e1ea8a701 100755 --- a/tests/Meziantou.Analyzer.Test/Rules/TaskInUsingAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/TaskInUsingAnalyzerTests.cs @@ -9,7 +9,8 @@ private static ProjectBuilder CreateProjectBuilder() { return new ProjectBuilder() .WithOutputKind(OutputKind.ConsoleApplication) - .WithAnalyzer(); + .WithAnalyzer() + .WithCodeFixProvider(); } [Fact] @@ -27,6 +28,29 @@ await CreateProjectBuilder() .ValidateAsync(); } + [Fact] + public async Task SingleTaskInUsing_CodeFix() + { + const string SourceCode = """ + using System.Threading.Tasks; + + Task t = null; + using ([|t|]) { } + """; + + const string FixedCode = """ + using System.Threading.Tasks; + + Task t = null; + using (await t) { } + """; + + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ShouldFixCodeWith(FixedCode) + .ValidateAsync(); + } + [Fact] public async Task SingleTaskAssignedInUsing() { diff --git a/tests/Meziantou.Analyzer.Test/Rules/ThrowIfNullWithNonNullableInstanceAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/ThrowIfNullWithNonNullableInstanceAnalyzerTests.cs index 408042444..c94d309a0 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/ThrowIfNullWithNonNullableInstanceAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/ThrowIfNullWithNonNullableInstanceAnalyzerTests.cs @@ -11,7 +11,8 @@ private static ProjectBuilder CreateProjectBuilder() return new ProjectBuilder() .WithOutputKind(OutputKind.ConsoleApplication) .WithTargetFramework(TargetFramework.Net7_0) - .WithAnalyzer(); + .WithAnalyzer() + .WithCodeFixProvider(); } [Theory] @@ -52,6 +53,25 @@ await CreateProjectBuilder() .ValidateAsync(); } + [Fact] + public async Task ThrowIfNull_Diagnostic_CodeFix() + { + var sourceCode = """ + int obj = default; + [|System.ArgumentNullException.ThrowIfNull(obj)|]; + """; + + var fixedCode = """ + int obj = default; + + """; + + await CreateProjectBuilder() + .WithSourceCode(sourceCode) + .ShouldFixCodeWith(fixedCode) + .ValidateAsync(); + } + [Fact] public async Task ThrowIfNull_GenericType() { diff --git a/tests/Meziantou.Analyzer.Test/Rules/UseStringEqualsInsteadOfIsPatternAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/UseStringEqualsInsteadOfIsPatternAnalyzerTests.cs index b5ff14046..20d04c9cd 100755 --- a/tests/Meziantou.Analyzer.Test/Rules/UseStringEqualsInsteadOfIsPatternAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/UseStringEqualsInsteadOfIsPatternAnalyzerTests.cs @@ -8,7 +8,8 @@ public sealed class UseStringEqualsInsteadOfIsPatternAnalyzerTests private static ProjectBuilder CreateProjectBuilder() { return new ProjectBuilder() - .WithAnalyzer(); + .WithAnalyzer() + .WithCodeFixProvider(); } [Fact] @@ -83,6 +84,35 @@ await CreateProjectBuilder() .ValidateAsync(); } + [Fact] + public async Task PatternMatching_CodeFix() + { + const string SourceCode = """ +class TypeName +{ + public void Test(string str) + { + _ = str is [|"b"|]; + } +} +"""; + + const string FixedCode = """ +class TypeName +{ + public void Test(string str) + { + _ = string.Equals(str, "b", System.StringComparison.Ordinal); + } +} +"""; + + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ShouldFixCodeWith(FixedCode) + .ValidateAsync(); + } + [Fact] public async Task PatternMatching_Complex1() { From a58689398dc13e54012886a844fa2b38a8bcf955 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Mon, 30 Mar 2026 14:07:25 -0400 Subject: [PATCH 2/6] Fix CI analyzer violations in code fixers Address CA1859, IDE0060, and IDE1006 violations in new code-fixer implementation so publish/build_and_test jobs compile cleanly across matrix versions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Rules/AvoidClosureWhenUsingConcurrentDictionaryFixer.cs | 6 +++--- .../Rules/OptimizeLinqUsageFixer.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/AvoidClosureWhenUsingConcurrentDictionaryFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/AvoidClosureWhenUsingConcurrentDictionaryFixer.cs index 6092e2c35..cb6453fe4 100644 --- a/src/Meziantou.Analyzer.CodeFixers/Rules/AvoidClosureWhenUsingConcurrentDictionaryFixer.cs +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/AvoidClosureWhenUsingConcurrentDictionaryFixer.cs @@ -57,7 +57,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) context.RegisterCodeFix( CodeAction.Create( "Use factoryArgument overload", - ct => UseFactoryArgumentOverload(context.Document, semanticModel, invocationOperation, lambdaOperation, lambdaArgument, ct), + ct => UseFactoryArgumentOverload(context.Document, semanticModel, invocationOperation, lambdaOperation, ct), equivalenceKey: "Use factoryArgument overload"), context.Diagnostics); } @@ -80,7 +80,7 @@ private static async Task UseLambdaParameters(Document document, Seman return editor.GetChangedDocument(); } - private static async Task UseFactoryArgumentOverload(Document document, SemanticModel semanticModel, IInvocationOperation invocationOperation, IAnonymousFunctionOperation lambdaOperation, IArgumentOperation lambdaArgument, CancellationToken cancellationToken) + private static async Task UseFactoryArgumentOverload(Document document, SemanticModel semanticModel, IInvocationOperation invocationOperation, IAnonymousFunctionOperation lambdaOperation, CancellationToken cancellationToken) { if (invocationOperation.Syntax is not InvocationExpressionSyntax invocationSyntax) return document; @@ -236,7 +236,7 @@ private static AnonymousFunctionExpressionSyntax ReplaceSymbolReferences(Anonymo return (AnonymousFunctionExpressionSyntax)rewriter.Visit(lambda)!; } - private static AnonymousFunctionExpressionSyntax? AddParameterToLambda(AnonymousFunctionExpressionSyntax lambda, string parameterName) + private static ParenthesizedLambdaExpressionSyntax? AddParameterToLambda(AnonymousFunctionExpressionSyntax lambda, string parameterName) { var parameter = Parameter(Identifier(parameterName)); diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/OptimizeLinqUsageFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/OptimizeLinqUsageFixer.cs index 907e0fcab..2e2109df4 100644 --- a/src/Meziantou.Analyzer.CodeFixers/Rules/OptimizeLinqUsageFixer.cs +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/OptimizeLinqUsageFixer.cs @@ -45,8 +45,8 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) if (diagnostic.Id == RuleIdentifiers.OptimizeEnumerable_UseCountInsteadOfAny) { - const string codeFixTitle = "Optimize linq usage"; - context.RegisterCodeFix(CodeAction.Create(codeFixTitle, ct => UseCountGreaterThanZero(context.Document, nodeToFix, ct), equivalenceKey: codeFixTitle), context.Diagnostics); + const string CodeFixTitle = "Optimize linq usage"; + context.RegisterCodeFix(CodeAction.Create(CodeFixTitle, ct => UseCountGreaterThanZero(context.Document, nodeToFix, ct), equivalenceKey: CodeFixTitle), context.Diagnostics); return; } From f352e4a157586a29bb23b433fa94aff6d31809c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Mon, 30 Mar 2026 14:22:23 -0400 Subject: [PATCH 3/6] Update generated docs for new code fixers Synchronize rule docs and docs index with newly added code fix providers so check_documentation passes in CI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/README.md | 18 +++++++++--------- docs/Rules/MA0088.md | 2 +- docs/Rules/MA0090.md | 2 +- docs/Rules/MA0099.md | 2 +- docs/Rules/MA0105.md | 2 +- docs/Rules/MA0106.md | 2 +- docs/Rules/MA0112.md | 2 +- docs/Rules/MA0127.md | 2 +- docs/Rules/MA0129.md | 2 +- docs/Rules/MA0131.md | 2 +- 10 files changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/README.md b/docs/README.md index 273a8c80b..ccec7ee0f 100755 --- a/docs/README.md +++ b/docs/README.md @@ -87,9 +87,9 @@ |[MA0085](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0085.md)|Usage|Anonymous delegates should not be used to unsubscribe from Events|⚠️|✔️|❌| |[MA0086](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0086.md)|Design|Do not throw from a finalizer|⚠️|✔️|❌| |[MA0087](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0087.md)|Design|Parameters with \[DefaultParameterValue\] attributes should also be marked \[Optional\]|⚠️|✔️|❌| -|[MA0088](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0088.md)|Design|Use \[DefaultParameterValue\] instead of \[DefaultValue\]|⚠️|✔️|❌| +|[MA0088](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0088.md)|Design|Use \[DefaultParameterValue\] instead of \[DefaultValue\]|⚠️|✔️|✔️| |[MA0089](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0089.md)|Performance|Optimize string method usage|ℹ️|✔️|✔️| -|[MA0090](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0090.md)|Design|Remove empty else/finally block|ℹ️|✔️|❌| +|[MA0090](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0090.md)|Design|Remove empty else/finally block|ℹ️|✔️|✔️| |[MA0091](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0091.md)|Usage|Sender should be 'this' for instance events|⚠️|✔️|✔️| |[MA0092](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0092.md)|Usage|Sender should be 'null' for static events|⚠️|✔️|❌| |[MA0093](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0093.md)|Usage|EventArgs should not be null when raising an event|⚠️|✔️|✔️| @@ -98,20 +98,20 @@ |[MA0096](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0096.md)|Design|A class that implements IComparable\ should also implement IEquatable\|⚠️|✔️|❌| |[MA0097](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0097.md)|Design|A class that implements IComparable\ or IComparable should override comparison operators|⚠️|✔️|❌| |[MA0098](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0098.md)|Performance|Use indexer instead of LINQ methods|ℹ️|✔️|✔️| -|[MA0099](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0099.md)|Usage|Use Explicit enum value instead of 0|⚠️|✔️|❌| +|[MA0099](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0099.md)|Usage|Use Explicit enum value instead of 0|⚠️|✔️|✔️| |[MA0100](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0100.md)|Usage|Await task before disposing of resources|⚠️|✔️|❌| |[MA0101](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0101.md)|Usage|String contains an implicit end of line character|👻|✔️|✔️| |[MA0102](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0102.md)|Design|Make member readonly|ℹ️|✔️|✔️| |[MA0103](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0103.md)|Usage|Use SequenceEqual instead of equality operator|⚠️|✔️|✔️| |[MA0104](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0104.md)|Design|Do not create a type with a name from the BCL|⚠️|❌|❌| -|[MA0105](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0105.md)|Performance|Use the lambda parameters instead of using a closure|ℹ️|✔️|❌| -|[MA0106](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0106.md)|Performance|Avoid closure by using an overload with the 'factoryArgument' parameter|ℹ️|✔️|❌| +|[MA0105](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0105.md)|Performance|Use the lambda parameters instead of using a closure|ℹ️|✔️|✔️| +|[MA0106](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0106.md)|Performance|Avoid closure by using an overload with the 'factoryArgument' parameter|ℹ️|✔️|✔️| |[MA0107](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0107.md)|Design|Do not use object.ToString|ℹ️|❌|❌| |[MA0108](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0108.md)|Usage|Remove redundant argument value|ℹ️|✔️|✔️| |[MA0109](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0109.md)|Design|Consider adding an overload with a Span\ or Memory\|ℹ️|❌|❌| |[MA0110](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0110.md)|Performance|Use the Regex source generator|ℹ️|✔️|✔️| |[MA0111](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0111.md)|Performance|Use string.Create instead of FormattableString|ℹ️|✔️|✔️| -|[MA0112](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0112.md)|Performance|Use 'Count \> 0' instead of 'Any()'|ℹ️|❌|❌| +|[MA0112](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0112.md)|Performance|Use 'Count \> 0' instead of 'Any()'|ℹ️|❌|✔️| |[MA0113](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0113.md)|Design|Use DateTime.UnixEpoch|ℹ️|✔️|✔️| |[MA0114](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0114.md)|Design|Use DateTimeOffset.UnixEpoch|ℹ️|✔️|✔️| |[MA0115](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0115.md)|Usage|Unknown component parameter|⚠️|✔️|❌| @@ -126,11 +126,11 @@ |[MA0124](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0124.md)|Design|Log parameter type is not valid|⚠️|✔️|❌| |[MA0125](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0125.md)|Design|The list of log parameter types contains an invalid type|⚠️|✔️|❌| |[MA0126](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0126.md)|Design|The list of log parameter types contains a duplicate|⚠️|✔️|❌| -|[MA0127](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0127.md)|Usage|Use String.Equals instead of is pattern|⚠️|❌|❌| +|[MA0127](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0127.md)|Usage|Use String.Equals instead of is pattern|⚠️|❌|✔️| |[MA0128](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0128.md)|Usage|Use 'is' operator instead of SequenceEqual|ℹ️|✔️|✔️| -|[MA0129](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0129.md)|Usage|Await task in using statement|⚠️|✔️|❌| +|[MA0129](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0129.md)|Usage|Await task in using statement|⚠️|✔️|✔️| |[MA0130](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0130.md)|Usage|GetType() should not be used on System.Type instances|⚠️|✔️|❌| -|[MA0131](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0131.md)|Usage|ArgumentNullException.ThrowIfNull should not be used with non-nullable types|⚠️|✔️|❌| +|[MA0131](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0131.md)|Usage|ArgumentNullException.ThrowIfNull should not be used with non-nullable types|⚠️|✔️|✔️| |[MA0132](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0132.md)|Design|Do not convert implicitly to DateTimeOffset|⚠️|✔️|❌| |[MA0133](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0133.md)|Design|Use DateTimeOffset instead of relying on the implicit conversion|ℹ️|✔️|❌| |[MA0134](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0134.md)|Usage|Observe result of async calls|⚠️|✔️|❌| diff --git a/docs/Rules/MA0088.md b/docs/Rules/MA0088.md index 8da7710c5..afd70ad1a 100644 --- a/docs/Rules/MA0088.md +++ b/docs/Rules/MA0088.md @@ -1,6 +1,6 @@ # MA0088 - Use \[DefaultParameterValue\] instead of \[DefaultValue\] -Source: [OptionalParametersAttributeAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/OptionalParametersAttributeAnalyzer.cs) +Sources: [OptionalParametersAttributeAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/OptionalParametersAttributeAnalyzer.cs), [OptionalParametersAttributeFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/OptionalParametersAttributeFixer.cs) ````csharp diff --git a/docs/Rules/MA0090.md b/docs/Rules/MA0090.md index d00d8f740..425724bf8 100644 --- a/docs/Rules/MA0090.md +++ b/docs/Rules/MA0090.md @@ -1,6 +1,6 @@ # MA0090 - Remove empty else/finally block -Source: [RemoveEmptyBlockAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/RemoveEmptyBlockAnalyzer.cs) +Sources: [RemoveEmptyBlockAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/RemoveEmptyBlockAnalyzer.cs), [RemoveEmptyBlockFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/RemoveEmptyBlockFixer.cs) This rule detects empty `else` and `finally` blocks that contain no executable code and suggests removing them to improve code readability and reduce clutter. Empty blocks serve no functional purpose and can make code harder to read by adding unnecessary visual noise. diff --git a/docs/Rules/MA0099.md b/docs/Rules/MA0099.md index e97b1522b..8e0bd5b57 100644 --- a/docs/Rules/MA0099.md +++ b/docs/Rules/MA0099.md @@ -1,6 +1,6 @@ # MA0099 - Use Explicit enum value instead of 0 -Source: [DoNotUseZeroToInitializeAnEnumValue.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/DoNotUseZeroToInitializeAnEnumValue.cs) +Sources: [DoNotUseZeroToInitializeAnEnumValue.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/DoNotUseZeroToInitializeAnEnumValue.cs), [DoNotUseZeroToInitializeAnEnumValueFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/DoNotUseZeroToInitializeAnEnumValueFixer.cs) ````c# diff --git a/docs/Rules/MA0105.md b/docs/Rules/MA0105.md index 53d602d32..85fa26eaf 100644 --- a/docs/Rules/MA0105.md +++ b/docs/Rules/MA0105.md @@ -1,6 +1,6 @@ # MA0105 - Use the lambda parameters instead of using a closure -Source: [AvoidClosureWhenUsingConcurrentDictionaryAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/AvoidClosureWhenUsingConcurrentDictionaryAnalyzer.cs) +Sources: [AvoidClosureWhenUsingConcurrentDictionaryAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/AvoidClosureWhenUsingConcurrentDictionaryAnalyzer.cs), [AvoidClosureWhenUsingConcurrentDictionaryFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/AvoidClosureWhenUsingConcurrentDictionaryFixer.cs) ````c# diff --git a/docs/Rules/MA0106.md b/docs/Rules/MA0106.md index 063b5f269..623fb8fef 100644 --- a/docs/Rules/MA0106.md +++ b/docs/Rules/MA0106.md @@ -1,6 +1,6 @@ # MA0106 - Avoid closure by using an overload with the 'factoryArgument' parameter -Source: [AvoidClosureWhenUsingConcurrentDictionaryAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/AvoidClosureWhenUsingConcurrentDictionaryAnalyzer.cs) +Sources: [AvoidClosureWhenUsingConcurrentDictionaryAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/AvoidClosureWhenUsingConcurrentDictionaryAnalyzer.cs), [AvoidClosureWhenUsingConcurrentDictionaryFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/AvoidClosureWhenUsingConcurrentDictionaryFixer.cs) ````c# diff --git a/docs/Rules/MA0112.md b/docs/Rules/MA0112.md index 686232adf..330c7c3a7 100644 --- a/docs/Rules/MA0112.md +++ b/docs/Rules/MA0112.md @@ -1,6 +1,6 @@ # MA0112 - Use 'Count \> 0' instead of 'Any()' -Source: [OptimizeLinqUsageAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/OptimizeLinqUsageAnalyzer.cs) +Sources: [OptimizeLinqUsageAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/OptimizeLinqUsageAnalyzer.cs), [OptimizeLinqUsageFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/OptimizeLinqUsageFixer.cs) For performance reasons, use the `Count` property instead of `Any()` diff --git a/docs/Rules/MA0127.md b/docs/Rules/MA0127.md index d0bbd67ae..59a6aa5c9 100644 --- a/docs/Rules/MA0127.md +++ b/docs/Rules/MA0127.md @@ -1,6 +1,6 @@ # MA0127 - Use String.Equals instead of is pattern -Source: [UseStringEqualsInsteadOfIsPatternAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/UseStringEqualsInsteadOfIsPatternAnalyzer.cs) +Sources: [UseStringEqualsInsteadOfIsPatternAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/UseStringEqualsInsteadOfIsPatternAnalyzer.cs), [UseStringEqualsInsteadOfIsPatternFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/UseStringEqualsInsteadOfIsPatternFixer.cs) Note that this rule is disabled by default and must be enabled manually using an `.editorconfig` file. diff --git a/docs/Rules/MA0129.md b/docs/Rules/MA0129.md index 623c8df37..fbfecac45 100644 --- a/docs/Rules/MA0129.md +++ b/docs/Rules/MA0129.md @@ -1,6 +1,6 @@ # MA0129 - Await task in using statement -Source: [TaskInUsingAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/TaskInUsingAnalyzer.cs) +Sources: [TaskInUsingAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/TaskInUsingAnalyzer.cs), [TaskInUsingFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/TaskInUsingFixer.cs) A `Task` doesn't need to be disposed. When used in a `using` statement, most of the time, developers forgot to await it. diff --git a/docs/Rules/MA0131.md b/docs/Rules/MA0131.md index 72f42e6ae..ff5aa9064 100644 --- a/docs/Rules/MA0131.md +++ b/docs/Rules/MA0131.md @@ -1,6 +1,6 @@ # MA0131 - ArgumentNullException.ThrowIfNull should not be used with non-nullable types -Source: [ThrowIfNullWithNonNullableInstanceAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/ThrowIfNullWithNonNullableInstanceAnalyzer.cs) +Sources: [ThrowIfNullWithNonNullableInstanceAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/ThrowIfNullWithNonNullableInstanceAnalyzer.cs), [ThrowIfNullWithNonNullableInstanceFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/ThrowIfNullWithNonNullableInstanceFixer.cs) `ArgumentNullException.ThrowIfNull` should not be used with non-nullable value, such as `int` or `bool`. From 5b601194cf96c04d5ddf06b6b952c750938355b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Mon, 30 Mar 2026 14:31:57 -0400 Subject: [PATCH 4/6] Use FullPath git repository root helper Update DocumentationGenerator to use FullPath.TryFindGitRepositoryRoot and upgrade Meziantou.Framework.FullPath to 1.1.18. This supports worktree repositories and allows documentation generation to run without custom git-folder logic. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 18 +++++++++--------- .../DocumentationGenerator.csproj | 2 +- src/DocumentationGenerator/Program.cs | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 3a9937a0e..947c97c10 100755 --- a/README.md +++ b/README.md @@ -103,9 +103,9 @@ If you are already using other analyzers, you can check [which rules are duplica |[MA0085](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0085.md)|Usage|Anonymous delegates should not be used to unsubscribe from Events|⚠️|✔️|❌| |[MA0086](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0086.md)|Design|Do not throw from a finalizer|⚠️|✔️|❌| |[MA0087](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0087.md)|Design|Parameters with \[DefaultParameterValue\] attributes should also be marked \[Optional\]|⚠️|✔️|❌| -|[MA0088](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0088.md)|Design|Use \[DefaultParameterValue\] instead of \[DefaultValue\]|⚠️|✔️|❌| +|[MA0088](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0088.md)|Design|Use \[DefaultParameterValue\] instead of \[DefaultValue\]|⚠️|✔️|✔️| |[MA0089](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0089.md)|Performance|Optimize string method usage|ℹ️|✔️|✔️| -|[MA0090](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0090.md)|Design|Remove empty else/finally block|ℹ️|✔️|❌| +|[MA0090](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0090.md)|Design|Remove empty else/finally block|ℹ️|✔️|✔️| |[MA0091](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0091.md)|Usage|Sender should be 'this' for instance events|⚠️|✔️|✔️| |[MA0092](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0092.md)|Usage|Sender should be 'null' for static events|⚠️|✔️|❌| |[MA0093](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0093.md)|Usage|EventArgs should not be null when raising an event|⚠️|✔️|✔️| @@ -114,20 +114,20 @@ If you are already using other analyzers, you can check [which rules are duplica |[MA0096](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0096.md)|Design|A class that implements IComparable\ should also implement IEquatable\|⚠️|✔️|❌| |[MA0097](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0097.md)|Design|A class that implements IComparable\ or IComparable should override comparison operators|⚠️|✔️|❌| |[MA0098](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0098.md)|Performance|Use indexer instead of LINQ methods|ℹ️|✔️|✔️| -|[MA0099](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0099.md)|Usage|Use Explicit enum value instead of 0|⚠️|✔️|❌| +|[MA0099](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0099.md)|Usage|Use Explicit enum value instead of 0|⚠️|✔️|✔️| |[MA0100](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0100.md)|Usage|Await task before disposing of resources|⚠️|✔️|❌| |[MA0101](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0101.md)|Usage|String contains an implicit end of line character|👻|✔️|✔️| |[MA0102](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0102.md)|Design|Make member readonly|ℹ️|✔️|✔️| |[MA0103](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0103.md)|Usage|Use SequenceEqual instead of equality operator|⚠️|✔️|✔️| |[MA0104](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0104.md)|Design|Do not create a type with a name from the BCL|⚠️|❌|❌| -|[MA0105](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0105.md)|Performance|Use the lambda parameters instead of using a closure|ℹ️|✔️|❌| -|[MA0106](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0106.md)|Performance|Avoid closure by using an overload with the 'factoryArgument' parameter|ℹ️|✔️|❌| +|[MA0105](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0105.md)|Performance|Use the lambda parameters instead of using a closure|ℹ️|✔️|✔️| +|[MA0106](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0106.md)|Performance|Avoid closure by using an overload with the 'factoryArgument' parameter|ℹ️|✔️|✔️| |[MA0107](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0107.md)|Design|Do not use object.ToString|ℹ️|❌|❌| |[MA0108](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0108.md)|Usage|Remove redundant argument value|ℹ️|✔️|✔️| |[MA0109](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0109.md)|Design|Consider adding an overload with a Span\ or Memory\|ℹ️|❌|❌| |[MA0110](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0110.md)|Performance|Use the Regex source generator|ℹ️|✔️|✔️| |[MA0111](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0111.md)|Performance|Use string.Create instead of FormattableString|ℹ️|✔️|✔️| -|[MA0112](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0112.md)|Performance|Use 'Count \> 0' instead of 'Any()'|ℹ️|❌|❌| +|[MA0112](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0112.md)|Performance|Use 'Count \> 0' instead of 'Any()'|ℹ️|❌|✔️| |[MA0113](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0113.md)|Design|Use DateTime.UnixEpoch|ℹ️|✔️|✔️| |[MA0114](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0114.md)|Design|Use DateTimeOffset.UnixEpoch|ℹ️|✔️|✔️| |[MA0115](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0115.md)|Usage|Unknown component parameter|⚠️|✔️|❌| @@ -142,11 +142,11 @@ If you are already using other analyzers, you can check [which rules are duplica |[MA0124](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0124.md)|Design|Log parameter type is not valid|⚠️|✔️|❌| |[MA0125](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0125.md)|Design|The list of log parameter types contains an invalid type|⚠️|✔️|❌| |[MA0126](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0126.md)|Design|The list of log parameter types contains a duplicate|⚠️|✔️|❌| -|[MA0127](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0127.md)|Usage|Use String.Equals instead of is pattern|⚠️|❌|❌| +|[MA0127](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0127.md)|Usage|Use String.Equals instead of is pattern|⚠️|❌|✔️| |[MA0128](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0128.md)|Usage|Use 'is' operator instead of SequenceEqual|ℹ️|✔️|✔️| -|[MA0129](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0129.md)|Usage|Await task in using statement|⚠️|✔️|❌| +|[MA0129](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0129.md)|Usage|Await task in using statement|⚠️|✔️|✔️| |[MA0130](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0130.md)|Usage|GetType() should not be used on System.Type instances|⚠️|✔️|❌| -|[MA0131](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0131.md)|Usage|ArgumentNullException.ThrowIfNull should not be used with non-nullable types|⚠️|✔️|❌| +|[MA0131](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0131.md)|Usage|ArgumentNullException.ThrowIfNull should not be used with non-nullable types|⚠️|✔️|✔️| |[MA0132](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0132.md)|Design|Do not convert implicitly to DateTimeOffset|⚠️|✔️|❌| |[MA0133](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0133.md)|Design|Use DateTimeOffset instead of relying on the implicit conversion|ℹ️|✔️|❌| |[MA0134](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0134.md)|Usage|Observe result of async calls|⚠️|✔️|❌| diff --git a/src/DocumentationGenerator/DocumentationGenerator.csproj b/src/DocumentationGenerator/DocumentationGenerator.csproj index 116de5924..8619292c9 100644 --- a/src/DocumentationGenerator/DocumentationGenerator.csproj +++ b/src/DocumentationGenerator/DocumentationGenerator.csproj @@ -6,7 +6,7 @@ - + diff --git a/src/DocumentationGenerator/Program.cs b/src/DocumentationGenerator/Program.cs index 07623845c..eaae9815c 100644 --- a/src/DocumentationGenerator/Program.cs +++ b/src/DocumentationGenerator/Program.cs @@ -10,7 +10,7 @@ using Microsoft.CodeAnalysis.CodeRefactorings; using Microsoft.CodeAnalysis.Diagnostics; -if (!FullPath.CurrentDirectory().TryFindFirstAncestorOrSelf(p => Directory.Exists(p / ".git"), out var outputFolder)) +if (!FullPath.CurrentDirectory().TryFindGitRepositoryRoot(out var outputFolder)) { Console.WriteLine("Cannot find the current git folder"); return 1; From 454710f3dd661cd4f1cecab04edf9ab7eb32e8ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Mon, 30 Mar 2026 15:47:24 -0400 Subject: [PATCH 5/6] Refine MA0099, MA0129, and MA0127 code fixes Add MA0099 enum-zero fallback cast fix/test, tighten MA0129 task-disposable fix applicability and tests, and update MA0127 to offer Ordinal + OrdinalIgnoreCase fixes while making the rule hidden and enabled by default. Regenerate docs and default editorconfig. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 2 +- docs/README.md | 4 +- docs/Rules/MA0127.md | 2 +- ...oNotUseZeroToInitializeAnEnumValueFixer.cs | 39 ++++-- .../Rules/TaskInUsingFixer.cs | 29 +++++ .../UseStringEqualsInsteadOfIsPatternFixer.cs | 27 ++-- .../configuration/default.editorconfig | 2 +- ...eStringEqualsInsteadOfIsPatternAnalyzer.cs | 6 +- ...oNotUseZeroToInitializeAnEnumValueTests.cs | 29 +++++ .../Rules/TaskInUsingAnalyzerTests.cs | 116 ++++++++++++++++++ ...ngEqualsInsteadOfIsPatternAnalyzerTests.cs | 42 ++++++- 11 files changed, 269 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 947c97c10..8e883ca55 100755 --- a/README.md +++ b/README.md @@ -142,7 +142,7 @@ If you are already using other analyzers, you can check [which rules are duplica |[MA0124](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0124.md)|Design|Log parameter type is not valid|⚠️|✔️|❌| |[MA0125](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0125.md)|Design|The list of log parameter types contains an invalid type|⚠️|✔️|❌| |[MA0126](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0126.md)|Design|The list of log parameter types contains a duplicate|⚠️|✔️|❌| -|[MA0127](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0127.md)|Usage|Use String.Equals instead of is pattern|⚠️|❌|✔️| +|[MA0127](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0127.md)|Usage|Use String.Equals instead of is pattern|👻|✔️|✔️| |[MA0128](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0128.md)|Usage|Use 'is' operator instead of SequenceEqual|ℹ️|✔️|✔️| |[MA0129](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0129.md)|Usage|Await task in using statement|⚠️|✔️|✔️| |[MA0130](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0130.md)|Usage|GetType() should not be used on System.Type instances|⚠️|✔️|❌| diff --git a/docs/README.md b/docs/README.md index ccec7ee0f..61e534180 100755 --- a/docs/README.md +++ b/docs/README.md @@ -126,7 +126,7 @@ |[MA0124](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0124.md)|Design|Log parameter type is not valid|⚠️|✔️|❌| |[MA0125](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0125.md)|Design|The list of log parameter types contains an invalid type|⚠️|✔️|❌| |[MA0126](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0126.md)|Design|The list of log parameter types contains a duplicate|⚠️|✔️|❌| -|[MA0127](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0127.md)|Usage|Use String.Equals instead of is pattern|⚠️|❌|✔️| +|[MA0127](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0127.md)|Usage|Use String.Equals instead of is pattern|👻|✔️|✔️| |[MA0128](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0128.md)|Usage|Use 'is' operator instead of SequenceEqual|ℹ️|✔️|✔️| |[MA0129](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0129.md)|Usage|Await task in using statement|⚠️|✔️|✔️| |[MA0130](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0130.md)|Usage|GetType() should not be used on System.Type instances|⚠️|✔️|❌| @@ -584,7 +584,7 @@ dotnet_diagnostic.MA0125.severity = warning dotnet_diagnostic.MA0126.severity = warning # MA0127: Use String.Equals instead of is pattern -dotnet_diagnostic.MA0127.severity = none +dotnet_diagnostic.MA0127.severity = silent # MA0128: Use 'is' operator instead of SequenceEqual dotnet_diagnostic.MA0128.severity = suggestion diff --git a/docs/Rules/MA0127.md b/docs/Rules/MA0127.md index 59a6aa5c9..e5a058ad8 100644 --- a/docs/Rules/MA0127.md +++ b/docs/Rules/MA0127.md @@ -3,7 +3,7 @@ Sources: [UseStringEqualsInsteadOfIsPatternAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/UseStringEqualsInsteadOfIsPatternAnalyzer.cs), [UseStringEqualsInsteadOfIsPatternFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/UseStringEqualsInsteadOfIsPatternFixer.cs) -Note that this rule is disabled by default and must be enabled manually using an `.editorconfig` file. +This rule is enabled by default as a silent suggestion. You should use `string.Equals` instead of `is`, to make string comparison rules explicit. _Similar to [MA0006](./MA0006.md) but for patterns._ diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/DoNotUseZeroToInitializeAnEnumValueFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/DoNotUseZeroToInitializeAnEnumValueFixer.cs index b6ed7a099..ced686dca 100644 --- a/src/Meziantou.Analyzer.CodeFixers/Rules/DoNotUseZeroToInitializeAnEnumValueFixer.cs +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/DoNotUseZeroToInitializeAnEnumValueFixer.cs @@ -43,15 +43,24 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) .OfType() .Where(field => field.HasConstantValue) .FirstOrDefault(field => IsZero(field.ConstantValue)); - if (zeroEnumField is null) - return; - - context.RegisterCodeFix( - CodeAction.Create( - $"Use {zeroEnumField.Name}", - ct => UseEnumField(context.Document, expressionToFix, zeroEnumField, ct), - equivalenceKey: "Use enum member"), - context.Diagnostics); + if (zeroEnumField is not null) + { + context.RegisterCodeFix( + CodeAction.Create( + $"Use {zeroEnumField.Name}", + ct => UseEnumField(context.Document, expressionToFix, zeroEnumField, ct), + equivalenceKey: "Use enum member"), + context.Diagnostics); + } + else + { + context.RegisterCodeFix( + CodeAction.Create( + "Use explicit enum cast", + ct => UseEnumCast(context.Document, expressionToFix, enumType, ct), + equivalenceKey: "Use explicit enum cast"), + context.Diagnostics); + } static bool IsZero(object? value) { @@ -121,4 +130,16 @@ private static async Task UseEnumField(Document document, ExpressionSy editor.ReplaceNode(expressionToFix, memberAccess); return editor.GetChangedDocument(); } + + private static async Task UseEnumCast(Document document, ExpressionSyntax expressionToFix, INamedTypeSymbol enumType, CancellationToken cancellationToken) + { + var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + var castExpression = CastExpression( + (TypeSyntax)ParseName(enumType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", "", StringComparison.Ordinal)), + expressionToFix.WithoutTrivia()) + .WithTriviaFrom(expressionToFix); + + editor.ReplaceNode(expressionToFix, castExpression); + return editor.GetChangedDocument(); + } } diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/TaskInUsingFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/TaskInUsingFixer.cs index c5edd2ac4..e40e0fa33 100644 --- a/src/Meziantou.Analyzer.CodeFixers/Rules/TaskInUsingFixer.cs +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/TaskInUsingFixer.cs @@ -7,6 +7,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Formatting; +using Meziantou.Analyzer.Internals; namespace Meziantou.Analyzer.Rules; @@ -24,6 +25,16 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) if (nodeToFix is null) return; + if (nodeToFix is not ExpressionSyntax expressionSyntax) + return; + + var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); + if (semanticModel is null) + return; + + if (!CanAwaitToDisposable(semanticModel, expressionSyntax, context.CancellationToken)) + return; + context.RegisterCodeFix( CodeAction.Create( "Await task", @@ -32,6 +43,24 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) context.Diagnostics); } + private static bool CanAwaitToDisposable(SemanticModel semanticModel, ExpressionSyntax expressionSyntax, CancellationToken cancellationToken) + { + var type = semanticModel.GetTypeInfo(expressionSyntax, cancellationToken).Type as INamedTypeSymbol; + if (type is null) + return false; + + var taskOfT = semanticModel.Compilation.GetBestTypeByMetadataName("System.Threading.Tasks.Task`1"); + if (taskOfT is null || !type.OriginalDefinition.IsEqualTo(taskOfT) || type.TypeArguments.Length != 1) + return false; + + var disposableType = semanticModel.Compilation.GetBestTypeByMetadataName("System.IDisposable"); + if (disposableType is null) + return false; + + var awaitedType = type.TypeArguments[0]; + return awaitedType.IsOrImplements(disposableType); + } + private static async Task AddAwait(Document document, SyntaxNode nodeToFix, CancellationToken cancellationToken) { var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/UseStringEqualsInsteadOfIsPatternFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/UseStringEqualsInsteadOfIsPatternFixer.cs index 3df0c2af5..b298835f5 100644 --- a/src/Meziantou.Analyzer.CodeFixers/Rules/UseStringEqualsInsteadOfIsPatternFixer.cs +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/UseStringEqualsInsteadOfIsPatternFixer.cs @@ -7,7 +7,7 @@ using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Editing; -using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using Microsoft.CodeAnalysis.Formatting; namespace Meziantou.Analyzer.Rules; @@ -32,15 +32,22 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) if (isPatternExpression.Pattern is not ConstantPatternSyntax { Expression: ExpressionSyntax constantExpression }) return; - context.RegisterCodeFix( - CodeAction.Create( - "Use string.Equals", - ct => ReplaceWithStringEquals(context.Document, isPatternExpression, constantExpression, ct), - equivalenceKey: "Use string.Equals"), - context.Diagnostics); + RegisterCodeFix(nameof(StringComparison.Ordinal)); + RegisterCodeFix(nameof(StringComparison.OrdinalIgnoreCase)); + + void RegisterCodeFix(string comparisonMode) + { + var title = "Use string.Equals " + comparisonMode; + context.RegisterCodeFix( + CodeAction.Create( + title, + ct => ReplaceWithStringEquals(context.Document, isPatternExpression, constantExpression, comparisonMode, ct), + equivalenceKey: title), + context.Diagnostics); + } } - private static async Task ReplaceWithStringEquals(Document document, IsPatternExpressionSyntax isPatternExpression, ExpressionSyntax constantExpression, CancellationToken cancellationToken) + private static async Task ReplaceWithStringEquals(Document document, IsPatternExpressionSyntax isPatternExpression, ExpressionSyntax constantExpression, string comparisonMode, CancellationToken cancellationToken) { var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); var generator = editor.Generator; @@ -53,9 +60,9 @@ private static async Task ReplaceWithStringEquals(Document document, I generator.MemberAccessExpression(generator.TypeExpression(SpecialType.System_String), nameof(string.Equals)), isPatternExpression.Expression, constantExpression, - ParseExpression("System.StringComparison.Ordinal")); + generator.MemberAccessExpression(generator.TypeExpression(stringComparisonType, addImport: true), comparisonMode)); - editor.ReplaceNode(isPatternExpression, newExpression); + editor.ReplaceNode(isPatternExpression, newExpression.WithAdditionalAnnotations(Formatter.Annotation)); return editor.GetChangedDocument(); } } diff --git a/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig index cee0e4864..d49f3766c 100644 --- a/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig +++ b/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig @@ -378,7 +378,7 @@ dotnet_diagnostic.MA0125.severity = warning dotnet_diagnostic.MA0126.severity = warning # MA0127: Use String.Equals instead of is pattern -dotnet_diagnostic.MA0127.severity = none +dotnet_diagnostic.MA0127.severity = silent # MA0128: Use 'is' operator instead of SequenceEqual dotnet_diagnostic.MA0128.severity = suggestion diff --git a/src/Meziantou.Analyzer/Rules/UseStringEqualsInsteadOfIsPatternAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UseStringEqualsInsteadOfIsPatternAnalyzer.cs index e64e5f5da..b7ee6aa6e 100644 --- a/src/Meziantou.Analyzer/Rules/UseStringEqualsInsteadOfIsPatternAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/UseStringEqualsInsteadOfIsPatternAnalyzer.cs @@ -1,4 +1,4 @@ -using System.Collections.Immutable; +using System.Collections.Immutable; using Meziantou.Analyzer.Internals; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; @@ -14,8 +14,8 @@ public sealed class UseStringEqualsInsteadOfIsPatternAnalyzer : DiagnosticAnalyz title: "Use String.Equals instead of is pattern", messageFormat: "Use string.Equals instead of 'is' pattern", RuleCategories.Usage, - DiagnosticSeverity.Warning, - isEnabledByDefault: false, + DiagnosticSeverity.Hidden, + isEnabledByDefault: true, description: "", helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.UseStringEqualsInsteadOfIsPattern)); diff --git a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseZeroToInitializeAnEnumValueTests.cs b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseZeroToInitializeAnEnumValueTests.cs index 6c1578863..98d1bc218 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseZeroToInitializeAnEnumValueTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseZeroToInitializeAnEnumValueTests.cs @@ -159,6 +159,35 @@ void A() .ValidateAsync(); } + [Fact] + public async Task Assignation_CodeFix_NoNamedZero() + { + await CreateProjectBuilder() + .WithSourceCode(""" + enum MyEnum { A = 1, B = 2 } + + class Test + { + void A() + { + MyEnum a = [|0|]; + } + } + """) + .ShouldFixCodeWith(""" + enum MyEnum { A = 1, B = 2 } + + class Test + { + void A() + { + MyEnum a = (MyEnum)0; + } + } + """) + .ValidateAsync(); + } + [Fact] public async Task Reassignation() { diff --git a/tests/Meziantou.Analyzer.Test/Rules/TaskInUsingAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/TaskInUsingAnalyzerTests.cs index e1ea8a701..5f60c10cf 100755 --- a/tests/Meziantou.Analyzer.Test/Rules/TaskInUsingAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/TaskInUsingAnalyzerTests.cs @@ -51,6 +51,122 @@ await CreateProjectBuilder() .ValidateAsync(); } + [Fact] + public async Task TaskOfNonDisposableInUsing_NoCodeFix() + { + const string SourceCode = """ + using System; + using System.Threading.Tasks; + + class Dummy + { + } + + class Test + { + static void Main() { } + + async Task A(IDisposable disposable) + { + Task t = null; + using (disposable) + { + using (var d = [|t|]) { await Task.Yield(); } + } + } + } + """; + + const string FixedCode = """ + using System; + using System.Threading.Tasks; + + class Dummy + { + } + + class Test + { + static void Main() { } + + async Task A(IDisposable disposable) + { + Task t = null; + using (disposable) + { + using (var d = t) { await Task.Yield(); } + } + } + } + """; + + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ShouldFixCodeWith(FixedCode) + .ValidateAsync(); + } + + [Fact] + public async Task TaskOfDisposableInUsing_CodeFix() + { + const string SourceCode = """ + using System; + using System.Threading.Tasks; + + class Dummy : IDisposable + { + public void Dispose() + { + } + } + + class Test + { + static void Main() { } + + async Task A(IDisposable disposable) + { + Task t = null; + using (disposable) + { + using (var d = [|t|]) { await Task.Yield(); } + } + } + } + """; + + const string FixedCode = """ + using System; + using System.Threading.Tasks; + + class Dummy : IDisposable + { + public void Dispose() + { + } + } + + class Test + { + static void Main() { } + + async Task A(IDisposable disposable) + { + Task t = null; + using (disposable) + { + using (var d = await t) { await Task.Yield(); } + } + } + } + """; + + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ShouldFixCodeWith(FixedCode) + .ValidateAsync(); + } + [Fact] public async Task SingleTaskAssignedInUsing() { diff --git a/tests/Meziantou.Analyzer.Test/Rules/UseStringEqualsInsteadOfIsPatternAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/UseStringEqualsInsteadOfIsPatternAnalyzerTests.cs index 20d04c9cd..cf25bffef 100755 --- a/tests/Meziantou.Analyzer.Test/Rules/UseStringEqualsInsteadOfIsPatternAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/UseStringEqualsInsteadOfIsPatternAnalyzerTests.cs @@ -1,4 +1,5 @@ using Meziantou.Analyzer.Rules; +using Microsoft.CodeAnalysis; using TestHelper; namespace Meziantou.Analyzer.Test.Rules; @@ -85,7 +86,7 @@ await CreateProjectBuilder() } [Fact] - public async Task PatternMatching_CodeFix() + public async Task PatternMatching_CodeFix_Ordinal() { const string SourceCode = """ class TypeName @@ -109,7 +110,36 @@ public void Test(string str) await CreateProjectBuilder() .WithSourceCode(SourceCode) - .ShouldFixCodeWith(FixedCode) + .ShouldFixCodeWith(0, FixedCode) + .ValidateAsync(); + } + + [Fact] + public async Task PatternMatching_CodeFix_OrdinalIgnoreCase() + { + const string SourceCode = """ +class TypeName +{ + public void Test(string str) + { + _ = str is [|"b"|]; + } +} +"""; + + const string FixedCode = """ +class TypeName +{ + public void Test(string str) + { + _ = string.Equals(str, "b", System.StringComparison.OrdinalIgnoreCase); + } +} +"""; + + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ShouldFixCodeWith(1, FixedCode) .ValidateAsync(); } @@ -172,4 +202,12 @@ await CreateProjectBuilder() .WithSourceCode(SourceCode) .ValidateAsync(); } + + [Fact] + public void Rule_SeverityAndDefault() + { + var rule = new UseStringEqualsInsteadOfIsPatternAnalyzer().SupportedDiagnostics[0]; + Assert.Equal(DiagnosticSeverity.Hidden, rule.DefaultSeverity); + Assert.True(rule.IsEnabledByDefault); + } } From 96d16a857a9d467680486c36bc524338c6044410 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Mon, 30 Mar 2026 15:53:34 -0400 Subject: [PATCH 6/6] wip --- .../Rules/TaskInUsingAnalyzerTests.cs | 30 ++----------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/tests/Meziantou.Analyzer.Test/Rules/TaskInUsingAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/TaskInUsingAnalyzerTests.cs index 5f60c10cf..27661947f 100755 --- a/tests/Meziantou.Analyzer.Test/Rules/TaskInUsingAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/TaskInUsingAnalyzerTests.cs @@ -18,7 +18,7 @@ public async Task SingleTaskInUsing() { const string SourceCode = """ using System.Threading.Tasks; - + Task t = null; using ([|t|]) { } """; @@ -77,32 +77,8 @@ async Task A(IDisposable disposable) } """; - const string FixedCode = """ - using System; - using System.Threading.Tasks; - - class Dummy - { - } - - class Test - { - static void Main() { } - - async Task A(IDisposable disposable) - { - Task t = null; - using (disposable) - { - using (var d = t) { await Task.Yield(); } - } - } - } - """; - await CreateProjectBuilder() .WithSourceCode(SourceCode) - .ShouldFixCodeWith(FixedCode) .ValidateAsync(); } @@ -172,7 +148,7 @@ public async Task SingleTaskAssignedInUsing() { const string SourceCode = """ using System.Threading.Tasks; - + Task t = null; using (var a = [|t|]) { } """; @@ -187,7 +163,7 @@ public async Task MultipleTasksInUsing() { const string SourceCode = """ using System.Threading.Tasks; - + Task t1 = null; Task t2 = null; using (Task a = [|t1|], b = [|t2|]) { }