// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.LanguageServices;
using Microsoft.CodeAnalysis.Shared.Extensions;
namespace Microsoft.CodeAnalysis.ConvertToInterpolatedString
{
///
/// Code refactoring that converts expresions of the form: a + b + " str " + d + e
/// into:
/// $"{a + b} str {d}{e}".
///
internal abstract class AbstractConvertConcatenationToInterpolatedStringRefactoringProvider : CodeRefactoringProvider
{
public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
{
var (document, textSpan, cancellationToken) = context;
// Currently only supported if there is no selection, to prevent possible confusion when
// selecting part of what would become an interpolated string
if (textSpan.Length > 0)
{
return;
}
var position = textSpan.Start;
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var token = root.FindToken(position);
var syntaxFacts = document.GetLanguageService();
var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
// The selected token has to at least be contained in a concatenation of some form.
// i.e. "goo" + a or 3 + 1 + "goo". However, those concats could be in larger
// concats as well. Walk to the top of that entire chain.
var selectedExpression = token.Parent;
var top = selectedExpression;
while (IsStringConcat(syntaxFacts, top.Parent, semanticModel, cancellationToken))
{
top = top.Parent;
}
if (top == selectedExpression && !IsStringConcat(syntaxFacts, top, semanticModel, cancellationToken))
{
// We weren't in a concatenation at all.
return;
}
// Now walk down the concatenation collecting all the pieces that we are
// concatenating.
var pieces = new List();
CollectPiecesDown(syntaxFacts, pieces, top, semanticModel, cancellationToken);
var stringLiterals = pieces
.Where(x => syntaxFacts.IsStringLiteralExpression(x) || syntaxFacts.IsCharacterLiteralExpression(x))
.ToImmutableArray();
// If the entire expression is just concatenated strings, then don't offer to
// make an interpolated string. The user likely manually split this for
// readability.
if (stringLiterals.Length == pieces.Count)
{
return;
}
var isVerbatimStringLiteral = false;
if (stringLiterals.Length > 0)
{
// Make sure that all the string tokens we're concatenating are the same type
// of string literal. i.e. if we have an expression like: @" "" " + " \r\n "
// then we don't merge this. We don't want to be munging different types of
// escape sequences in these strings, so we only support combining the string
// tokens if they're all the same type.
var firstStringToken = stringLiterals[0].GetFirstToken();
isVerbatimStringLiteral = syntaxFacts.IsVerbatimStringLiteral(firstStringToken);
if (stringLiterals.Any(
lit => isVerbatimStringLiteral != syntaxFacts.IsVerbatimStringLiteral(lit.GetFirstToken())))
{
return;
}
}
var interpolatedString = CreateInterpolatedString(document, isVerbatimStringLiteral, pieces);
context.RegisterRefactoring(new MyCodeAction(
c => UpdateDocumentAsync(document, root, top, interpolatedString, c)));
}
private Task UpdateDocumentAsync(Document document, SyntaxNode root, SyntaxNode top, SyntaxNode interpolatedString, CancellationToken c)
{
var newRoot = root.ReplaceNode(top, interpolatedString);
return Task.FromResult(document.WithSyntaxRoot(newRoot));
}
protected SyntaxNode CreateInterpolatedString(
Document document, bool isVerbatimStringLiteral, List pieces)
{
var syntaxFacts = document.GetLanguageService();
var generator = SyntaxGenerator.GetGenerator(document);
var startToken = CreateInterpolatedStringStartToken(isVerbatimStringLiteral)
.WithLeadingTrivia(pieces.First().GetLeadingTrivia());
var endToken = CreateInterpolatedStringEndToken()
.WithTrailingTrivia(pieces.Last().GetTrailingTrivia());
var content = new List(pieces.Count);
var previousContentWasStringLiteralExpression = false;
foreach (var piece in pieces)
{
var isCharacterLiteral = syntaxFacts.IsCharacterLiteralExpression(piece);
var currentContentIsStringOrCharacterLiteral = syntaxFacts.IsStringLiteralExpression(piece) || isCharacterLiteral;
if (currentContentIsStringOrCharacterLiteral)
{
var text = piece.GetFirstToken().Text;
var textWithEscapedBraces = text.Replace("{", "{{").Replace("}", "}}");
var textWithoutQuotes = GetTextWithoutQuotes(textWithEscapedBraces, isVerbatimStringLiteral, isCharacterLiteral);
if (previousContentWasStringLiteralExpression)
{
// Last part we added to the content list was also an interpolated-string-text-node.
// We need to combine these as the API for creating an interpolated strings
// does not expect to be given a list containing non-contiguous string nodes.
// Essentially if we combine '"A" + 1 + "B" + "C"' into '$"A{1}BC"' it must be:
// {InterpolatedStringText}{Interpolation}{InterpolatedStringText}
// not:
// {InterpolatedStringText}{Interpolation}{InterpolatedStringText}{InterpolatedStringText}
var existingInterpolatedStringTextNode = content.Last();
var newText = ConcatinateTextToTextNode(generator, existingInterpolatedStringTextNode, textWithoutQuotes);
content[content.Count - 1] = newText;
}
else
{
// This is either the first string literal we have encountered or it is the most recent one we've seen
// after adding an interpolation. Add a new interpolated-string-text-node to the list.
content.Add(generator.InterpolatedStringText(generator.InterpolatedStringTextToken(textWithoutQuotes)));
}
}
else
{
content.Add(generator.Interpolation(piece.WithoutTrivia()));
}
// Update this variable to be true every time we encounter a new string literal expression
// so we know to concatinate future string literals together if we encounter them.
previousContentWasStringLiteralExpression = currentContentIsStringOrCharacterLiteral;
}
return generator.InterpolatedStringExpression(startToken, content, endToken);
}
private static SyntaxNode ConcatinateTextToTextNode(SyntaxGenerator generator, SyntaxNode interpolatedStringTextNode, string textWithoutQuotes)
{
var existingText = interpolatedStringTextNode.GetFirstToken().Text;
var newText = existingText + textWithoutQuotes;
return generator.InterpolatedStringText(generator.InterpolatedStringTextToken(newText));
}
protected abstract string GetTextWithoutQuotes(string text, bool isVerbatimStringLiteral, bool isCharacterLiteral);
protected abstract SyntaxToken CreateInterpolatedStringStartToken(bool isVerbatimStringLiteral);
protected abstract SyntaxToken CreateInterpolatedStringEndToken();
private void CollectPiecesDown(
ISyntaxFactsService syntaxFacts,
List pieces,
SyntaxNode node,
SemanticModel semanticModel,
CancellationToken cancellationToken)
{
if (!IsStringConcat(syntaxFacts, node, semanticModel, cancellationToken))
{
pieces.Add(node);
return;
}
syntaxFacts.GetPartsOfBinaryExpression(node, out var left, out var right);
CollectPiecesDown(syntaxFacts, pieces, left, semanticModel, cancellationToken);
pieces.Add(right);
}
private bool IsStringConcat(
ISyntaxFactsService syntaxFacts, SyntaxNode expression,
SemanticModel semanticModel, CancellationToken cancellationToken)
{
if (!syntaxFacts.IsBinaryExpression(expression))
{
return false;
}
return semanticModel.GetSymbolInfo(expression, cancellationToken).Symbol is IMethodSymbol method &&
method.MethodKind == MethodKind.BuiltinOperator &&
method.ContainingType?.SpecialType == SpecialType.System_String &&
(method.MetadataName == WellKnownMemberNames.AdditionOperatorName ||
method.MetadataName == WellKnownMemberNames.ConcatenateOperatorName);
}
private class MyCodeAction : CodeAction.DocumentChangeAction
{
public MyCodeAction(Func> createChangedDocument)
: base(FeaturesResources.Convert_to_interpolated_string, createChangedDocument)
{
}
}
}
}