mark
mark

Reputation: 62876

How to remove base type from the source code using Roslyn while preserving the newline at the end of the declaration?

I use the following class to remove the base type:

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Linq;

namespace CSTool.Rewriters
{
    public class BaseTypeNameRemover : CSharpSyntaxRewriter
    {
        private readonly string m_typeName;
        private readonly string m_baseOldTypeName;
        public bool Changed { get; private set; }

        public BaseTypeNameRemover(string typeName, string baseOldTypeName)
        {
            m_typeName = typeName;
            m_baseOldTypeName = baseOldTypeName;
        }

        public override SyntaxNode VisitBaseList(BaseListSyntax node)
        {
            var trailingTrivia = node.GetTrailingTrivia();
            node = (BaseListSyntax)base.VisitBaseList(node);
            if (!node.ChildNodes().Any())
            {
                return null;
            }
            if (trailingTrivia == null || node.HasTrailingTrivia)
            {
                return node;
            }
            return node.WithTrailingTrivia(trailingTrivia);
        }

        public override SyntaxNode VisitSimpleBaseType(SimpleBaseTypeSyntax node)
        {
            if (node.ToString() != m_baseOldTypeName)
            {
                return node;
            }
            
            Changed = true;
            return null;
        }

        public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node) =>
            node.Parent is BaseTypeDeclarationSyntax || node.Identifier.Text != m_typeName
            ? node
            : base.VisitClassDeclaration(node);

        public override SyntaxNode VisitInterfaceDeclaration(InterfaceDeclarationSyntax node) =>
            node.Parent is BaseTypeDeclarationSyntax || node.Identifier.Text != m_typeName
            ? node
            : base.VisitInterfaceDeclaration(node);
    }
}

However, I am unable to preserve the newline after the declaration. Suppose this is the input source code:

namespace xyz
{
    interface IInterface : SomeClass
    {
    }
}

After running it through my BaseTypeNameRemover it becomes:

namespace xyz
{
    interface IInterface    {
    }
}

But I want to preserve the newline!

In general removing the base type is painful, because I have to override both VisitSimpleBaseType and VisitBaseList. And I have no idea how to preserve the newline. In short - messy and inaccurate.

What is the proper way to remove the base type cleanly while preserving the newline?

EDIT 1

And my code is broken when there are more than 1 base type. I truly do not understand how to do it idiomatically with CSharpSyntaxRewriter

Upvotes: 0

Views: 521

Answers (2)

Simon Jones
Simon Jones

Reputation: 26

I came across this as well today, the solution is to modify the identifier of the outer ClassDeclarationSyntax to add the trailing trivia from the base list, then set the base list to null. Here's an extension method that does the job:

    /// <summary>
    /// Removes the specified base type from a Class node.
    /// </summary>
    /// <param name="node">The <see cref="ClassDeclarationSyntax"/> node that will be modified.</param>
    /// <param name="typeName">The name of the type to be removed.</param>
    /// <returns>An updated <see cref="ClassDeclarationSyntax"/> node.</returns>
    public static ClassDeclarationSyntax RemoveBaseType(this ClassDeclarationSyntax node, string typeName)
    {
        var baseType = node.BaseList?.Types.FirstOrDefault(x => string.Equals(x.ToString(), typeName, StringComparison.OrdinalIgnoreCase));
        if (baseType == null)
        {
            // Base type not found
            return node;
        }

        var baseTypes = node.BaseList!.Types.Remove(baseType);
        if (baseTypes.Count == 0)
        {
            // No more base implementations, remove the base list entirely
            // Make sure we update the identifier though to include the baselist trailing trivia (typically '\r\n')
            // so the trailing opening brace gets put onto a new line.
            return node
                .WithBaseList(null)
                .WithIdentifier(node.Identifier.WithTrailingTrivia(node.BaseList.GetTrailingTrivia()));
        }
        else
        {
            // Remove the type but retain all remaining types and trivia
            return node.WithBaseList(node.BaseList!.WithTypes(baseTypes));
        }
    }

Upvotes: 1

mark
mark

Reputation: 62876

ִI had enough trying to figure out how to do it. Finally fell back to brute text manipulation:

if (node.BaseList == null)
{
    continue;
}

var index = node.BaseList.Types.IndexOf(o => o.ToString() == simpleOldBaseTypeName);
if (index < 0)
{
    continue;
}

var text = File.ReadAllText(filePath);
var found = node.BaseList.Types[index];
var start = found.SpanStart;
var end = found.Span.End;
if (node.BaseList.Types.Count == 1 || index > 0)
{
    char stopChar = index == 0 ? ':' : ',';
    while (char.IsWhiteSpace(text[--start]))
    {
    }
    if (text[start] != stopChar)
    {
        throw new ApplicationException($"Failed to parse the base type list in {filePath}");
    }
    while (char.IsWhiteSpace(text[start - 1]))
    {
        --start;
    }
}
else
{
    while (char.IsWhiteSpace(text[end]))
    {
        ++end;
    }
    if (text[end] != ',')
    {
        throw new ApplicationException($"Failed to parse the base type list in {filePath}");
    }
    while (char.IsWhiteSpace(text[++end]))
    {
    }
}

text = text.Remove(start, end - start);
File.WriteAllText(filePath, text);

Not ideal, but good enough for me.

Upvotes: 0

Related Questions