Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Merging Two .CS Files in C# to generate a new Class

Tags:

c#

I want to merge two .cs files to create a third one. Can anyone help me please.

public partial class A 
{
 // some methods
}

suppose this code is written in a file A.cs

public partial class B 
{
 // some methods
}

and this code is written in a file B.cs. I want to generate a new C.cs having all the code of A.cs and B.cs ignoring namespaces.

like image 346
Farrukh Niaz Avatar asked Feb 08 '19 11:02

Farrukh Niaz


1 Answers

I assume you indeed want to merge the partial definitions of the same class. If you really need to merge different classes into a single one, the code can be easily adjusted, but there will be no guarantee that it compiles (because, for example, the classes could have members with the same name).


The problem is quite complicated indeed because of the symbol meaning: it depends on the usings, so one needs to be really careful when merging them.

So the best idea would be not to try to analyse the code semantics manually, but to use a big hammer: Roslyn analyzer.

Let's start.

First of all, you'll need to install Extension Development Workload as it's described here. After this, you'll be able to create a Standalone code analysis tool project.

When you create it, you'll get a lot of useful boilerplate code like this:

class Program
{
    static async Task Main(string[] args)
    {
        // ...
        using (var workspace = MSBuildWorkspace.Create())
        {
            var solutionPath = args[0];
            WriteLine($"Loading solution '{solutionPath}'");

            var solution = await workspace.OpenSolutionAsync(solutionPath,
                    new ConsoleProgressReporter());
            WriteLine($"Finished loading solution '{solutionPath}'");

            // insert your code here
        }
    }

    private static VisualStudioInstance SelectVisualStudioInstance(
        VisualStudioInstance[] visualStudioInstances)
    {
        // ...
    }

    private class ConsoleProgressReporter : IProgress<ProjectLoadProgress>
    {
        // ...
    }
}

Let's fill in what is needed.

Instead of // insert your code here let's put the following code:

var targetClass = args[1];
var modifiedSolution = await MergePartialClasses(targetClass, solution);
workspace.TryApplyChanges(modifiedSolution);

We'll need to implement the logic in MergePartialClasses. The name of the class should be passed as the second command line parameter.

Let's first add the following usings at the top:

using static System.Console;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

Now we can start with the main method. I've put the comments about what's happening directly in the code.

static async Task<Solution> MergePartialClasses(string targetClass, Solution solution)
{
    // https://stackoverflow.com/a/32179708/276994
    // we loop through the projects in the solution and process each of the projects
    foreach (var projectId in solution.ProjectIds)
    {
        var project = solution.GetProject(projectId);
        WriteLine($"Processing project {project.Name}");
        var compilation = await project.GetCompilationAsync();

        // finding the type which we want to merge
        var type = compilation.GetTypeByMetadataName(targetClass);
        if (type == null)
        {
            WriteLine($"Type {targetClass} is not found");
            return solution;
        }

        // look up number of declarations. if it's only 1, we have nothing to merge
        var declarationRefs = type.DeclaringSyntaxReferences;
        if (declarationRefs.Length <= 1)
        {
            WriteLine($"Type {targetClass} has only one location");
            return solution;
        }

        // I didn't implement the case of nested types, which would require to
        // split the outer class, too
        if (type.ContainingType != null)
            throw new NotImplementedException("Splitting nested types");

        // we'll accumulate usings and class members as we traverse all the definitions
        var accumulatedUsings = new List<UsingDirectiveSyntax>();
        var classParts = new List<ClassDeclarationSyntax>();
        foreach (var declarationRef in declarationRefs)
        {
            var declaration = (ClassDeclarationSyntax)await declarationRef.GetSyntaxAsync();
            // get hold of the usings
            var tree = declaration.SyntaxTree;
            var root = await tree.GetRootAsync();
            var usings = root.DescendantNodes().OfType<UsingDirectiveSyntax>();
            accumulatedUsings.AddRange(usings);
            // since we are trying to move the syntax into another file,
            // we need to expand everything in order to remove the dependency
            // on usings
            // in order to do it, we use a custom CSharpSyntaxRewriter (defined later)
            var document = project.GetDocument(tree);
            var expander = new AllSymbolsExpander(document);
            var expandedDeclaration = (ClassDeclarationSyntax)expander.Visit(declaration);
            classParts.Add(expandedDeclaration);
            // remove the old declaration from the place where it is
            // we can't just remove the whole file as it may contain some other classes
            var modifiedRoot =
                root.RemoveNodes(new[] { declaration }, SyntaxRemoveOptions.KeepNoTrivia);
            var modifiedDocument = document.WithSyntaxRoot(modifiedRoot);
            project = modifiedDocument.Project;
        }

        // now, sort the usings and remove the duplicates
        // in order to use DistinctBy, I added MoreLinq nuget package and added
        // using MoreLinq; at the beginning
        // https://stackoverflow.com/a/34063289/276994
        var sortedUsings = accumulatedUsings
                .DistinctBy(x => x.Name.ToString())
                .OrderBy(x => x.StaticKeyword.IsKind(SyntaxKind.StaticKeyword) ?
                                  1 : x.Alias == null ? 0 : 2)
                .ThenBy(x => x.Alias?.ToString())
                .ThenByDescending(x => x.Name.ToString().StartsWith(nameof(System) + "."))
                .ThenBy(x => x.Name.ToString());

        // now, we have to merge the class definitions.
        // split the name into namespace and class name
        var (nsName, className) = SplitName(targetClass);

        // gather all the attributes
        var attributeLists = List(classParts.SelectMany(p => p.AttributeLists));
        // modifiers must be the same, so we are taking them from the
        // first definition, but remove partial if it's there
        var modifiers = classParts[0].Modifiers;
        var partialModifier = modifiers.FirstOrDefault(
                m => m.Kind() == SyntaxKind.PartialKeyword);
        if (partialModifier != null)
            modifiers = modifiers.Remove(partialModifier);
        // gather all the base types
        var baseTypes =
                classParts
                    .SelectMany(p => p.BaseList?.Types ?? Enumerable.Empty<BaseTypeSyntax>())
                    .Distinct()
                    .ToList();
        var baseList = baseTypes.Count > 0 ? BaseList(SeparatedList(baseTypes)) : null;
        // and constraints (I hope that Distinct() works as expected)
        var constraintClauses =
                List(classParts.SelectMany(p => p.ConstraintClauses).Distinct());

        // now, we construct class members by pasting together the accumulated
        // per-part member lists
        var members = List(classParts.SelectMany(p => p.Members));

        // now we can build the class declaration
        var classDef = ClassDeclaration(
            attributeLists: attributeLists,
            modifiers: modifiers,
            identifier: Identifier(className),
            typeParameterList: classParts[0].TypeParameterList,
            baseList: baseList,
            constraintClauses: constraintClauses,
            members: members);

        // if there was a namespace, let's put the class inside it 
        var body = (nsName == null) ?
            (MemberDeclarationSyntax)classDef :
            NamespaceDeclaration(IdentifierName(nsName)).AddMembers(classDef);

        // now create the compilation unit and insert it into the project
        // http://roslynquoter.azurewebsites.net/
        var newTree = CompilationUnit()
                          .WithUsings(List(sortedUsings))
                          .AddMembers(body)
                          .NormalizeWhitespace();
        var newDocument = project.AddDocument(className, newTree);
        var simplifiedNewDocument = await Simplifier.ReduceAsync(newDocument);
        project = simplifiedNewDocument.Project;

        solution = project.Solution;
    }

    // finally, return the modified solution
    return solution;
}

The rest is the AllSymbolsExpander, which just calls Simplifier.ExpandAsync for every node:

class AllSymbolsExpander : CSharpSyntaxRewriter
{
    Document document;
    public AllSymbolsExpander(Document document)
    {
        this.document = document;
    }

    public override SyntaxNode VisitAttribute(AttributeSyntax node) =>
        Expand(node);
    public override SyntaxNode VisitAttributeArgument(AttributeArgumentSyntax node) =>
        Expand(node);
    public override SyntaxNode VisitConstructorInitializer(ConstructorInitializerSyntax node) =>
        Expand(node);
    public override SyntaxNode VisitFieldDeclaration(FieldDeclarationSyntax node) =>
        Expand(node);
    public override SyntaxNode VisitXmlNameAttribute(XmlNameAttributeSyntax node) =>
        Expand(node);
    public override SyntaxNode VisitTypeConstraint(TypeConstraintSyntax node) =>
        Expand(node);

    public override SyntaxNode DefaultVisit(SyntaxNode node)
    {
        if (node is ExpressionSyntax ||
            node is StatementSyntax ||
            node is CrefSyntax ||
            node is BaseTypeSyntax)
            return Expand(node);
        return base.DefaultVisit(node);
    }

    SyntaxNode Expand(SyntaxNode node) =>
        Simplifier.ExpandAsync(node, document).Result; //? async-counterpart?
}

and the trivial function SplitName:

static (string, string) SplitName(string name)
{
    var pos = name.LastIndexOf('.');
    if (pos == -1)
        return (null, name);
    else
        return (name.Substring(0, pos), name.Substring(pos + 1));
}

That's all!

like image 83
Vlad Avatar answered Sep 29 '22 18:09

Vlad