File: src\Analyzers\CSharp\CodeFixes\UseCollectionExpression\CSharpCollectionExpressionRewriter.cs
Web Access
Project: src\src\CodeStyle\CSharp\CodeFixes\Microsoft.CodeAnalysis.CSharp.CodeStyle.Fixes.csproj (Microsoft.CodeAnalysis.CSharp.CodeStyle.Fixes)
// 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.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Formatting;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Indentation;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.UseCollectionExpression;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.UseCollectionExpression;
 
using static CSharpSyntaxTokens;
using static SyntaxFactory;
 
internal static class CSharpCollectionExpressionRewriter
{
    /// <summary>
    /// Creates the final collection-expression <c>[...]</c> that will replace the given <paramref
    /// name="expressionToReplace"/> expression.
    /// </summary>
    public static async Task<CollectionExpressionSyntax> CreateCollectionExpressionAsync<TParentExpression, TMatchNode>(
        Document workspaceDocument,
        TParentExpression expressionToReplace,
        ImmutableArray<CollectionMatch<TMatchNode>> preMatches,
        ImmutableArray<CollectionMatch<TMatchNode>> postMatches,
        Func<TParentExpression, InitializerExpressionSyntax?> getInitializer,
        Func<TParentExpression, InitializerExpressionSyntax, TParentExpression> withInitializer,
        CancellationToken cancellationToken)
        where TParentExpression : ExpressionSyntax
        where TMatchNode : SyntaxNode
    {
        // This method is quite complex, but primarily because it wants to perform all the trivia handling itself.
        // We are moving nodes around in the tree in complex ways, and the formatting engine is just not sufficient
        // for performing this task.
 
        var document = await ParsedDocument.CreateAsync(workspaceDocument, cancellationToken).ConfigureAwait(false);
 
        var formattingOptions = await workspaceDocument.GetCSharpSyntaxFormattingOptionsAsync(cancellationToken).ConfigureAwait(false);
        var indentationOptions = new IndentationOptions(formattingOptions);
 
        var wrappingLength = formattingOptions.CollectionExpressionWrappingLength;
 
        var initializer = getInitializer(expressionToReplace);
        var endOfLine = DetermineEndOfLine(document, expressionToReplace, formattingOptions);
 
        // Determine if we want to end up with a multiline collection expression.  The general intuition is that we
        // want a multiline expression if any of the following are true:
        //
        //  1. any of the elements we're going to add are multi-line themselves.
        //  2. any of the elements we're going to add will have comments on them.  These will need to be multiline
        //     so that the comments do not end up wrongly consuming other elements that come after them.
        //  3. the resultant collection expression would be very long.
        var makeMultiLineCollectionExpression = MakeMultiLineCollectionExpression();
 
        // If we have an initializer already with elements in it (e.g. `new List<int> { 1, 2, 3 }`) then we want to
        // preserve as much as we can from the `{ 1, 2, 3 }` portion when converting to a collection expression and
        // we want to match the style there as much as is reasonably possible.  Note that this initializer itself
        // may be multiline, and we want to match that style closely.
        //
        // If there is no existing initializer (e.g. `new List<int>();`), or the initializer has no items in it, we
        // will instead try to figure out the best form for the final collection expression based on the elements
        // we're going to add.
        return initializer == null || initializer.Expressions.Count == 0
            ? CreateCollectionExpressionWithoutExistingElements()
            : CreateCollectionExpressionWithExistingElements();
 
        CollectionExpressionSyntax CreateCollectionExpressionWithoutExistingElements()
        {
            // Didn't have an existing initializer (or it was empty).  For both cases, just create an entirely
            // fresh collection expression, and replace the object entirely.
 
            if (preMatches is [{ Node: ExpressionSyntax } preMatch] && postMatches.IsEmpty)
            {
                return CreateSingleElementCollection(preMatch);
            }
            else if (preMatches.IsEmpty && postMatches is [{ Node: ExpressionSyntax } postMatch])
            {
                return CreateSingleElementCollection(postMatch);
            }
            else if (makeMultiLineCollectionExpression)
            {
                // Slightly difficult case.  We're replacing `new List<int>();` with a fresh, multi-line collection
                // expression.  To figure out what to do here, we need to figure out where the braces *and* elements
                // will need to go.  To figure this out, first replace `new List<int>()` with `new List<int>() { null }`
                // then see where the indenter would place the `{` and `null` if they were on new lines.
                var openBraceTokenAnnotation = new SyntaxAnnotation();
                var nullTokenAnnotation = new SyntaxAnnotation();
                var initializer = InitializerExpression(
                    SyntaxKind.CollectionInitializerExpression,
                    OpenBraceToken.WithAdditionalAnnotations(openBraceTokenAnnotation),
                    [LiteralExpression(SyntaxKind.NullLiteralExpression, NullKeyword.WithAdditionalAnnotations(nullTokenAnnotation))],
                    CloseBraceToken);
 
                // Update the doc with the new object (now with initializer).
                var updatedRoot = document.Root.ReplaceNode(
                    expressionToReplace,
                    withInitializer(expressionToReplace, initializer));
                var updatedParsedDocument = document.WithChangedRoot(updatedRoot, cancellationToken);
 
                // Find the '{' and 'null' tokens after the rewrite.
                var openBraceToken = updatedRoot.GetAnnotatedTokens(openBraceTokenAnnotation).Single();
                var nullToken = updatedRoot.GetAnnotatedTokens(nullTokenAnnotation).Single();
                initializer = (InitializerExpressionSyntax)openBraceToken.GetRequiredParent();
 
                // Figure out where those tokens would prefer to be placed if they were on their own line.
                var openBraceIndentation = openBraceToken.GetPreferredIndentation(updatedParsedDocument, indentationOptions, cancellationToken);
                var elementIndentation = nullToken.GetPreferredIndentation(updatedParsedDocument, indentationOptions, cancellationToken);
 
                // now create the elements, following that indentation preference.
                using var _ = ArrayBuilder<SyntaxNodeOrToken>.GetInstance(out var nodesAndTokens);
                CreateAndAddElements(preMatches, nodesAndTokens, preferredIndentation: elementIndentation, forceTrailingComma: true, moreToCome: true);
                CreateAndAddElements(postMatches, nodesAndTokens, preferredIndentation: elementIndentation, forceTrailingComma: true, moreToCome: false);
 
                // Add a newline between the last element and the close bracket if we don't already have one.
                if (nodesAndTokens.Count > 0 && nodesAndTokens.Last().GetTrailingTrivia() is [.., (kind: not SyntaxKind.EndOfLineTrivia)])
                    nodesAndTokens[^1] = nodesAndTokens[^1].WithAppendedTrailingTrivia(endOfLine);
 
                // Make the collection expression with the braces on new lines, at the desired brace indentation.
                var finalCollection = CollectionExpression(
                    OpenBracketToken.WithLeadingTrivia(endOfLine, Whitespace(openBraceIndentation)).WithTrailingTrivia(endOfLine),
                    SeparatedList<CollectionElementSyntax>(nodesAndTokens),
                    CloseBracketToken.WithLeadingTrivia(Whitespace(openBraceIndentation)));
 
                // Now, figure out what trivia to move over from the original object over to the new collection.
                return UseCollectionExpressionHelpers.ReplaceWithCollectionExpression(
                    updatedParsedDocument.Text, initializer, finalCollection, newCollectionIsSingleLine: false);
            }
            else
            {
                // All the elements would work on a single line.  This is a trivial case.  We can just make the
                // fresh collection expression, and do a wholesale replacement of the original object creation
                // expression with it.
                using var _ = ArrayBuilder<SyntaxNodeOrToken>.GetInstance(out var nodesAndTokens);
                CreateAndAddElements(preMatches, nodesAndTokens, preferredIndentation: null, forceTrailingComma: false, moreToCome: true);
                CreateAndAddElements(postMatches, nodesAndTokens, preferredIndentation: null, forceTrailingComma: false, moreToCome: false);
 
                // Remove any trailing whitespace from the last element/comma and the final close bracket.
                if (nodesAndTokens.Count > 0)
                    nodesAndTokens[^1] = RemoveTrailingWhitespace(nodesAndTokens[^1]);
 
                var collectionExpression = CollectionExpression(
                    OpenBracketToken.WithoutTrivia(),
                    SeparatedList<CollectionElementSyntax>(nodesAndTokens),
                    CloseBracketToken.WithoutTrivia());
 
                // Even though the collection expression itself fits on a single line, there could be
                // additional trivia between the array creation expression and the initializer list.
                // We should include this additional trivia in the final collection expression.
                //
                // int[][] = new int[]
                // {
                //     new int[] // some identifying comment
                //     { 1, 2, 3 }
                // }
                //
                //  ...
                //
                // int[][] =
                // [
                //    // some identifying comment
                //    [1, 2, 3]
                // ]
                var shouldIncludeAdditionalLeadingTrivia = initializer is not null &&
                    initializer.OpenBraceToken.GetPreviousToken().TrailingTrivia.Any(static x => x.IsSingleOrMultiLineComment());
 
                if (shouldIncludeAdditionalLeadingTrivia)
                {
                    var additionalLeadingTrivia = initializer!.OpenBraceToken.GetPreviousToken().TrailingTrivia
                        .SkipInitialWhitespace()
                        .Concat(initializer.OpenBraceToken.LeadingTrivia);
                    return collectionExpression.WithLeadingTrivia(additionalLeadingTrivia);
                }
                else
                {
                    // otherwise, we want to unconditionally preserve any and all trivia in the original expression
                    return collectionExpression.WithTriviaFrom(expressionToReplace);
                }
            }
        }
 
        CollectionExpressionSyntax CreateSingleElementCollection(CollectionMatch<TMatchNode> match)
        {
            // Specialize when we're taking some expression (like x.y.ToArray()) and converting to a spreaded
            // collection expression.  We just want to trivially make that `[.. x.y]` without any specialized
            // behavior.  In particular, we do not want to generate something like:
            //
            //  [
            //      .. x.y,
            //  ]
            //
            // For that sort of case.  Single element collections should stay closely associated with the original
            // expression.
            var expression = (ExpressionSyntax)(object)match.Node;
            return CollectionExpression([
                match.UseSpread
                    ? SpreadElement(expression.WithoutTrivia())
                    : ExpressionElement(expression.WithoutTrivia())]).WithTriviaFrom(expressionToReplace);
        }
 
        CollectionExpressionSyntax CreateCollectionExpressionWithExistingElements()
        {
            // If the object creation expression had an initializer (with at least one element in it).  Attempt to
            // preserve the formatting of the original initializer and the new collection expression.
 
            if (!document.Text.AreOnSameLine(initializer.GetFirstToken(), initializer.GetLastToken()))
            {
                // initializer itself was on multiple lines.  We'll want to create a collection expression whose
                // braces (and initial elements) match whatever the initializer correct looks like.
 
                var initialCollection = UseCollectionExpressionHelpers.ConvertInitializerToCollectionExpression(
                    initializer, wasOnSingleLine: false);
 
                if (!makeMultiLineCollectionExpression &&
                    document.Text.AreOnSameLine(initializer.Expressions.First().GetFirstToken(), initializer.Expressions.Last().GetLastToken()))
                {
                    // New elements were all single line, and existing elements were on a single line.  e.g.
                    //
                    //  {
                    //      1, 2, 3
                    //  }
                    //
                    // Just add the new elements to this.
                    var finalCollection = AddMatchesToExistingNonEmptyCollectionExpression(
                        initialCollection, preferredIndentation: null);
 
                    return UseCollectionExpressionHelpers.ReplaceWithCollectionExpression(
                        document.Text, initializer, finalCollection, newCollectionIsSingleLine: false);
                }
                else
                {
                    // We want the new items to be multiline *or* existing items were on different lines already.
                    // Figure out what the preferred indentation is, and prepend each new item with it.
                    var preferredIndentation = initializer.Expressions.First().GetFirstToken().GetPreferredIndentation(
                        document, indentationOptions, cancellationToken);
 
                    var finalCollection = AddMatchesToExistingNonEmptyCollectionExpression(
                        initialCollection, preferredIndentation);
 
                    return UseCollectionExpressionHelpers.ReplaceWithCollectionExpression(
                        document.Text, initializer, finalCollection, newCollectionIsSingleLine: false);
                }
            }
            else if (makeMultiLineCollectionExpression)
            {
                // The existing initializer is on a single line.  Like: `new List<int>() { 1, 2, 3 }` But we're
                // adding elements that want to be multi-line.  So wrap the braces, and add the new items to the
                // end.
 
                var initialCollection = UseCollectionExpressionHelpers.ConvertInitializerToCollectionExpression(
                    initializer, wasOnSingleLine: false);
 
                if (document.Text.AreOnSameLine(initializer.OpenBraceToken.GetPreviousToken(), initializer.OpenBraceToken))
                {
                    // Determine where both the braces and the items would like to be wrapped to.
                    var preferredBraceIndentation = initializer.OpenBraceToken.GetPreferredIndentation(document, indentationOptions, cancellationToken);
                    var preferredItemIndentation = initializer.Expressions.First().GetFirstToken().GetPreferredIndentation(document, indentationOptions, cancellationToken);
 
                    // Update both the braces and initial elements to the right location.
                    initialCollection = initialCollection.Update(
                        RemoveTrailingWhitespace(initialCollection.OpenBracketToken.WithLeadingTrivia(endOfLine, Whitespace(preferredBraceIndentation))),
                        FixLeadingAndTrailingWhitespace(initialCollection.Elements, preferredItemIndentation),
                        initialCollection.CloseBracketToken.WithLeadingTrivia(endOfLine, Whitespace(preferredBraceIndentation)));
 
                    // Then add all new elements at the right indentation level.
                    var finalCollection = AddMatchesToExistingNonEmptyCollectionExpression(initialCollection, preferredItemIndentation);
 
                    return UseCollectionExpressionHelpers.ReplaceWithCollectionExpression(
                        document.Text, initializer, finalCollection, newCollectionIsSingleLine: false);
                }
                else
                {
                    // ')' and '{' are not on the same line.  So the code looks like this:
                    //
                    //  new List<int>()
                    //  { 1, 2, 3 }
 
                    // Here, the brace is already in the right location.  So all we need to do is determine the preferred indentation for the items.
                    var braceIndentation = GetIndentationStringForToken(initializer.OpenBraceToken);
                    var preferredItemIndentation = initializer.Expressions.First().GetFirstToken().GetPreferredIndentation(document, indentationOptions, cancellationToken);
 
                    initialCollection = initialCollection.Update(
                        RemoveTrailingWhitespace(initialCollection.OpenBracketToken),
                        FixLeadingAndTrailingWhitespace(initialCollection.Elements, preferredItemIndentation),
                        initialCollection.CloseBracketToken.WithLeadingTrivia(endOfLine, Whitespace(braceIndentation)));
 
                    var finalCollection = AddMatchesToExistingNonEmptyCollectionExpression(initialCollection, preferredItemIndentation);
 
                    return UseCollectionExpressionHelpers.ReplaceWithCollectionExpression(
                        document.Text, initializer, finalCollection, newCollectionIsSingleLine: false);
                }
            }
            else
            {
                // Both the initializer and the new elements all would work on a single line.
 
                // First, convert the existing initializer (and its expressions) into a corresponding collection
                // expression.  This will fixup the braces properly for the collection expression.
                var initialCollection = UseCollectionExpressionHelpers.ConvertInitializerToCollectionExpression(
                    initializer, wasOnSingleLine: true);
 
                // now, add all the matches in after the existing elements.
                var finalCollection = AddMatchesToExistingNonEmptyCollectionExpression(initialCollection, preferredIndentation: null);
 
                // Now do the actual replacement.  This will ensure the location of the collection expression
                // properly corresponds to the equivalent pieces of the collection initializer.
                return UseCollectionExpressionHelpers.ReplaceWithCollectionExpression(
                    document.Text, initializer, finalCollection, newCollectionIsSingleLine: true);
            }
        }
 
        SeparatedSyntaxList<CollectionElementSyntax> FixLeadingAndTrailingWhitespace(
            SeparatedSyntaxList<CollectionElementSyntax> elements,
            string preferredItemIndentation)
        {
            var elementsWithSeparators = elements.GetWithSeparators();
 
            var first = elementsWithSeparators.First();
            elementsWithSeparators = elementsWithSeparators.Replace(first, first.WithLeadingTrivia(endOfLine, Whitespace(preferredItemIndentation)));
            var last = elementsWithSeparators.Last();
            elementsWithSeparators = elementsWithSeparators.Replace(last, RemoveTrailingWhitespace(last));
 
            return SeparatedList<CollectionElementSyntax>(elementsWithSeparators);
        }
 
        // Helper which produces the CollectionElementSyntax nodes and adds to the separated syntax list builder array.
        // Used to we can uniformly add the items correctly with the requested (but optional) indentation.  And so that
        // commas are added properly to the sequence.
        void CreateAndAddElements(
            ImmutableArray<CollectionMatch<TMatchNode>> matches,
            ArrayBuilder<SyntaxNodeOrToken> nodesAndTokens,
            string? preferredIndentation,
            bool forceTrailingComma,
            bool moreToCome)
        {
            // If there's no requested indentation, then we want to produce the sequence as: `a, b, c, d`.  So just
            // a space after any comma.  If there is desired indentation for an element, then we always follow a comma
            // with a newline so that the element node comes on the next line indented properly.
            var triviaAfterComma = preferredIndentation is null
                ? TriviaList(Space)
                : TriviaList(endOfLine);
 
            foreach (var element in matches.SelectMany(m => CreateElements(m, preferredIndentation)))
            {
                AddCommaIfMissing(last: false);
                nodesAndTokens.Add(element);
            }
 
            if (matches.Length > 0 && forceTrailingComma)
                AddCommaIfMissing(last: !moreToCome);
 
            return;
 
            void AddCommaIfMissing(bool last)
            {
                // Add a comment before each new element we're adding.  Move any trailing whitespace/comment trivia
                // from the prior node to come after that comma.  e.g. if the prior node was `x // comment` then we
                // end up with: `x, // comment<new-line>`
                if (nodesAndTokens is [.., { IsNode: true } lastNode])
                {
                    var trailingWhitespaceAndComments = lastNode.GetTrailingTrivia().Where(static t => t.IsWhitespaceOrSingleOrMultiLineComment());
 
                    nodesAndTokens[^1] = lastNode.WithTrailingTrivia(lastNode.GetTrailingTrivia().Where(t => !trailingWhitespaceAndComments.Contains(t)));
 
                    var commaToken = CommaToken
                        .WithoutLeadingTrivia()
                        .WithTrailingTrivia(TriviaList(trailingWhitespaceAndComments).AddRange(triviaAfterComma));
 
                    // Strip trailing whitespace after the last comma.
                    if (last)
                        commaToken = RemoveTrailingWhitespace(commaToken);
 
                    nodesAndTokens.Add(commaToken);
                }
            }
        }
 
        // Helper which takes a collection expression that already has at least one element in it and adds the new
        // elements to it.
        CollectionExpressionSyntax AddMatchesToExistingNonEmptyCollectionExpression(
            CollectionExpressionSyntax initialCollectionExpression,
            string? preferredIndentation)
        {
            using var _ = ArrayBuilder<SyntaxNodeOrToken>.GetInstance(out var nodesAndTokens);
 
            // Add any pre-items before the initializer items.
            CreateAndAddElements(preMatches, nodesAndTokens, preferredIndentation, forceTrailingComma: true, moreToCome: true);
 
            // Now add all the initializer items.
            nodesAndTokens.AddRange(initialCollectionExpression.Elements.GetWithSeparators());
 
            // If there is already a trailing comma before, remove it.  We'll add it back at the end. If there is no
            // trailing comma, then grab the trailing trivia off of the last element. We'll move it to the final
            // last element once we've added everything.
            var trailingComma = default(SyntaxToken);
            var trailingTrivia = default(SyntaxTriviaList);
            if (nodesAndTokens[^1].IsToken)
            {
                trailingComma = nodesAndTokens[^1].AsToken();
                nodesAndTokens.RemoveLast();
            }
            else
            {
                trailingTrivia = nodesAndTokens[^1].GetTrailingTrivia();
                nodesAndTokens[^1] = nodesAndTokens[^1].WithTrailingTrivia();
            }
 
            // Now add all the post matches in.
 
            // If we're wrapping to multiple lines, and we don't already have a trailing comma, then force one at the
            // end.  This keeps every element consistent with ending the line with a comma, which makes code easier to
            // maintain.
            CreateAndAddElements(
                postMatches, nodesAndTokens, preferredIndentation,
                forceTrailingComma: preferredIndentation != null && trailingComma == default,
                moreToCome: false);
 
            if (trailingComma != default)
            {
                // If we ended with a comma before, continue ending with a comma.
                nodesAndTokens.Add(trailingComma);
            }
            else
            {
                // Otherwise, move the trailing trivia from before to the end.
                nodesAndTokens[^1] = nodesAndTokens[^1].WithTrailingTrivia(trailingTrivia);
            }
 
            var finalCollection = initialCollectionExpression.WithElements(
                SeparatedList<CollectionElementSyntax>(nodesAndTokens));
            return finalCollection;
        }
 
        static CollectionElementSyntax CreateCollectionElement(
            bool useSpread, ExpressionSyntax expression)
        {
            return useSpread
                ? SpreadElement(
                    DotDotToken.WithLeadingTrivia(expression.GetLeadingTrivia()).WithTrailingTrivia(Space),
                    expression.WithoutLeadingTrivia())
                : ExpressionElement(expression);
        }
 
        IEnumerable<CollectionElementSyntax> CreateElements(
            CollectionMatch<TMatchNode> match, string? preferredIndentation)
        {
            var node = match.Node;
 
            if (node is ExpressionStatementSyntax expressionStatement)
            {
                // Create:
                //
                //      `x` for `collection.Add(x)`
                //      `.. x` for `collection.AddRange(x)`
                //      `x, y, z` for `collection.AddRange(x, y, z)`
                var expressions = ConvertExpressions(expressionStatement.Expression, expr => IndentExpression(expressionStatement, expr, preferredIndentation));
 
                Contract.ThrowIfTrue(expressions.Length >= 2 && match.UseSpread);
 
                if (match.UseSpread && expressions is [CollectionExpressionSyntax collectionExpression])
                {
                    // If we're spreading a collection expression, just insert those inner collection expression
                    // elements as is into the outer collection expression.
                    foreach (var element in collectionExpression.Elements)
                    {
                        if (element is SpreadElementSyntax spreadElement)
                        {
                            yield return CreateCollectionElement(useSpread: true, spreadElement.Expression);
                        }
                        else if (element is ExpressionElementSyntax expressionElement)
                        {
                            yield return CreateCollectionElement(useSpread: false, expressionElement.Expression);
                        }
                    }
                }
                else
                {
                    foreach (var expression in expressions)
                        yield return CreateCollectionElement(match.UseSpread, expression);
                }
            }
            else if (node is ForEachStatementSyntax foreachStatement)
            {
                // Create: `.. x` for `foreach (var v in x) collection.Add(v)`
                yield return CreateCollectionElement(
                    match.UseSpread,
                    IndentExpression(foreachStatement, foreachStatement.Expression, preferredIndentation));
            }
            else if (node is IfStatementSyntax ifStatement)
            {
                var condition = IndentExpression(ifStatement, ifStatement.Condition, preferredIndentation).Parenthesize(includeElasticTrivia: false);
                var trueStatement = (ExpressionStatementSyntax)UnwrapEmbeddedStatement(ifStatement.Statement);
 
                if (ifStatement.Else is null)
                {
                    // Create: `x ? [y] : []` for `if (x) collection.Add(y)`
                    var expression = ConditionalExpression(
                        condition,
                        CollectionExpression([
                            ExpressionElement(ConvertExpression(trueStatement.Expression, indent: null))]),
                        CollectionExpression());
                    yield return CreateCollectionElement(match.UseSpread, expression);
                }
                else
                {
                    // Create: `x ? y : z` for `if (x) collection.Add(y) else collection.Add(z)`
                    var falseStatement = (ExpressionStatementSyntax)UnwrapEmbeddedStatement(ifStatement.Else.Statement);
                    var expression = ConditionalExpression(
                        condition,
                        ConvertExpression(trueStatement.Expression, indent: null).Parenthesize(includeElasticTrivia: false),
                        ConvertExpression(falseStatement.Expression, indent: null).Parenthesize(includeElasticTrivia: false));
                    yield return CreateCollectionElement(match.UseSpread, expression);
                }
            }
            else if (node is ExpressionSyntax expression)
            {
                yield return CreateCollectionElement(match.UseSpread, IndentExpression(parentStatement: null, expression, preferredIndentation));
            }
            else
            {
                throw ExceptionUtilities.Unreachable();
            }
        }
 
        ExpressionSyntax IndentExpression(
            StatementSyntax? parentStatement,
            ExpressionSyntax expression,
            string? preferredIndentation)
        {
            // This must be called from an expression from the original tree.  Not something we're already transforming.
            // Otherwise, we'll have no idea how to apply the preferredIndentation if present.
            Contract.ThrowIfNull(expression.Parent);
            if (preferredIndentation is null)
                return expression.WithoutLeadingTrivia();
 
            var startLine = document.Text.Lines.GetLineFromPosition(GetAnchorNode(expression).SpanStart);
            var firstTokenOnLineIndentationString = GetIndentationStringForToken(document.Root.FindToken(startLine.Start));
 
            var expressionFirstToken = expression.GetFirstToken();
            var updatedExpression = expression.ReplaceTokens(
                expression.DescendantTokens(),
                (currentToken, _) =>
                {
                    // Ensure the first token has the indentation we're moving the entire node to
                    if (currentToken == expressionFirstToken)
                        return currentToken.WithLeadingTrivia(Whitespace(preferredIndentation));
 
                    return IndentToken(currentToken, preferredIndentation, firstTokenOnLineIndentationString);
                });
 
            // Now, once we've indented the expression, attempt to move comments on its containing statement to it.
            return TransferParentStatementComments(parentStatement, updatedExpression, preferredIndentation);
 
            SyntaxNode GetAnchorNode(SyntaxNode node)
            {
                // we're starting with something either like:
                //
                //      collection.Add(some_expr +
                //          cont);
                //
                // or
                //
                //      collection.Add(
                //          some_expr +
                //              cont);
                //
                // In the first, we want to consider the `some_expr + cont` to actually start where `collection` starts so
                // that we can accurately determine where the preferred indentation should move all of it.
 
                // If the expression is parented by a statement or member-decl (like a field/prop), use that container
                // to determine the indentation point. Otherwise, default to the indentation of the line the expression
                // is on.
                var firstToken = node.GetFirstToken();
                if (document.Text.AreOnSameLine(firstToken.GetPreviousToken(), firstToken))
                {
                    for (var current = node; current != null; current = current.Parent)
                    {
                        if (current is StatementSyntax or MemberDeclarationSyntax)
                            return current;
                    }
                }
 
                return node;
            }
        }
 
        SyntaxToken IndentToken(
            SyntaxToken token,
            string preferredIndentation,
            string firstTokenOnLineIndentationString)
        {
            // If a token has any leading whitespace, it must be at the start of a line.  Whitespace is
            // otherwise always consumed as trailing trivia if it comes after a token.
            if (token.LeadingTrivia is not [.., (kind: SyntaxKind.WhitespaceTrivia)])
                return token;
 
            using var _ = ArrayBuilder<SyntaxTrivia>.GetInstance(out var result);
 
            // Walk all trivia (except the final whitespace).  If we hit any comments within at the start of a line
            // indent them as well.
            for (int i = 0, n = token.LeadingTrivia.Count - 1; i < n; i++)
            {
                var currentTrivia = token.LeadingTrivia[i];
                var nextTrivia = token.LeadingTrivia[i + 1];
 
                var afterNewLine = i == 0 || token.LeadingTrivia[i - 1].IsEndOfLine();
                if (afterNewLine &&
                    currentTrivia.IsWhitespace() &&
                    nextTrivia.IsSingleOrMultiLineComment())
                {
                    result.Add(GetIndentedWhitespaceTrivia(
                        preferredIndentation, firstTokenOnLineIndentationString, nextTrivia.SpanStart));
                }
                else
                {
                    result.Add(currentTrivia);
                }
            }
 
            // Finally, figure out how much this token is indented *from the line* the first token was on.
            // Then adjust the preferred indentation that amount for this token.
            result.Add(GetIndentedWhitespaceTrivia(
                preferredIndentation, firstTokenOnLineIndentationString, token.SpanStart));
 
            return token.WithLeadingTrivia(TriviaList(result));
        }
 
        SyntaxTrivia GetIndentedWhitespaceTrivia(string preferredIndentation, string firstTokenOnLineIndentationString, int pos)
        {
            var positionIndentation = GetIndentationStringForPosition(pos);
            return Whitespace(positionIndentation.StartsWith(firstTokenOnLineIndentationString)
                ? preferredIndentation + positionIndentation[firstTokenOnLineIndentationString.Length..]
                : preferredIndentation);
        }
 
        static ExpressionSyntax TransferParentStatementComments(
            StatementSyntax? parentStatement,
            ExpressionSyntax expression,
            string preferredIndentation)
        {
            if (parentStatement is null)
                return expression;
 
            using var _1 = ArrayBuilder<SyntaxTrivia>.GetInstance(out var newLeadingTrivia);
            using var _2 = ArrayBuilder<SyntaxTrivia>.GetInstance(out var newTrailingTrivia);
 
            // If the statement has any leading comments, then move the range of leading trivia it has over (from
            // the first leading comment to the last).
            var leadingTrivia = parentStatement.GetLeadingTrivia();
            var firstLeadingComment = leadingTrivia.FirstOrDefault(t => t.IsSingleOrMultiLineComment());
            var lastLeadingComment = leadingTrivia.LastOrDefault(t => t.IsSingleOrMultiLineComment());
            if (firstLeadingComment != default)
            {
                var firstLeadingCommentIndex = leadingTrivia.IndexOf(firstLeadingComment);
                var lastLeadingCommentIndex = leadingTrivia.IndexOf(lastLeadingComment);
 
                var afterNewLine = true;
                for (var i = firstLeadingCommentIndex; i <= lastLeadingCommentIndex; i++)
                {
                    var currentTrivia = leadingTrivia[i];
                    if (currentTrivia.IsSingleOrMultiLineComment() && afterNewLine)
                    {
                        if (newLeadingTrivia.LastOrDefault().IsWhitespace())
                            newLeadingTrivia.RemoveLast();
 
                        newLeadingTrivia.Add(Whitespace(preferredIndentation));
                        afterNewLine = false;
                    }
 
                    newLeadingTrivia.Add(currentTrivia);
                    if (currentTrivia.IsEndOfLine())
                        afterNewLine = true;
                }
 
                // Attempt to preserve the last newline after the last comment copied.
                if (lastLeadingCommentIndex + 1 < leadingTrivia.Count &&
                    leadingTrivia[lastLeadingCommentIndex + 1].IsEndOfLine())
                {
                    newLeadingTrivia.Add(leadingTrivia[lastLeadingCommentIndex + 1]);
                }
            }
 
            // if there are trailing comments, move the trailing whitespace and comments over.
            if (parentStatement.GetTrailingTrivia().Any(static t => t.IsSingleOrMultiLineComment()))
            {
                foreach (var trivia in parentStatement.GetTrailingTrivia())
                {
                    if (trivia.IsWhitespaceOrSingleOrMultiLineComment())
                        newTrailingTrivia.Add(trivia);
                }
            }
 
            expression = expression
                .WithPrependedLeadingTrivia(newLeadingTrivia)
                .WithAppendedTrailingTrivia(newTrailingTrivia);
 
            return expression;
        }
 
        string GetIndentationStringForToken(SyntaxToken token)
            => GetIndentationStringForPosition(token.SpanStart);
 
        string GetIndentationStringForPosition(int position)
        {
            var lineContainingPosition = document.Text.Lines.GetLineFromPosition(position);
            var lineText = lineContainingPosition.ToString();
            var indentation = lineText.ConvertTabToSpace(formattingOptions.TabSize, initialColumn: 0, endPosition: position - lineContainingPosition.Start);
            return indentation.CreateIndentationString(formattingOptions.UseTabs, formattingOptions.TabSize);
        }
 
        bool MakeMultiLineCollectionExpression()
        {
            // If there's already an initializer, and we're not adding anything to it, then just keep the initializer
            // as-is.  No need to convert it to be multi-line if it's currently single-line.
            if (initializer != null && preMatches.Length == 0 && postMatches.Length == 0)
                return false;
 
            var totalLength = 0;
            if (initializer != null)
            {
                foreach (var expression in initializer.Expressions)
                    totalLength += expression.Span.Length;
            }
 
            if (CheckForMultiLine(preMatches) ||
                CheckForMultiLine(postMatches))
            {
                return true;
            }
 
            return totalLength > wrappingLength;
 
            bool CheckForMultiLine(ImmutableArray<CollectionMatch<TMatchNode>> matches)
            {
                foreach (var (node, _) in matches)
                {
                    // if the statement we're replacing has any comments on it, then we need to be multiline to give them an
                    // appropriate place to go.
                    if (node.GetLeadingTrivia().Any(static t => t.IsSingleOrMultiLineComment()) ||
                        node.GetTrailingTrivia().Any(static t => t.IsSingleOrMultiLineComment()))
                    {
                        return true;
                    }
 
                    foreach (var component in GetElementComponents(node))
                    {
                        // if any of the expressions we're adding are multiline, then make things multiline.
                        if (!document.Text.AreOnSameLine(component.GetFirstToken(), component.GetLastToken()))
                            return true;
 
                        totalLength += component.Span.Length;
                    }
                }
 
                return false;
            }
        }
 
        static IEnumerable<SyntaxNode> GetElementComponents(TMatchNode node)
        {
            if (node is ExpressionStatementSyntax expressionStatement)
            {
                yield return expressionStatement.Expression;
            }
            else if (node is ForEachStatementSyntax foreachStatement)
            {
                yield return foreachStatement.Expression;
            }
            else if (node is IfStatementSyntax ifStatement)
            {
                yield return ifStatement.Condition;
                yield return UnwrapEmbeddedStatement(ifStatement.Statement);
                if (ifStatement.Else != null)
                    yield return UnwrapEmbeddedStatement(ifStatement.Else.Statement);
            }
            else if (node is ExpressionSyntax expression)
            {
                yield return expression;
            }
        }
 
        static StatementSyntax UnwrapEmbeddedStatement(StatementSyntax statement)
            => statement is BlockSyntax { Statements: [var innerStatement] } ? innerStatement : statement;
 
        static ExpressionSyntax ConvertExpression(
            ExpressionSyntax expression, Func<ExpressionSyntax, ExpressionSyntax>? indent)
        {
            var expressions = ConvertExpressions(expression, indent);
            return expressions.Single();
        }
 
        static ImmutableArray<ExpressionSyntax> ConvertExpressions(
            ExpressionSyntax expression, Func<ExpressionSyntax, ExpressionSyntax>? indent)
        {
            indent ??= static e => e;
 
            // This must be called from an expression from the original tree.  Not something we're already transforming.
            // Otherwise, we'll have no idea how to apply the preferredIndentation if present.
            Contract.ThrowIfNull(expression.Parent);
            return expression switch
            {
                InvocationExpressionSyntax invocation => ConvertInvocation(invocation, indent),
                AssignmentExpressionSyntax assignment => ConvertAssignment(assignment, indent),
                _ => throw new InvalidOperationException(),
            };
        }
 
        static ImmutableArray<ExpressionSyntax> ConvertAssignment(
            AssignmentExpressionSyntax assignment, Func<ExpressionSyntax, ExpressionSyntax> indent)
        {
            return [indent(assignment.Right)];
        }
 
        static ImmutableArray<ExpressionSyntax> ConvertInvocation(
            InvocationExpressionSyntax invocation, Func<ExpressionSyntax, ExpressionSyntax> indent)
        {
            var arguments = invocation.ArgumentList.Arguments;
 
            return arguments.SelectAsArray(a => indent(a.Expression));
        }
    }
 
    /// <summary>
    /// Use the same EOL text when producing the collection as the EOL on the line the original expression was on.
    /// </summary>
    private static SyntaxTrivia DetermineEndOfLine<TParentExpression>(
        ParsedDocument document, TParentExpression expressionToReplace, SyntaxFormattingOptions formattingOptions) where TParentExpression : ExpressionSyntax
    {
        var text = document.Text;
        var lineToConsider = text.Lines.GetLineFromPosition(expressionToReplace.SpanStart);
        var lineBreakSpan = TextSpan.FromBounds(lineToConsider.End, lineToConsider.EndIncludingLineBreak);
 
        return lineBreakSpan.IsEmpty
            ? EndOfLine(formattingOptions.NewLine)
            : EndOfLine(text.ToString(lineBreakSpan));
    }
 
    private static SyntaxToken RemoveTrailingWhitespace(SyntaxToken token)
        => RemoveTrailingWhitespace((SyntaxNodeOrToken)token).AsToken();
 
    private static SyntaxNodeOrToken RemoveTrailingWhitespace(SyntaxNodeOrToken nodeOrToken)
    {
        var trivia = nodeOrToken.GetTrailingTrivia();
        var index = trivia.Count;
        while (index - 1 >= 0 && trivia[index - 1].Kind() == SyntaxKind.WhitespaceTrivia)
            index--;
 
        return index == trivia.Count
            ? nodeOrToken
            : nodeOrToken.WithTrailingTrivia(trivia.Take(index));
    }
}