如何找出哪个程序集拥有从给定的“编译”对象及其声明语法引用中获得的给定 ISymbol 对象?

Posted

技术标签:

【中文标题】如何找出哪个程序集拥有从给定的“编译”对象及其声明语法引用中获得的给定 ISymbol 对象?【英文标题】:How to find out which assembly owns the given ISymbol object obtained from the given `Compilation` object and its declaring syntax references? 【发布时间】:2021-12-21 17:56:51 【问题描述】:

我有以下简单的单元测试:

    在内存中创建 base.dll 程序集 - 获取其字节数组。 根据内存中的 base.dll 创建 main.dll 程序集 - 获取其字节数组。 从两个 dll 中创建 CSharpCompilation 对象

我将在最后发布完整的单元测试,但现在这里是相关片段:

const string BASE_CODE = "public interface I ";
const string MAIN_CODE = "public class T: I ";

MetadataReference systemDllRef = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);

var baseDllBytes = GetDllBytes("base", BASE_CODE);
MetadataReference baseDllRef = MetadataReference.CreateFromStream(new MemoryStream(baseDllBytes), filePath: "base.dll");

var mainDllBytes = GetDllBytes("main", MAIN_CODE, systemDllRef, baseDllRef);
MetadataReference mainDllRef = MetadataReference.CreateFromStream(new MemoryStream(mainDllBytes), filePath: "main.dll");

var compilation = CSharpCompilation.Create("temp", null, new[]  systemDllRef, baseDllRef, mainDllRef );

如您所见,ma​​in.dll 定义了一个单一类型 T,它实现了在 base.dll 中定义的接口 I

接下来我想获取T的类型符号并回答以下问题:

    哪个程序集拥有它? 哪个程序集拥有它的接口? 是否有任何声明语法引用? 它的接口是否有任何声明语法引用?

代码如下:

var mainTypeSymbol = compilation.GetTypeByMetadataName("T");
Assert.AreEqual("main", mainTypeSymbol.ContainingAssembly.Name);                    // GOOD
Assert.AreEqual("base", mainTypeSymbol.Interfaces[0].ContainingAssembly.Name);      // GOOD
CollectionAssert.IsEmpty(mainTypeSymbol.DeclaringSyntaxReferences);                 // BAD !!!
CollectionAssert.IsEmpty(mainTypeSymbol.Interfaces[0].DeclaringSyntaxReferences);   // BAD !!!

当然,声明的语法引用应该是空的,毕竟编译对象根本不包含语法树。我现在要修复它:

compilation = compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(MAIN_CODE));
mainTypeSymbol = compilation.GetTypeByMetadataName("T");
Assert.AreEqual("temp", mainTypeSymbol.ContainingAssembly.Name);                    // BAD !!!
Assert.AreEqual("base", mainTypeSymbol.Interfaces[0].ContainingAssembly.Name);      // GOOD
CollectionAssert.IsNotEmpty(mainTypeSymbol.DeclaringSyntaxReferences);              // GOOD
CollectionAssert.IsEmpty(mainTypeSymbol.Interfaces[0].DeclaringSyntaxReferences);   // BAD !!!

IBYP?现在我有 T 的声明语法参考,但它的程序集报告为 temp,而不是 ma​​in !!!现在,如果我为I 添加语法树:

compilation = compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(BASE_CODE));
mainTypeSymbol = compilation.GetTypeByMetadataName("T");
Assert.AreEqual("temp", mainTypeSymbol.ContainingAssembly.Name);                        // BAD !!!
Assert.AreEqual("temp", mainTypeSymbol.Interfaces[0].ContainingAssembly.Name);          // BAD !!!
CollectionAssert.IsNotEmpty(mainTypeSymbol.DeclaringSyntaxReferences);                  // GOOD
CollectionAssert.IsNotEmpty(mainTypeSymbol.Interfaces[0].DeclaringSyntaxReferences);    // GOOD

现在所有的组装结果都被搞砸了,但是返回了声明的语法引用。

完整的单元测试代码为:

[Test]
public void SymbolAssembly()

    const string BASE_CODE = "public interface I ";
    const string MAIN_CODE = "public class T: I ";

    MetadataReference systemDllRef = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
    
    var baseDllBytes = GetDllBytes("base", BASE_CODE);
    MetadataReference baseDllRef = MetadataReference.CreateFromStream(new MemoryStream(baseDllBytes), filePath: "base.dll");

    var mainDllBytes = GetDllBytes("main", MAIN_CODE, systemDllRef, baseDllRef);
    MetadataReference mainDllRef = MetadataReference.CreateFromStream(new MemoryStream(mainDllBytes), filePath: "main.dll");

    var compilation = CSharpCompilation.Create("temp", null, new[]  systemDllRef, baseDllRef, mainDllRef );
    var mainTypeSymbol = compilation.GetTypeByMetadataName("T");
    Assert.AreEqual("main", mainTypeSymbol.ContainingAssembly.Name);                    // GOOD
    Assert.AreEqual("base", mainTypeSymbol.Interfaces[0].ContainingAssembly.Name);      // GOOD
    CollectionAssert.IsEmpty(mainTypeSymbol.DeclaringSyntaxReferences);                 // BAD !!!
    CollectionAssert.IsEmpty(mainTypeSymbol.Interfaces[0].DeclaringSyntaxReferences);   // BAD !!!

    compilation = compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(MAIN_CODE));
    mainTypeSymbol = compilation.GetTypeByMetadataName("T");
    Assert.AreEqual("temp", mainTypeSymbol.ContainingAssembly.Name);                    // BAD !!!
    Assert.AreEqual("base", mainTypeSymbol.Interfaces[0].ContainingAssembly.Name);      // GOOD
    CollectionAssert.IsNotEmpty(mainTypeSymbol.DeclaringSyntaxReferences);              // GOOD
    CollectionAssert.IsEmpty(mainTypeSymbol.Interfaces[0].DeclaringSyntaxReferences);   // BAD !!!

    compilation = compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(BASE_CODE));
    mainTypeSymbol = compilation.GetTypeByMetadataName("T");
    Assert.AreEqual("temp", mainTypeSymbol.ContainingAssembly.Name);                        // BAD !!!
    Assert.AreEqual("temp", mainTypeSymbol.Interfaces[0].ContainingAssembly.Name);          // BAD !!!
    CollectionAssert.IsNotEmpty(mainTypeSymbol.DeclaringSyntaxReferences);                  // GOOD
    CollectionAssert.IsNotEmpty(mainTypeSymbol.Interfaces[0].DeclaringSyntaxReferences);    // GOOD


private static byte[] GetDllBytes(string name, string code, params MetadataReference[] metadataReferences)

    var syntaxTree = CSharpSyntaxTree.ParseText(code);
    var c = CSharpCompilation.Create(name, new[]  syntaxTree , metadataReferences, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
    var stream = new MemoryStream();
    var res = c.Emit(stream);
    Assert.IsTrue(res.Success);
    var bytes = stream.GetBuffer();
    if (bytes.Length > stream.Position)
    
        bytes = new byte[stream.Position];
        Array.Copy(stream.GetBuffer(), bytes, stream.Position);
    
    return bytes;

我可以这样解释结果:

当没有匹配的语法树时: 没有声明语法引用。可以理解。 ISymbol.ContainingAssembly 属性返回由各自的 MetadataReference 对象表示的实际程序集。 当存在匹配的语法树时: 有声明语法引用。也有道理。 由于某种原因,在 Compilation 对象中找到匹配的语法树会更改 ISymbol.ContainingAssembly 属性的结果 - 现在它是 Compilation 对象的名称。

现在我的问题 - 如何从包含所有正确 MetadataReferenceSyntaxTree 对象的 Compilation 对象中获取包含程序集和相应的声明语法引用?

基本原理

我们正在分解我们的单体应用程序。这包括很多“愚蠢”的重构。我所说的“愚蠢”是指可以合理自动化的那些。例如,假设有两个非常频繁使用的依赖注入接口,我想将一个方法从一个移动到另一个。在使用移动方法的所有地方都需要进行许多类似的更改。其中 95% 可以自动化,所以我编写了一个工具来实现它。但不是试图猜测代码必须调整的所有地方,而是编译代码,然后自动解决构建错误。也许这是一种错误的方法,但这就是我目前正在做的事情:

我将代码中的所有类型映射到所有解决方案(我们有许多解决方案,并且对所有解决方案都进行了重构),包括源文件路径以及相关类型正在使用和正在使用的类型。这是重构开始之前的初步操作。它非常聪明,因为它知道处理“好的”动态调用和已知常量。随后使用生成的地图(大小约为 100MB)。 代码移动了方法(碰巧在这种特殊情况下几乎没有需要移动的依赖项) 代码开始构建修复循环,其中每个错误都被解析并相应地修复代码。

修复包括从所有相关的MetadataReference 对象创建一个Compilation 对象,并添加SyntaxTree 对象,以修复错误。现在,Compilation 对象的名称与正在构建的程序集的名称相匹配,因此只要修复仅限于同一个程序集,一切都运行良好。但是,如果为了修复项目 X,我需要返回并更新项目 Y,这意味着 Compilation 对象现在具有来自 X 和 Y 的 SyntaxTree 对象,这不好,因为它更改了 ContainingAssembly 属性。所以,现在我每个错误修复会话只有一个 Compilation 对象,但我似乎不能再使用这个模型了。

也许这都是一个愚蠢的想法,但它确实可以很好地工作并产生良好的结果,只要我在修复当前项目中的错误时不必回到其他项目。如果无法修复代码(因为它不知道如何修复),构建修复循环允许手动干预,并且它能够自动完成大约 95% 的更改。

说明 1

当发生编译错误时,我使用以下部分创建 Compilation 对象:

    上一次成功构建项目的 DLL(即从它创建的MetadataReference)。 来自 (1) 的 DLL 引用的所有 DLL 错误中提到的文件的语法树。

然后我开始根据需要添加语法树。所以我从不为所有源文件添加所有语法树。只有少数需要,有时其中一些会对应于依赖项目中的符号。这就是发生的情况,为了修复与该错误相关的项目中的错误,我需要返回并更改某个依赖项项目所拥有的源文件中的某些内容。在此过程中,来自其他项目的一些语法树被添加到 Compilation 对象中,这就是我最终在同一 Compilation 对象中获得来自不同项目的语法树的方式。

【问题讨论】:

【参考方案1】:

在 Compilation 对象中找到匹配的语法树会更改 ISymbol.ContainingAssembly 属性的结果 - 它现在是 Compilation 对象的名称。

这是实现 C# 行为,如果您有定义类型的元数据引用和定义相同类型的源文件,则源文件胜出。因此,一旦添加了 T 的源定义,就隐藏了元数据实现。类似地,一旦你添加了 I 的定义,那也是来自源并隐藏了 I 的元数据定义。

我不清楚你想通过同时添加源文件和元数据引用来实现什么;您可能想在此处更新您的问题以阐明您的最终目标。

【讨论】:

我发布了理由。有点长,但我希望它有意义。 我想我可以使用我的类型映射从完整的类型名称中推断出程序集。有一些测试项目链接到源文件,因此在不同的程序集中会产生相同的完整类型名称,但当然,这些项目永远不会出现在同一个 Compilation 对象中。所以这场比赛将是独一无二的。不过,我很好奇是否有可能以某种方式找出哪个 MetadataReference 拥有哪个符号,即使给出了源树。 所以阅读你的理由,我仍然不明白你为什么要添加像这样镜像元数据引用的语法节点。具体来说:“但是,如果为了修复项目 X,我需要返回并更新项目 Y,这意味着 Compilation 对象现在具有来自 X 和 Y 的 SyntaxTree 对象,这是不好的,因为它改变了 ContainingAssembly 属性。”感觉在这种情况下,我不确定您是否应该编辑 Y 的内容,然后更新对 X 的引用。如果管理所有对象很棘手,我们有工作区层来管理跨项目引用。 在您的情况下,引用是来自在其他地方构建的 DLL,还是也构建在您的应用程序中?因为通常如果它来自您的同一个应用程序并且您正在为两者制作编译对象,请不要将其发射到内存中——我们允许直接编译到编译的引用跳过发射步骤,而且速度也快得多。在这些情况下,符号实际上是从源代码中出现的,因为中间没有步骤。 我需要处理您的输入。

以上是关于如何找出哪个程序集拥有从给定的“编译”对象及其声明语法引用中获得的给定 ISymbol 对象?的主要内容,如果未能解决你的问题,请参考以下文章

如何从给定的计数、平均值、标准差、最小值、最大值等生成数据集?

如何找出从哪个模块导入名称?

加载程序集及其依赖项

如何从 C 源文件调用 arm 程序集?

如何注册COM对象及其相关的Interop程序集?

从给定的 x86 程序集编写 C 函数