// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.CodeAnalysis.CodeFixes; using System.Collections.Immutable; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.NetAnalyzers; using System.Threading; using Analyzer.Utilities; namespace Microsoft.NetCore.Analyzers.Runtime { /// <summary> /// CA1820: Test for empty strings using string length /// </summary> public abstract class TestForEmptyStringsUsingStringLengthFixer : CodeFixProvider { private const string TestForEmptyStringCorrectlyUsingIsNullOrEmpty = nameof(TestForEmptyStringCorrectlyUsingIsNullOrEmpty); private const string TestForEmptyStringCorrectlyUsingStringLength = nameof(TestForEmptyStringCorrectlyUsingStringLength); public sealed override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(TestForEmptyStringsUsingStringLengthAnalyzer.RuleId); // Two fixes are offered for the same diagnostic, so the equivalence key decides which one a // fix-all applies. SyntaxEditorFixAllProvider does not filter on it. public sealed override FixAllProvider GetFixAllProvider() => SyntaxEditorFixAllProvider.Create<string?>(context => context.CodeActionEquivalenceKey, ApplyFixAsync); public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) { SyntaxNode root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); SyntaxNode node = root.FindNode(context.Span); SyntaxNode expressionSyntax = GetExpression(node); if (!IsFixableBinaryExpression(expressionSyntax) && !IsFixableInvocationExpression(expressionSyntax)) { return; } SemanticModel model = await context.Document.GetRequiredSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); FixResolution? resolution = TryGetFixResolution(expressionSyntax, model, context.CancellationToken); if (resolution != null) { context.RegisterCodeFix(CreateCodeAction(context, TestForEmptyStringCorrectlyUsingIsNullOrEmpty), context.Diagnostics); context.RegisterCodeFix(CreateCodeAction(context, TestForEmptyStringCorrectlyUsingStringLength), context.Diagnostics); } } private CodeAction CreateCodeAction(CodeFixContext context, string equivalenceKey) { Document document = context.Document; ImmutableArray<Diagnostic> diagnostics = context.Diagnostics; return CodeAction.Create( MicrosoftNetCoreAnalyzersResources.TestForEmptyStringsUsingStringLengthMessage, ct => SyntaxEditorFixAllProvider.ApplyFixesAsync(document, diagnostics, (doc, diagnostic, editor, token) => ApplyFixAsync(doc, diagnostic, editor, equivalenceKey, token), ct), equivalenceKey); } private async Task ApplyFixAsync(Document document, Diagnostic diagnostic, SyntaxEditor editor, string? equivalenceKey, CancellationToken cancellationToken) { SyntaxNode expressionSyntax = GetExpression(editor.OriginalRoot.FindNode(diagnostic.Location.SourceSpan)); if (!IsFixableBinaryExpression(expressionSyntax) && !IsFixableInvocationExpression(expressionSyntax)) { return; } SemanticModel model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); if (TryGetFixResolution(expressionSyntax, model, cancellationToken) is not FixResolution resolution) { return; } if (equivalenceKey == TestForEmptyStringCorrectlyUsingIsNullOrEmpty) { ConvertToMethodInvocation(editor, resolution); } else if (equivalenceKey == TestForEmptyStringCorrectlyUsingStringLength) { ConvertToStringLengthComparison(editor, resolution); } } private FixResolution? TryGetFixResolution(SyntaxNode expressionSyntax, SemanticModel model, CancellationToken cancellationToken) { if (IsFixableBinaryExpression(expressionSyntax)) { bool isEqualsOperator = IsEqualsOperator(expressionSyntax); SyntaxNode leftOperand = GetLeftOperand(expressionSyntax); SyntaxNode rightOperand = GetRightOperand(expressionSyntax); if (ContainsSystemStringEmpty(leftOperand, model, cancellationToken) || ContainsEmptyStringLiteral(leftOperand, model, cancellationToken)) { return new FixResolution(expressionSyntax, rightOperand, isEqualsOperator); } if (ContainsSystemStringEmpty(rightOperand, model, cancellationToken) || ContainsEmptyStringLiteral(rightOperand, model, cancellationToken)) { return new FixResolution(expressionSyntax, leftOperand, isEqualsOperator); } } else if (IsFixableInvocationExpression(expressionSyntax)) { SyntaxNode? target = GetInvocationTarget(expressionSyntax); if (target == null) { return null; } return new FixResolution(expressionSyntax, target, true); } return null; } private static bool ContainsSystemStringEmpty(SyntaxNode expressionSyntax, SemanticModel model, CancellationToken cancellationToken) { if (model.GetSymbolInfo(expressionSyntax, cancellationToken).Symbol is IFieldSymbol fieldSymbol) { if (fieldSymbol.Type.SpecialType == SpecialType.System_String) { return fieldSymbol.IsReadOnly && fieldSymbol.Name == "Empty"; } } return false; } private static void ConvertToMethodInvocation(SyntaxEditor editor, FixResolution fixResolution) { // The replacement carries the target over from inside the node being replaced, so track it: // a nested violation may already have rewritten it. editor.TrackNode(fixResolution.Target); editor.ReplaceNode(fixResolution.ExpressionSyntax, (currentNode, generator) => { SyntaxNode target = currentNode.GetCurrentNode(fixResolution.Target) ?? fixResolution.Target; SyntaxNode typeNameSyntax = generator.TypeExpression(SpecialType.System_String); SyntaxNode nullOrEmptyMemberSyntax = generator.MemberAccessExpression(typeNameSyntax, "IsNullOrEmpty"); SyntaxNode nullOrEmptyInvocationSyntax = generator.InvocationExpression(nullOrEmptyMemberSyntax, target.WithoutTrailingTrivia()); SyntaxNode replacementSyntax = fixResolution.UsesEqualsOperator ? nullOrEmptyInvocationSyntax : generator.LogicalNotExpression(nullOrEmptyInvocationSyntax); return replacementSyntax.WithAdditionalAnnotations(Formatter.Annotation).WithTriviaFrom(currentNode); }); } private void ConvertToStringLengthComparison(SyntaxEditor editor, FixResolution fixResolution) { SyntaxNode originalLeftOperand = GetLeftOperand(fixResolution.ExpressionSyntax); SyntaxNode originalRightOperand = GetRightOperand(fixResolution.ExpressionSyntax); bool targetIsLeftOperand = originalLeftOperand == fixResolution.Target; editor.TrackNode(originalLeftOperand); editor.TrackNode(originalRightOperand); editor.ReplaceNode(fixResolution.ExpressionSyntax, (currentNode, generator) => { SyntaxNode leftOperand = currentNode.GetCurrentNode(originalLeftOperand) ?? originalLeftOperand; SyntaxNode rightOperand = currentNode.GetCurrentNode(originalRightOperand) ?? originalRightOperand; // Take the below example: // if (f == String.Empty) ... // The comparison operand, f, will now become 'f.Length' and a the other operand will become '0' SyntaxNode zeroLengthSyntax = generator.LiteralExpression(0); if (targetIsLeftOperand) { leftOperand = generator.MemberAccessExpression(leftOperand, "Length"); rightOperand = zeroLengthSyntax.WithTriviaFrom(rightOperand); } else { leftOperand = zeroLengthSyntax; rightOperand = generator.MemberAccessExpression(rightOperand.WithoutTrivia(), "Length"); } SyntaxNode replacementSyntax = fixResolution.UsesEqualsOperator ? generator.ValueEqualsExpression(leftOperand, rightOperand) : generator.ValueNotEqualsExpression(leftOperand, rightOperand); return replacementSyntax.WithAdditionalAnnotations(Formatter.Annotation); }); } private static bool ContainsEmptyStringLiteral(SyntaxNode node, SemanticModel model, CancellationToken cancellationToken) => model.GetConstantValue(node, cancellationToken) is Optional<object> optionalValue && optionalValue.HasValue && optionalValue.Value is string value && value.Length == 0; protected abstract SyntaxNode GetExpression(SyntaxNode node); protected abstract bool IsFixableBinaryExpression(SyntaxNode node); protected abstract bool IsFixableInvocationExpression(SyntaxNode node); protected abstract bool IsEqualsOperator(SyntaxNode node); protected abstract bool IsNotEqualsOperator(SyntaxNode node); protected abstract SyntaxNode GetLeftOperand(SyntaxNode binaryExpressionSyntax); protected abstract SyntaxNode GetRightOperand(SyntaxNode binaryExpressionSyntax); protected abstract SyntaxNode? GetInvocationTarget(SyntaxNode node); private sealed class FixResolution { public SyntaxNode ExpressionSyntax { get; } public SyntaxNode Target { get; } public bool UsesEqualsOperator { get; } public FixResolution(SyntaxNode expressionSyntax, SyntaxNode target, bool usesEqualsOperator) { ExpressionSyntax = expressionSyntax; Target = target; UsesEqualsOperator = usesEqualsOperator; } } } }