File: JSInteropCodeFixProvider.cs
Web Access
Project: src\aspnetcore\src\Components\Analyzers\src\Microsoft.AspNetCore.Components.Analyzers.csproj (Microsoft.AspNetCore.Components.Analyzers)
// 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.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
 
namespace Microsoft.AspNetCore.Components.Analyzers;
 
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(JSInteropCodeFixProvider)), Shared]
public sealed class JSInteropCodeFixProvider : CodeFixProvider
{
    private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.UnguardedJSInteropCall_FixTitle), Resources.ResourceManager, typeof(Resources));
 
    public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(DiagnosticDescriptors.UnguardedJSInteropCall.Id);
 
    public sealed override FixAllProvider GetFixAllProvider()
    {
        return WellKnownFixAllProviders.BatchFixer;
    }
 
    public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
    {
        var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
        if (root is null)
        {
            return;
        }
 
        var diagnostic = context.Diagnostics[0];
        var diagnosticSpan = diagnostic.Location.SourceSpan;
 
        var invocation = root.FindToken(diagnosticSpan.Start)
            .Parent?
            .AncestorsAndSelf()
            .OfType<InvocationExpressionSyntax>()
            .FirstOrDefault();
 
        if (invocation is null)
        {
            return;
        }
 
        if (invocation.Ancestors().OfType<AnonymousFunctionExpressionSyntax>().Any())
        {
            return;
        }
 
        var statement = invocation.FirstAncestorOrSelf<StatementSyntax>();
        if (statement is not ExpressionStatementSyntax expressionStatement)
        {
            return;
        }
 
        var title = Title.ToString(CultureInfo.CurrentCulture);
        context.RegisterCodeFix(
            CodeAction.Create(
                title: title,
                createChangedDocument: cancellationToken => TryCatchWrapJSInteropCallAsync(context.Document, root, expressionStatement, cancellationToken),
                equivalenceKey: title),
            diagnostic);
    }
 
    private static async Task<Document> TryCatchWrapJSInteropCallAsync(Document document, SyntaxNode root, ExpressionStatementSyntax expressionStatement, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
 
        // Derive indentation and line ending from the original statement so existing trivia is reused
        var leadingTrivia = expressionStatement.GetLeadingTrivia();
        var trailingTrivia = expressionStatement.GetTrailingTrivia();
 
        // Prefer the statement's own line ending, then any in the document, then CRLF.
        var eol = leadingTrivia.FirstOrDefault(t => t.IsKind(SyntaxKind.EndOfLineTrivia));
        if (eol == default)
        {
            eol = root.DescendantTrivia().FirstOrDefault(t => t.IsKind(SyntaxKind.EndOfLineTrivia));
            if (eol == default)
            {
                eol = SyntaxFactory.CarriageReturnLineFeed;
            }
        }
 
        var indentTrivia = leadingTrivia.Where(t => t.IsKind(SyntaxKind.WhitespaceTrivia)).LastOrDefault();
        var indent = indentTrivia == default ? "" : indentTrivia.ToFullString();
 
        // Use the document's indentation settings for the added indent level
        var options = await document.GetOptionsAsync(cancellationToken).ConfigureAwait(false);
        var useTabs = options.GetOption(FormattingOptions.UseTabs, LanguageNames.CSharp);
        var indentSize = options.GetOption(FormattingOptions.IndentationSize, LanguageNames.CSharp);
        var singleIndent = useTabs ? "\t" : new string(' ', indentSize);
 
        var indentList = SyntaxFactory.TriviaList(SyntaxFactory.Whitespace(indent));
        var innerIndent = SyntaxFactory.Whitespace(indent + singleIndent);
        var eolList = SyntaxFactory.TriviaList(eol);
 
        var tryStatement = SyntaxFactory.TryStatement(
            SyntaxFactory.Block(
                SyntaxFactory.SingletonList<StatementSyntax>(
                    expressionStatement
                        .WithLeadingTrivia(innerIndent)
                        .WithTrailingTrivia(eol)))
            .WithOpenBraceToken(SyntaxFactory.Token(indentList, SyntaxKind.OpenBraceToken, eolList))
            .WithCloseBraceToken(SyntaxFactory.Token(indentList, SyntaxKind.CloseBraceToken, eolList)),
            SyntaxFactory.SingletonList(
                SyntaxFactory.CatchClause()
                    .WithCatchKeyword(SyntaxFactory.Token(indentList, SyntaxKind.CatchKeyword, SyntaxFactory.TriviaList(SyntaxFactory.Space)))
                    .WithDeclaration(
                        SyntaxFactory.CatchDeclaration(
                            SyntaxFactory.ParseTypeName("Exception"))
                            .WithCloseParenToken(SyntaxFactory.Token(SyntaxTriviaList.Empty, SyntaxKind.CloseParenToken, eolList)))
                    .WithBlock(
                        SyntaxFactory.Block()
                            .WithOpenBraceToken(SyntaxFactory.Token(indentList, SyntaxKind.OpenBraceToken, eolList))
                            .WithCloseBraceToken(SyntaxFactory.Token(indentList, SyntaxKind.CloseBraceToken, SyntaxTriviaList.Empty)))),
            null)
        .WithTryKeyword(SyntaxFactory.Token(leadingTrivia, SyntaxKind.TryKeyword, eolList))
        .WithTrailingTrivia(trailingTrivia);
 
        var newRoot = root.ReplaceNode(expressionStatement, tryStatement);
        if (newRoot is null)
        {
            return document;
        }
 
        return document.WithSyntaxRoot(newRoot);
    }
}