.net 在MSBuild中的编译器处理C#文件之前重写这些文件

pod7payv  于 2023-02-17  发布在  .NET
关注(0)|答案(1)|浏览(145)

bounty将在4天后过期。回答此问题可获得+500的声誉奖励。noseratio正在寻找来自声誉良好来源的答案

我正在寻找一种方法来扩展C#项目的现有MSBuild编译流程,方法是在编译器获取某些代码文件之前重写它们。更具体地说,我希望使用顶级语句处理文件,并将底层代码 Package 在自定义类/方法中,而不是自动生成的<Program>$.Main()中。
换句话说,给定一个具有以下两个编译单元的C#项目:

  • Foo.cs
Console.WriteLine("Foo");
  • Bar.cs
Console.WriteLine("Bar");

我想注入一些处理逻辑,这样这些文件就可以传递给编译器:

  • Foo.g.cs
public static class Foo
{
    public static void Execute()
    {
        Console.WriteLine("Foo");
    }
}
  • Bar.g.cs
public static class Bar
{
    public static void Execute()
    {
        Console.WriteLine("Bar");
    }
}

同时,我希望用户在编辑原始的Foo.csBar.cs文件时保持完整的静态分析、自动完成和其他编译器支持的IDE特性,也就是说,如果生成的文件包含错误,我希望在原始文件中报告错误。
最终,我的目标是使用户能够在多个文件中编写顶级语句,同时让我的框架在生成的代码上调用Execute()来执行所有语句(顺序不重要)。

要实现这样的目标,我需要采取的最佳方法和最少步骤是什么?

我最初的想法是这样做:
1.从编译中排除原始文件,但将其作为<None>项包括在内,以便它们仍作为项目的一部分
1.生成输出文件(使用源代码生成器或简单的MSBuild任务)
1.使用#line指令将所有静态分析从生成的文件重新Map到原始文件

  • Project.csproj
<ItemGroup>
    <Compile Remove="*.cs" />
    <None Include="*.cs" />
</ItemGroup>
  • x1米11米1x
public static class Foo
{
    public static void Execute()
    {
#line 1 "File.cs"
Console.WriteLine("Foo");
    }
}

这在dotnet build中运行良好(在原始文件中报告错误),但在IDE中的行为不是最佳的:

  • 在VS代码中,这可以正常工作,但OmniSharp继续说“只有一个编译单元可以有顶级语句”,尽管文件根本不是编译的一部分。使用#pragma warning disable CS8802没有效果。错误也需要一段时间来重复,有时会“卡住”,直到我手动运行dotnet build
  • 在Visual Studio和JetBrains Rider中,行重Map似乎根本没有得到考虑。原始文件中没有显示错误,也没有对它们执行静态分析。但语法突出显示可以工作,包括符号检测(我可以检查方法签名,跳转到定义等)。

注意:我试图尽可能简单地描述这个问题,但我在这里的主要想法是原型化一个新的测试框架,它将使用TLS来定义测试(与类和方法相反),类似于其他语言中的做法:https://github.com/Tyrrrz/Hallstatt

v1uwarro

v1uwarro1#

为了维持两者
...完整的静态分析、自动完成和其他编译器支持的IDE功能。这意味着,如果生成的文件包含错误,我希望在原始文件中报告该错误。
以及
...使用户能够在多个文件中编写顶级语句,同时让我的框架在生成的代码上调用Execute()来执行所有语句...
会很困难。
我已经找到了一种方法,它有***严重的限制和警告***。其他解决方案,如源代码生成器,自定义预编译脚本/构建步骤和动态代码生成 * 可能 * 是可能的,但它们会有自己的包袱。

    • 注:我并不是在提倡这个。**我的解决方案可能非常脆弱,而且它高度滥用了. NET中惯用的做事方式。最终,我会让你所谓的"用户"(稍后我将不带引号地引用这个词)在名称空间中的类中(出于源代码控制的目的,在文件中)创作一个方法。
    • 限制1**

每个顶级语句都必须存在于一个单独的.csproj中。这很不幸,但是. NET世界在强加逻辑边界(名称空间)之前 * 就为代码强加了一个物理边界(项目/程序集)。为了避免CS8802,您别无选择,只能将每个顶级语句代码放在一个单独的项目中。
如果我们可以接受这个限制,那么在编译is a breeze之前的用户体验,他们可以得到顶级的静态分析,并且在调试时,他们可以运行他们的"main",获得很好的调试器支持,并与其他用户隔离。

    • 注意事项1**

但是,奇怪的是,这些独立的.csproj都必须是可执行文件(可能是控制台项目),而对于您,框架开发人员,您如何处理所有这些项目呢?
通常的做法是添加一个项目引用。你需要为每一个"顶级体验"添加一个。你可以添加一个可执行项目作为对库或其他可执行文件(例如你的框架)的引用,因为. NET的exe和dll仍然是程序集。
它有点颠倒了构建过程。我理解不想把硬引用带进框架。在MEF的时代,很可能需要某种程度的运行时检查。

    • 限制2和3**

顶级语句没有命名空间,或者更准确地说,它们的命名空间是根命名空间,这意味着每个顶级语句都有相同的命名空间、类和方法:global::Program.<Main>$如图所示已反编译:

更糟糕的是,类和方法的可访问性是internal

    • 注意事项2和3**

那么,如何绕过internal以及如何绕过相同的类型和签名呢?答案是应用InternalsVisibleToAttribute并为每个引用命名别名。
在已使用的项目中,将属性添加到顶级语句文件中(我知道,这并不酷,而且有点违背目的,但您可以这样做)。

using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("ClassLibrary1")]

Console.WriteLine("Hello, World!");

在使用项目(我称之为ClassLibrary1)中,将Aliases元素添加到每个项目引用中。

<ItemGroup>
  <ProjectReference Include="..\ConsoleApp1\ConsoleApp1.csproj">
    <Aliases>One</Aliases>
  </ProjectReference>
  <ProjectReference Include="..\ConsoleApp2\ConsoleApp6.csproj">
    <Aliases>Two</Aliases>
  </ProjectReference>
</ItemGroup>

为了减少这种多项目体验的负担,您可以为用户创建一个模板,也许可以从C#Console Application Template对其进行自定义。在此模板中,您可以添加一个C#文件,例如包含以下内容的InternalsVisibleTo.cs

using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("ClassLibrary1")]

请参阅documentation以了解如何创建自定义模板以及如何安装它。一旦完成,用户就可以使用dotnet new nameOfTemplate或Visual Studio快速添加项目。创建的Program.cs现在不会有这些样板行,用户可以忽略InternalsVisibleTo.cs

    • 执行这场混乱**

要调用用户的每个顶级语句,可以使用反射和extern alias指令:

using static System.Reflection.BindingFlags;

namespace ClassLibrary1;

extern alias Two;
extern alias One;

public static class Class1
{
    public static void YourExecutor()
    {
        var method = typeof(One::Program).GetMethod("<Main>$", NonPublic | Static);
        method?.Invoke(null, new object?[] { Array.Empty<string>() });
        method = typeof(Two::Program).GetMethod("<Main>$", NonPublic | Static);
        method?.Invoke(null, new object?[] { Array.Empty<string>() });
    }
}

当然,您可能需要一个变体,它可以通过反射找到所有用户的语句,我只是在这里提供了一个示例。
上面的测试已经过了,下面是一个简单的测试,它按预期运行。

[TestFixture]
public class Tester
{
    [Test]
    public void ThisIsCrazy()
    {
        Class1.YourExecutor();
    }
}
    • 最后警告**

这显然与编写插件、框架和尝试泛化代码时通常的(惯用的)做事方式不同,但它是有效的。
然而,

  • AOT编译可能会使整个方法失败。
  • 非公开的更改使它变得脆弱(如果<Main>$不再正确会发生什么)?
  • 添加几行代码并理解 * 为什么 * 这些代码是必要的,这对您的用户来说是一种负担。

和往常一样,这归结为编程中的常见权衡。

相关问题