| File: Microsoft.NetCore.Analyzers\Runtime\UseRegexMembersFixer.cs | Web Access |
| Project: src\sdk\src\Microsoft.CodeAnalysis.NetAnalyzers\src\Microsoft.CodeAnalysis.NetAnalyzers\Microsoft.CodeAnalysis.NetAnalyzers.csproj (Microsoft.CodeAnalysis.NetAnalyzers) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Immutable; using System.Composition; using System.Linq; using System.Threading; using System.Threading.Tasks; using Analyzer.Utilities; using Analyzer.Utilities.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.NetAnalyzers; using Microsoft.CodeAnalysis.Operations; namespace Microsoft.NetCore.Analyzers.Runtime { using static MicrosoftNetCoreAnalyzersResources; /// <summary> /// CA1874: <inheritdoc cref="UseRegexIsMatchMessage"/> /// CA1875: <inheritdoc cref="UseRegexCountMessage"/> /// </summary> [ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic), Shared] public sealed class UseRegexMembersFixer : CodeFixProvider { public sealed override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create( UseRegexMembers.RegexIsMatchRuleId, UseRegexMembers.RegexCountRuleId); // The two rules carry different fix titles, and so different equivalence keys, which // SyntaxEditorFixAllProvider does not filter on - so the state is the key to apply. public sealed override FixAllProvider GetFixAllProvider() => SyntaxEditorFixAllProvider.Create<string?>(context => context.CodeActionEquivalenceKey, ApplyFixAsync); public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) { Document doc = context.Document; SemanticModel model = await doc.GetRequiredSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); SyntaxNode root = await doc.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); SyntaxNode node = root.FindNode(context.Span, getInnermostNodeForTie: true); if (GetRegexCall(model, node, context.CancellationToken) is null || GetMemberName(context.Diagnostics[0].Id) is null || GetTitle(context.Diagnostics[0].Id) is not string title) { return; } ImmutableArray<Diagnostic> diagnostics = context.Diagnostics; context.RegisterCodeFix( CodeAction.Create( title, cancellationToken => SyntaxEditorFixAllProvider.ApplyFixesAsync( doc, diagnostics, (document, diagnostic, editor, token) => ApplyFixAsync(document, diagnostic, editor, title, token), cancellationToken), equivalenceKey: title), diagnostics); } private static async Task ApplyFixAsync(Document document, Diagnostic diagnostic, SyntaxEditor editor, string? equivalenceKey, CancellationToken cancellationToken) { if (equivalenceKey is not null && GetTitle(diagnostic.Id) != equivalenceKey) { return; } SemanticModel model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); SyntaxNode node = editor.OriginalRoot.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); if (GetRegexCall(model, node, cancellationToken) is not IInvocationOperation regexCall || GetMemberName(diagnostic.Id) is not string memberName || !model.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemTextRegularExpressionsRegex, out INamedTypeSymbol? regexType)) { return; } SyntaxNode? instance = regexCall.Instance?.Syntax; ImmutableArray<SyntaxNode> arguments = regexCall.Arguments.Select(argument => argument.Syntax).ToImmutableArray(); if (instance is not null) { editor.TrackNode(instance); } foreach (SyntaxNode argument in arguments) { editor.TrackNode(argument); } // The receiver and the arguments are carried over from inside the node being replaced, so they have // to be read back off the current node - either can itself hold a diagnostic already fixed here. editor.ReplaceNode(node, (currentNode, generator) => { SyntaxNode target = instance is null ? generator.TypeExpressionForStaticMemberAccess(regexType) : currentNode.GetCurrentNode(instance) ?? instance; // Swap in the new member name, dropping the subsequent property access, and keep the arguments. return generator.InvocationExpression( generator.MemberAccessExpression(target, memberName), arguments.Select(argument => currentNode.GetCurrentNode(argument) ?? argument)) .WithTriviaFrom(currentNode); }); } private static IInvocationOperation? GetRegexCall(SemanticModel model, SyntaxNode node, CancellationToken cancellationToken) { return model.GetOperation(node, cancellationToken) is IPropertyReferenceOperation operation && operation.Instance is IInvocationOperation regexCall ? regexCall : null; } private static string? GetTitle(string ruleId) => ruleId switch { UseRegexMembers.RegexIsMatchRuleId => UseRegexIsMatchFix, UseRegexMembers.RegexCountRuleId => UseRegexCountFix, _ => null, }; private static string? GetMemberName(string ruleId) => ruleId switch { UseRegexMembers.RegexIsMatchRuleId => "IsMatch", UseRegexMembers.RegexCountRuleId => "Count", _ => null, }; } }