Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Reliably compare type symbols (ITypeSymbol) with Roslyn

I am trying to reliably compare two instances of ITypeSymbol the easiest and most straight forward way possible in the following situation (I came across these issues in a bigger project and tried to simplify it as much as possible):

I have got a CSharpCompilation with this SyntaxTree:

  namespace MyAssembly
  {
    public class Foo
    {
      public Foo(Foo x)
      {
      }
    }
  }

We are walking through the tree with a CSharpSyntaxRewriter, changing the class and updating the Compilation. In the first run we remember the ITypeSymbol of the first constructor parameter (which is the type of the class itself in this case). After updating the compilation we are calling the same rewriter again and obtaining the ITypeSymbol from the constructor parameter a second time. After that, I compare the two ITypeSymbols which I expect to represent the same type MyAssembly.Foo.

My first comparison approach was just calling the ITypeSymbol.Equals() method, but it’s returning false. It basically returns false because we changed the compilation and got a new SemanticModel in the meantime. If we don’t do this, the Equals() method actually returns true.

Comparing the DeclaringSyntaxReferences (as also stated here How to compare type symbols (ITypeSymbol) from different projects in Roslyn?) returns false because we changed the class Foo itself in the meantime. The behaviour would be the same if the constructor parameter would be of type Bar and we rewrote Bar. To verify this, just uncomment the line

//RewriteBar(rewriter, compilation, resultTree); 

and replace the constructor parameter type by Bar in the code sample.

Conclusion: ITypeSymbol.Equals() doesn’t work with a new compilation and semantic model and comparing the DeclaringSyntaxReferences doesn’t work with a type we changed in the meantime. (I also tested the behaviour with a type of an external assembly – in this case ITypeSymbol.Equals() worked for me.)

So my questions are:

  • What’s the intended way to compare types in the described situation?
  • Is there a single catch-all-solution or do I have to kind of mix/combine different approaches to determine type equality (maybe also taking the string representation of the fully qualified name into account)?

This is the full test program with which the issue is reproducible for me. Just copy, include Roslyn references and execute:

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

namespace Demo.TypeSymbol
{
  class Program
  {
    static void Main(string[] args)
    {    
      var compilation = (CSharpCompilation) GetTestCompilation();

      var rewriter = new Rewriter(changeSomething: true);
      var tree = compilation.SyntaxTrees.First(); //first SyntaxTree is the one of class MyAssembly.Foo
      rewriter.Model = compilation.GetSemanticModel (tree);

      //first rewrite run
      var resultTree = rewriter.Visit (tree.GetRoot()).SyntaxTree;
      compilation = UpdateIfNecessary (compilation, rewriter, tree, resultTree);
      rewriter.Model = compilation.GetSemanticModel (resultTree);

      //just for demonstration; comment in to test behaviour when we are rewriting the class Bar -> in this case use Bar as constructor parameter in Foo
      //RewriteBar(rewriter, compilation, resultTree);

      //second rewrite run
      rewriter.Visit (resultTree.GetRoot());

      //now we want to compare the types...

      Console.WriteLine(rewriter.ParameterTypeFirstRun);
      Console.WriteLine(rewriter.ParameterTypeSecondRun);

      //=> types are *not* equal
      var typesAreEqual = rewriter.ParameterTypeFirstRun.Equals (rewriter.ParameterTypeSecondRun);
      Console.WriteLine("typesAreEqual:            " + typesAreEqual);

      //=> syntax references are not equal
      if(rewriter.ParameterTypeFirstRun.DeclaringSyntaxReferences.Any())
      {
        var syntaxReferencesAreEqual =
          rewriter.ParameterTypeFirstRun.DeclaringSyntaxReferences.First()
          .Equals(rewriter.ParameterTypeSecondRun.DeclaringSyntaxReferences.First());
        Console.WriteLine("syntaxReferencesAreEqual: " + syntaxReferencesAreEqual);
      }

      //==> other options??
    }

    private static CSharpCompilation UpdateIfNecessary(CSharpCompilation compilation, Rewriter rewriter, SyntaxTree oldTree, SyntaxTree newTree)
    {
      if (oldTree != newTree)
      {
        //update compilation as the syntaxTree changed
        compilation = compilation.ReplaceSyntaxTree(oldTree, newTree);
        rewriter.Model = compilation.GetSemanticModel(newTree);
      }
      return compilation;
    }

    /// <summary>
    /// rewrites the SyntaxTree of the class Bar, updates the compilation as well as the semantic model of the passed rewriter
    /// </summary>
    private static void RewriteBar(Rewriter rewriter, CSharpCompilation compilation, SyntaxTree firstSyntaxTree)
    {
      var otherRewriter = new Rewriter(true);
      var otherTree = compilation.SyntaxTrees.Last();
      otherRewriter.Model = compilation.GetSemanticModel(otherTree);
      var otherResultTree = otherRewriter.Visit(otherTree.GetRoot()).SyntaxTree;
      compilation = UpdateIfNecessary(compilation, otherRewriter, otherTree, otherResultTree);
      rewriter.Model = compilation.GetSemanticModel(firstSyntaxTree);
    }

    public class Rewriter : CSharpSyntaxRewriter
    {
      public SemanticModel Model { get; set; }
      private bool _firstRun = true;
      private bool _changeSomething;

      public ITypeSymbol ParameterTypeFirstRun { get; set; }
      public ITypeSymbol ParameterTypeSecondRun { get; set; }

      public Rewriter (bool changeSomething)
      {
        _changeSomething = changeSomething;
      }

      public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node)
      {
        node = (ClassDeclarationSyntax)base.VisitClassDeclaration(node);

        //remember the types of the parameter
        if (_firstRun)
          ParameterTypeFirstRun = GetTypeSymbol (node);
        else
          ParameterTypeSecondRun = GetTypeSymbol (node);

        _firstRun = false;

        //change something and return updated node
        if(_changeSomething)
          node = node.WithMembers(node.Members.Add(GetMethod()));
        return node;
      }

      /// <summary>
      /// Gets the type of the first parameter of the first method
      /// </summary>
      private ITypeSymbol GetTypeSymbol(ClassDeclarationSyntax classDeclaration)
      {
        var members = classDeclaration.Members;
        var methodSymbol = (IMethodSymbol) Model.GetDeclaredSymbol(members[0]);
        return methodSymbol.Parameters[0].Type;
      }

      private MethodDeclarationSyntax GetMethod()
      {
        return (MethodDeclarationSyntax)
          CSharpSyntaxTree.ParseText (@"public void SomeMethod(){ }").GetRoot().ChildNodes().First();
      }
    }

    private static SyntaxTree[] GetTrees()
    {
      var treeList = new List<SyntaxTree>();
      treeList.Add(CSharpSyntaxTree.ParseText(Source.Foo));
      treeList.Add(CSharpSyntaxTree.ParseText(Source.Bar));
      return treeList.ToArray();
    }

    private static Compilation GetTestCompilation()
    {
      var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
      var refs = new List<PortableExecutableReference> { mscorlib };

      // I used this to test it with a reference to an external assembly
      // var testAssembly = MetadataReference.CreateFromFile(@"../../../Demo.TypeSymbol.TestAssembly/bin/Debug/Demo.TypeSymbol.TestAssembly.dll");
      // refs.Add (testAssembly);

      return CSharpCompilation.Create("dummyAssembly", GetTrees(), refs);
    }
  }

  public static class Source
  {
    public static string Foo => @"

      // for test with external assembly
      //using Demo.TypeSymbol.TestAssembly;

      namespace MyAssembly
      {
        public class Foo
        {
          public Foo(Foo x)
          {
          }
        }
      }
    ";

    public static string Bar => @"
      namespace MyAssembly
      {
        public class Bar
        {
          public Bar(int i)
          {
          }       
        }
      }
    ";
  }
}
like image 436
Alex Avatar asked Dec 12 '15 17:12

Alex


1 Answers

One possibility is to call SymbolFinder.FindSimilarSymbols which will give you a symbol in your new solution that matches by name and a few other properties. From there you could Equals in your newer Compilation.

like image 128
Jason Malinowski Avatar answered Sep 28 '22 17:09

Jason Malinowski