|
// 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.Linq;
using Microsoft.CodeAnalysis.Host;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.Editing;
/// <summary>
/// An editor for making changes to a syntax tree. The editor works by giving a list of changes to perform to a
/// particular tree <em>in order</em>. Changes are given a <see cref="SyntaxNode"/> they will apply to in the
/// original tree the editor is created for. The semantics of application are as follows:
///
/// <list type="number">
/// <item>
/// The original root provided is used as the 'current' root for all operations. This 'current' root will
/// continually be updated, becoming the new 'current' root. The original root is never changed.
/// </item>
/// <item>
/// Each change has its given <see cref="SyntaxNode"/> tracked, using a <see cref="SyntaxAnnotation"/>, producing a
/// 'current' root that tracks all of them. This allows that same node to be found after prior changes are applied
/// which mutate the tree.
/// </item>
/// <item>
/// Each change is then applied in order it was added to the editor.
/// </item>
/// <item>
/// A change first attempts to find its <see cref="SyntaxNode"/> in the 'current' root. If that node cannot be
/// found, the operation will fail with an <see cref="ArgumentException"/>.
/// </item>
/// <item>
/// The particular change will run on that node, removing, replacing, or inserting around it according to the
/// change. If the change is passed a delegate as its 'compute' argument, it will be given the <see
/// cref="SyntaxNode"/> found in the current root. The 'current' root will then be updated by replacing the current
/// node with the new computed node.
/// </item>
/// <item>
/// The 'current' root is then returned.
/// </item>
/// </list>
/// </summary>
/// <remarks>
/// The above editing strategy makes it an error for a client of the editor to add a change that updates a parent
/// node and then adds a change that updates a child node (unless the parent change is certain to contain the
/// child), and attempting this will throw at runtime. If a client ever needs to update both a child and a parent,
/// it <em>should</em> add the child change first, and then the parent change. And the parent change should pass an
/// appropriate 'compute' callback so it will see the results of the child change.
/// <para/> If a client wants to make a replacement, then find the <em>value</em> <see cref="SyntaxNode"/> put into
/// the tree, that can be done by adding a dedicated annotation to that node and then looking it back up in the
/// 'current' node passed to a 'compute' callback.
/// </remarks>
public class SyntaxEditor
{
private readonly List<Change> _changes = [];
/// <summary>
/// Creates a new <see cref="SyntaxEditor"/> instance.
/// </summary>
[Obsolete("Use SyntaxEditor(SyntaxNode, HostWorkspaceServices)")]
public SyntaxEditor(SyntaxNode root, Workspace workspace)
: this(root, (workspace ?? throw new ArgumentNullException(nameof(workspace))).Services.SolutionServices)
{
}
/// <summary>
/// Creates a new <see cref="SyntaxEditor"/> instance.
/// </summary>
public SyntaxEditor(SyntaxNode root, HostWorkspaceServices services)
: this(root, (services ?? throw new ArgumentNullException(nameof(services))).SolutionServices)
{
}
/// <summary>
/// Creates a new <see cref="SyntaxEditor"/> instance.
/// </summary>
public SyntaxEditor(SyntaxNode root, SolutionServices services)
: this(root ?? throw new ArgumentNullException(nameof(root)),
SyntaxGenerator.GetGenerator(services ?? throw new ArgumentNullException(nameof(services)), root.Language))
{
}
internal SyntaxEditor(SyntaxNode root, SyntaxGenerator generator)
{
OriginalRoot = root;
Generator = generator;
}
/// <summary>
/// The <see cref="SyntaxNode"/> that was specified when the <see cref="SyntaxEditor"/> was constructed.
/// </summary>
public SyntaxNode OriginalRoot { get; }
/// <summary>
/// A <see cref="SyntaxGenerator"/> to use to create and change <see cref="SyntaxNode"/>'s.
/// </summary>
public SyntaxGenerator Generator { get; }
/// <summary>
/// Returns the changed root node.
/// </summary>
public SyntaxNode GetChangedRoot()
{
var nodes = Enumerable.Distinct(_changes.Where(c => OriginalRoot.Contains(c.OriginalNode))
.Select(c => c.OriginalNode));
var newRoot = OriginalRoot.TrackNodes(nodes);
foreach (var change in _changes)
newRoot = change.Apply(newRoot, Generator);
return newRoot;
}
/// <summary>
/// Makes sure the node is tracked, even if it is not changed.
/// </summary>
public void TrackNode(SyntaxNode node)
{
CheckNodeInOriginalTree(node);
_changes.Add(new NoChange(node));
}
/// <summary>
/// Remove the node from the tree.
/// </summary>
/// <param name="node">The node to remove that currently exists as part of the tree.</param>
public void RemoveNode(SyntaxNode node)
=> RemoveNode(node, SyntaxGenerator.DefaultRemoveOptions);
/// <summary>
/// Remove the node from the tree.
/// </summary>
/// <param name="node">The node to remove that currently exists as part of the tree.</param>
/// <param name="options">Options that affect how node removal works.</param>
public void RemoveNode(SyntaxNode node, SyntaxRemoveOptions options)
{
CheckNodeInOriginalTree(node);
_changes.Add(new RemoveChange(node, options));
}
/// <summary>
/// Replace the specified node with a node produced by the function.
/// </summary>
/// <param name="node">The node to replace that already exists in the tree.</param>
/// <param name="computeReplacement">A function that computes a replacement node.
/// The node passed into the compute function includes changes from prior edits. It will not appear as a descendant of the original root.</param>
public void ReplaceNode(SyntaxNode node, Func<SyntaxNode, SyntaxGenerator, SyntaxNode> computeReplacement)
{
CheckNodeInOriginalTree(node);
if (computeReplacement == null)
throw new ArgumentNullException(nameof(computeReplacement));
_changes.Add(new ReplaceChange(node, computeReplacement));
}
internal void ReplaceNode(SyntaxNode node, Func<SyntaxNode, SyntaxGenerator, IEnumerable<SyntaxNode>> computeReplacement)
{
CheckNodeInOriginalTree(node);
if (computeReplacement == null)
throw new ArgumentNullException(nameof(computeReplacement));
_changes.Add(new ReplaceWithCollectionChange(node, computeReplacement));
}
internal void ReplaceNode<TArgument>(SyntaxNode node, Func<SyntaxNode, SyntaxGenerator, TArgument, SyntaxNode> computeReplacement, TArgument argument)
{
CheckNodeInOriginalTree(node);
if (computeReplacement == null)
throw new ArgumentNullException(nameof(computeReplacement));
_changes.Add(new ReplaceChange<TArgument>(node, computeReplacement, argument));
}
/// <summary>
/// Replace the specified node with a different node.
/// </summary>
/// <param name="node">The node to replace that already exists in the tree.</param>
/// <param name="newNode">The new node that will be placed into the tree in the existing node's location.</param>
public void ReplaceNode(SyntaxNode node, SyntaxNode newNode)
{
CheckNodeInOriginalTree(node);
if (node == newNode)
return;
_changes.Add(new ReplaceChange(node, (n, g) => newNode));
}
/// <summary>
/// Insert the new nodes before the specified node already existing in the tree.
/// </summary>
/// <param name="node">The node already existing in the tree that the new nodes will be placed before. This must be a node this is contained within a syntax list.</param>
/// <param name="newNodes">The nodes to place before the existing node. These nodes must be of a compatible type to be placed in the same list containing the existing node.</param>
public void InsertBefore(SyntaxNode node, IEnumerable<SyntaxNode> newNodes)
{
CheckNodeInOriginalTree(node);
if (newNodes == null)
throw new ArgumentNullException(nameof(newNodes));
_changes.Add(new InsertChange(node, newNodes, isBefore: true));
}
/// <summary>
/// Insert the new node before the specified node already existing in the tree.
/// </summary>
/// <param name="node">The node already existing in the tree that the new nodes will be placed before. This must be a node this is contained within a syntax list.</param>
/// <param name="newNode">The node to place before the existing node. This node must be of a compatible type to be placed in the same list containing the existing node.</param>
public void InsertBefore(SyntaxNode node, SyntaxNode newNode)
=> InsertBefore(node, [newNode]);
/// <summary>
/// Insert the new nodes after the specified node already existing in the tree.
/// </summary>
/// <param name="node">The node already existing in the tree that the new nodes will be placed after. This must be a node this is contained within a syntax list.</param>
/// <param name="newNodes">The nodes to place after the existing node. These nodes must be of a compatible type to be placed in the same list containing the existing node.</param>
public void InsertAfter(SyntaxNode node, IEnumerable<SyntaxNode> newNodes)
{
CheckNodeInOriginalTree(node);
if (newNodes == null)
throw new ArgumentNullException(nameof(newNodes));
_changes.Add(new InsertChange(node, newNodes, isBefore: false));
}
/// <summary>
/// Insert the new node after the specified node already existing in the tree.
/// </summary>
/// <param name="node">The node already existing in the tree that the new nodes will be placed after. This must be a node this is contained within a syntax list.</param>
/// <param name="newNode">The node to place after the existing node. This node must be of a compatible type to be placed in the same list containing the existing node.</param>
public void InsertAfter(SyntaxNode node, SyntaxNode newNode)
=> this.InsertAfter(node, [newNode]);
private void CheckNodeInOriginalTree(SyntaxNode node)
{
if (node == null)
throw new ArgumentNullException(nameof(node));
if (OriginalRoot.Contains(node))
return;
throw new ArgumentException(WorkspacesResources.The_node_is_not_part_of_the_tree, nameof(node));
}
private abstract class Change(SyntaxNode node)
{
internal readonly SyntaxNode OriginalNode = node;
public SyntaxNode Apply(SyntaxNode root, SyntaxGenerator generator)
{
var currentNode = root.GetCurrentNode(OriginalNode);
if (currentNode is null)
Contract.Fail($"GetCurrentNode returned null with the following node: {OriginalNode}");
return Apply(root, currentNode, generator);
}
protected static SyntaxNode ValidateNewRoot(SyntaxNode? root)
=> root ?? throw new InvalidOperationException("Tree root deleted");
protected abstract SyntaxNode Apply(SyntaxNode root, SyntaxNode currentNode, SyntaxGenerator generator);
}
private sealed class NoChange(SyntaxNode node) : Change(node)
{
protected override SyntaxNode Apply(SyntaxNode root, SyntaxNode currentNode, SyntaxGenerator generator)
=> root;
}
private sealed class RemoveChange(SyntaxNode node, SyntaxRemoveOptions options) : Change(node)
{
protected override SyntaxNode Apply(SyntaxNode root, SyntaxNode currentNode, SyntaxGenerator generator)
=> ValidateNewRoot(generator.RemoveNode(root, currentNode, options));
}
private sealed class ReplaceChange : Change
{
private readonly Func<SyntaxNode, SyntaxGenerator, SyntaxNode?> _modifier;
public ReplaceChange(
SyntaxNode node,
Func<SyntaxNode, SyntaxGenerator, SyntaxNode?> modifier)
: base(node)
{
Contract.ThrowIfNull(node);
_modifier = modifier;
}
protected override SyntaxNode Apply(SyntaxNode root, SyntaxNode currentNode, SyntaxGenerator generator)
=> ValidateNewRoot(generator.ReplaceNode(root, currentNode, _modifier(currentNode, generator)));
}
private sealed class ReplaceWithCollectionChange(
SyntaxNode node,
Func<SyntaxNode, SyntaxGenerator, IEnumerable<SyntaxNode>> modifier) : Change(node)
{
protected override SyntaxNode Apply(SyntaxNode root, SyntaxNode currentNode, SyntaxGenerator generator)
=> SyntaxGenerator.ReplaceNode(root, currentNode, modifier(currentNode, generator));
}
private sealed class ReplaceChange<TArgument>(
SyntaxNode node,
Func<SyntaxNode, SyntaxGenerator, TArgument, SyntaxNode> modifier,
TArgument argument) : Change(node)
{
protected override SyntaxNode Apply(SyntaxNode root, SyntaxNode currentNode, SyntaxGenerator generator)
=> ValidateNewRoot(generator.ReplaceNode(root, currentNode, modifier(currentNode, generator, argument)));
}
private sealed class InsertChange(SyntaxNode node, IEnumerable<SyntaxNode> newNodes, bool isBefore) : Change(node)
{
private readonly List<SyntaxNode> _newNodes = newNodes.ToList();
protected override SyntaxNode Apply(SyntaxNode root, SyntaxNode currentNode, SyntaxGenerator generator)
=> isBefore
? generator.InsertNodesBefore(root, currentNode, _newNodes)
: generator.InsertNodesAfter(root, currentNode, _newNodes);
}
}
|