|
// 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 SyntaxGenerator _generator;
private readonly List<Change> _changes = new();
/// <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 => _generator;
/// <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, new[] { 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, new[] { 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
{
internal readonly SyntaxNode OriginalNode;
public Change(SyntaxNode node)
=> 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 : Change
{
public NoChange(SyntaxNode node)
: base(node)
{
}
protected override SyntaxNode Apply(SyntaxNode root, SyntaxNode currentNode, SyntaxGenerator generator)
=> root;
}
private sealed class RemoveChange : Change
{
private readonly SyntaxRemoveOptions _options;
public RemoveChange(SyntaxNode node, SyntaxRemoveOptions options)
: base(node)
{
_options = options;
}
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 : Change
{
private readonly Func<SyntaxNode, SyntaxGenerator, IEnumerable<SyntaxNode>> _modifier;
public ReplaceWithCollectionChange(
SyntaxNode node,
Func<SyntaxNode, SyntaxGenerator, IEnumerable<SyntaxNode>> modifier)
: base(node)
{
_modifier = modifier;
}
protected override SyntaxNode Apply(SyntaxNode root, SyntaxNode currentNode, SyntaxGenerator generator)
=> SyntaxGenerator.ReplaceNode(root, currentNode, _modifier(currentNode, generator));
}
private sealed class ReplaceChange<TArgument> : Change
{
private readonly Func<SyntaxNode, SyntaxGenerator, TArgument, SyntaxNode> _modifier;
private readonly TArgument _argument;
public ReplaceChange(
SyntaxNode node,
Func<SyntaxNode, SyntaxGenerator, TArgument, SyntaxNode> modifier,
TArgument argument)
: base(node)
{
_modifier = modifier;
_argument = argument;
}
protected override SyntaxNode Apply(SyntaxNode root, SyntaxNode currentNode, SyntaxGenerator generator)
=> ValidateNewRoot(generator.ReplaceNode(root, currentNode, _modifier(currentNode, generator, _argument)));
}
private sealed class InsertChange : Change
{
private readonly List<SyntaxNode> _newNodes;
private readonly bool _isBefore;
public InsertChange(SyntaxNode node, IEnumerable<SyntaxNode> newNodes, bool isBefore)
: base(node)
{
_newNodes = newNodes.ToList();
_isBefore = isBefore;
}
protected override SyntaxNode Apply(SyntaxNode root, SyntaxNode currentNode, SyntaxGenerator generator)
=> _isBefore
? generator.InsertNodesBefore(root, currentNode, _newNodes)
: generator.InsertNodesAfter(root, currentNode, _newNodes);
}
}
}
|