// 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.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.LanguageServices;
using Microsoft.CodeAnalysis.PooledObjects;
namespace Microsoft.CodeAnalysis.Wrapping.ChainedExpression
{
///
/// Finds and wraps 'chained' expressions. For the purpose of this feature, a chained
/// expression is built out of 'chunks' where each chunk is of the form
///
///
/// . name (arglist) remainder
///
///
/// So, if there are two or more of these like:
///
///
/// . name1 (arglist1) remainder1 . name2 (arglist2) remainder2
///
///
/// Then this will be wrapped such that the dots align like so:
///
///
/// . name1 (arglist1) remainder1
/// . name2 (arglist2) remainder2
///
///
/// Note: for the sake of simplicity, (arglist) is used both for the argument list of
/// an InvocationExpression and an ElementAccessExpression.
///
/// 'remainder' is all the postfix expression that can follow . name (arglist). i.e.
/// member-access expressions, conditional-access expressions, etc. Effectively, anything
/// the language allows at this point as long as it doesn't start another 'chunk' itself.
///
/// This approach gives an intuitive wrapping algorithm that matches the common way
/// many wrap dotted invocations, while also effectively not limiting the wrapper to
/// only simple forms like .a(...).b(...).c(...).
///
internal abstract partial class AbstractChainedExpressionWrapper<
TNameSyntax,
TBaseArgumentListSyntax> : AbstractSyntaxWrapper
where TNameSyntax : SyntaxNode
where TBaseArgumentListSyntax : SyntaxNode
{
private readonly ISyntaxFactsService _syntaxFacts;
private readonly int _dotToken;
private readonly int _questionToken;
protected AbstractChainedExpressionWrapper(
Indentation.IIndentationService indentationService,
ISyntaxFactsService syntaxFacts) : base(indentationService)
{
_syntaxFacts = syntaxFacts;
_dotToken = syntaxFacts.SyntaxKinds.DotToken;
_questionToken = syntaxFacts.SyntaxKinds.QuestionToken;
}
///
/// Gets the language specific trivia that should be inserted before an operator if the
/// user wants to wrap the operator to the next line. For C# this is a simple newline-trivia.
/// For VB, this will be a line-continuation char (_), followed by a newline.
///
protected abstract SyntaxTriviaList GetNewLineBeforeOperatorTrivia(SyntaxTriviaList newLine);
public sealed override async Task TryCreateComputerAsync(
Document document, int position, SyntaxNode node, CancellationToken cancellationToken)
{
// We have to be on a chain part. If not, there's nothing to do here at all.
if (!IsDecomposableChainPart(node))
{
return null;
}
// Has to be the topmost chain part. If we're not on the topmost, then just
// bail out here. Our caller will continue walking upwards until it hits the
// topmost node.
if (IsDecomposableChainPart(node.Parent))
{
return null;
}
// We're at the top of something that looks like it could be part of a chained
// expression. Break it into the individual chunks. We need to have at least
// two chunks or this to be worth wrapping.
//
// i.e. if we only have this.Goo(...) there's nothing to wrap. However, we can
// wrap when we have this.Goo(...).Bar(...).
var chunks = GetChainChunks(node);
if (chunks.Length <= 1)
{
return null;
}
// If any of these chunk parts are unformattable, then we don't want to offer anything
// here as we may make formatting worse for this construct.
foreach (var chunk in chunks)
{
var unformattable = await ContainsUnformattableContentAsync(
document, chunk, cancellationToken).ConfigureAwait(false);
if (unformattable)
{
return null;
}
}
// Looks good. Create the action computer which will actually determine
// the set of wrapping options to provide.
var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
var options = await document.GetOptionsAsync(cancellationToken).ConfigureAwait(false);
return new CallExpressionCodeActionComputer(
this, document, sourceText, options, chunks, cancellationToken);
}
private ImmutableArray> GetChainChunks(SyntaxNode node)
{
// First, just take the topmost chain node and break into the individual
// nodes and tokens we want to treat as individual elements. i.e. an
// element that would be kept together. For example, the arg-list of an
// invocation is an element we do not want to ever break-up/wrap.
using var _ = ArrayBuilder.GetInstance(out var pieces);
Decompose(node, pieces);
// Now that we have the pieces, find 'chunks' similar to the form:
//
// . Name (...) Remainder...
//
// These will be the chunks that are wrapped and aligned on that dot.
//
// Here 'remainder' is everything up to the next . Name (...) chunk.
var chunks = ArrayBuilder>.GetInstance();
BreakPiecesIntoChunks(pieces, chunks);
return chunks.ToImmutableAndFree();
}
private void BreakPiecesIntoChunks(
ArrayBuilder pieces,
ArrayBuilder> chunks)
{
// Have to look for the first chunk after the first piece. i.e. if the pieces
// starts with .Foo().Bar().Baz() then the chunks would be .Bar()
// and .Baz().
//
// However, if we had this.Foo().Bar().Baz() then the chunks would be
// .Foo() .Bar() and .Baz().
//
// Note: the only way to get the .Foo().Bar().Baz() case today is in VB in
// a 'with' statement. if we have that, we don't want to wrap it into:
//
//
// with ...
// .Foo()
// .Bar()
// .Baz()
//
//
// Instead, we want to create
//
//
// with ...
// .Foo().Bar()
// .Baz()
//
var currentChunkStart = FindNextChunkStart(pieces, firstChunk: true, index: 1);
if (currentChunkStart < 0)
{
return;
}
while (true)
{
// Look for the next chunk starting after the current chunk we're on.
var nextChunkStart = FindNextChunkStart(pieces, firstChunk: false, index: currentChunkStart + 1);
if (nextChunkStart < 0)
{
// No next chunk after the current one. The current chunk just
// extends to the end of the pieces.
chunks.Add(GetSubRange(pieces, currentChunkStart, end: pieces.Count));
return;
}
// Had a chunk after this one. Record the current chunk, move to the start
// of the next one, and then keep going.
chunks.Add(GetSubRange(pieces, currentChunkStart, end: nextChunkStart));
currentChunkStart = nextChunkStart;
}
}
///
/// Looks for the next sequence of . Name (ArgList). Note, except for the first
/// chunk, this cannot be of the form ? . Name (ArgList) as we do not want to
/// wrap before a dot in a ?. form. This doesn't matter for the first chunk as
/// we won't be wrapping that one.
///
private int FindNextChunkStart(
ArrayBuilder pieces, bool firstChunk, int index)
{
for (var i = index; i < pieces.Count; i++)
{
if (IsToken(_dotToken, pieces, i) &&
IsNode(pieces, i + 1) &&
IsNode(pieces, i + 2))
{
if (firstChunk ||
!IsToken(_questionToken, pieces, i - 1))
{
return i;
}
}
}
// Couldn't find the start of another chunk.
return -1;
}
private bool IsNode(ArrayBuilder pieces, int index)
=> index < pieces.Count &&
pieces[index] is var piece &&
piece.IsNode &&
piece.AsNode() is TNode;
private bool IsToken(int tokenKind, ArrayBuilder pieces, int index)
=> index < pieces.Count &&
pieces[index] is var piece &&
piece.IsToken &&
piece.AsToken().RawKind == tokenKind;
private ImmutableArray GetSubRange(
ArrayBuilder pieces, int start, int end)
{
using var resultDisposer = ArrayBuilder.GetInstance(end - start, out var result);
for (var i = start; i < end; i++)
{
result.Add(pieces[i]);
}
return result.ToImmutable();
}
private bool IsDecomposableChainPart(SyntaxNode node)
{
// This is the effective set of language constructs that can 'chain'
// off of a call .M(...). They are:
//
// 1. .Name or ->Name. i.e. .M(...).Name
// 2. (...). i.e. .M(...)(...)
// 3. [...]. i.e. .M(...)[...]
// 4. ++, --, !. i.e. .M(...)++
// 5. ?. i.e. .M(...)?. ... or .M(...)?[...]
// '5' handles both the ConditionalAccess and MemberBinding cases below.
if (node != null)
{
return _syntaxFacts.IsAnyMemberAccessExpression(node)
|| _syntaxFacts.IsInvocationExpression(node)
|| _syntaxFacts.IsElementAccessExpression(node)
|| _syntaxFacts.IsPostfixUnaryExpression(node)
|| _syntaxFacts.IsConditionalAccessExpression(node)
|| _syntaxFacts.IsMemberBindingExpression(node);
}
return false;
}
///
/// Recursively walks down decomposing it into the individual
/// tokens and nodes we want to look for chunks in.
///
private void Decompose(SyntaxNode node, ArrayBuilder pieces)
{
// Ignore null nodes, they are never relevant when building up the sequence of
// pieces in this chained expression.
if (node is null)
{
return;
}
// We've hit some node that can't be decomposed further (like an argument list,
// or name node). Just add directly to the pieces list.
if (!IsDecomposableChainPart(node))
{
pieces.Add(node);
return;
}
// For everything else that is a chain part, just decompose into its constituent
// parts and add to the pieces array.
foreach (var child in node.ChildNodesAndTokens())
{
if (child.IsNode)
{
Decompose(child.AsNode(), pieces);
}
else
{
pieces.Add(child.AsToken());
}
}
}
}
}