// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Collections.Immutable; using System.Composition; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp.ConditionalExpressionInStringInterpolation; using static CSharpSyntaxTokens; [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AddParenthesesAroundConditionalExpressionInInterpolatedString), Shared] [method: ImportingConstructor] [method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] internal sealed class CSharpAddParenthesesAroundConditionalExpressionInInterpolatedStringCodeFixProvider() : CodeFixProvider { private const string CS8361 = nameof(CS8361); //A conditional expression cannot be used directly in a string interpolation because the ':' ends the interpolation.Parenthesize the conditional expression. // CS8361 is a syntax error and it is unlikely that there is more than one CS8361 at a time. public override FixAllProvider? GetFixAllProvider() => null; public override ImmutableArray<string> FixableDiagnosticIds => [CS8361]; public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) { var root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); var diagnostic = context.Diagnostics.First(); var diagnosticSpan = diagnostic.Location.SourceSpan; var token = root.FindToken(diagnosticSpan.Start); var conditionalExpression = token.GetAncestor<ConditionalExpressionSyntax>(); if (conditionalExpression != null) { var documentChangeAction = CodeAction.Create( CSharpCodeFixesResources.Add_parentheses_around_conditional_expression_in_interpolated_string, c => GetChangedDocumentAsync(context.Document, conditionalExpression.SpanStart, c), nameof(CSharpCodeFixesResources.Add_parentheses_around_conditional_expression_in_interpolated_string)); context.RegisterCodeFix(documentChangeAction, diagnostic); } } private static async Task<Document> GetChangedDocumentAsync(Document document, int conditionalExpressionSyntaxStartPosition, CancellationToken cancellationToken) { // The usual SyntaxTree transformations are complicated if string literals are present in the false part as in // $"{ condition ? "Success": "Failure" }" // The colon starts a FormatClause and the double quote left to 'F' therefore ends the interpolated string. // The text starting with 'F' is parsed as code and the resulting syntax tree is impractical. // The same problem arises if a } is present in the false part. // To circumvent these problems this solution // 1. Inserts an opening parenthesis // 2. Re-parses the resulting document (now the colon isn't treated as starting a FormatClause anymore) // 3. Replaces the missing CloseParenToken with a new one var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); var openParenthesisPosition = conditionalExpressionSyntaxStartPosition; var textWithOpenParenthesis = text.Replace(openParenthesisPosition, 0, "("); var documentWithOpenParenthesis = document.WithText(textWithOpenParenthesis); var syntaxRoot = await documentWithOpenParenthesis.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); var nodeAtInsertPosition = syntaxRoot.FindNode(new TextSpan(openParenthesisPosition, 0)); if (nodeAtInsertPosition is not ParenthesizedExpressionSyntax parenthesizedExpression || !parenthesizedExpression.CloseParenToken.IsMissing) { return documentWithOpenParenthesis; } return await InsertCloseParenthesisAsync( documentWithOpenParenthesis, parenthesizedExpression, cancellationToken).ConfigureAwait(false); } private static async Task<Document> InsertCloseParenthesisAsync( Document document, ParenthesizedExpressionSyntax parenthesizedExpression, CancellationToken cancellationToken) { var sourceText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); if (parenthesizedExpression.Expression is ConditionalExpressionSyntax conditional && parenthesizedExpression.GetAncestor<InterpolatedStringExpressionSyntax>()?.StringStartToken.Kind() == SyntaxKind.InterpolatedStringStartToken) { // If they have something like: // // var s3 = $""Text1 { true ? ""Text2""[|:|] // NextLineOfCode(); // // We will update this initially to: // // var s3 = $""Text1 { (true ? ""Text2""[|:|] // NextLineOfCode(); // // And we have to decide where the close paren should go. Based on the parse tree, the // 'NextLineOfCode()' expression will be pulled into the WhenFalse portion of the conditional. // So placing the close paren after the conditional woudl result in: 'NextLineOfCode())'. // // However, the user intent is likely that NextLineOfCode is not part of the conditional // So instead find the colon and place the close paren after that, producing: // // var s3 = $""Text1 { (true ? ""Text2"":) // NextLineOfCode(); var endToken = sourceText.AreOnSameLine(conditional.ColonToken, conditional.WhenFalse.GetFirstToken()) ? conditional.WhenFalse.GetLastToken() : conditional.ColonToken; var closeParenPosition = endToken.Span.End; var textWithCloseParenthesis = sourceText.Replace(closeParenPosition, 0, ")"); return document.WithText(textWithCloseParenthesis); } var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); var newCloseParen = CloseParenToken.WithTriviaFrom(parenthesizedExpression.CloseParenToken); var parenthesizedExpressionWithClosingParen = parenthesizedExpression.WithCloseParenToken(newCloseParen); var newRoot = root.ReplaceNode(parenthesizedExpression, parenthesizedExpressionWithClosingParen); return document.WithSyntaxRoot(newRoot); } } |