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.cs
和Bar.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
1条答案
按热度按时间v1uwarro1#
为了维持两者
...完整的静态分析、自动完成和其他编译器支持的IDE功能。这意味着,如果生成的文件包含错误,我希望在原始文件中报告该错误。
以及
...使用户能够在多个文件中编写顶级语句,同时让我的框架在生成的代码上调用Execute()来执行所有语句...
会很困难。
我已经找到了一种方法,它有***严重的限制和警告***。其他解决方案,如源代码生成器,自定义预编译脚本/构建步骤和动态代码生成 * 可能 * 是可能的,但它们会有自己的包袱。
每个顶级语句都必须存在于一个单独的
.csproj
中。这很不幸,但是. NET世界在强加逻辑边界(名称空间)之前 * 就为代码强加了一个物理边界(项目/程序集)。为了避免CS8802
,您别无选择,只能将每个顶级语句代码放在一个单独的项目中。如果我们可以接受这个限制,那么在编译is a breeze之前的用户体验,他们可以得到顶级的静态分析,并且在调试时,他们可以运行他们的"main",获得很好的调试器支持,并与其他用户隔离。
但是,奇怪的是,这些独立的
.csproj
都必须是可执行文件(可能是控制台项目),而对于您,框架开发人员,您如何处理所有这些项目呢?通常的做法是添加一个项目引用。你需要为每一个"顶级体验"添加一个。你可以添加一个可执行项目作为对库或其他可执行文件(例如你的框架)的引用,因为. NET的exe和dll仍然是程序集。
它有点颠倒了构建过程。我理解不想把硬引用带进框架。在MEF的时代,很可能需要某种程度的运行时检查。
顶级语句没有命名空间,或者更准确地说,它们的命名空间是根命名空间,这意味着每个顶级语句都有相同的命名空间、类和方法:
global::Program.<Main>$
如图所示已反编译:更糟糕的是,类和方法的可访问性是
internal
。那么,如何绕过
internal
以及如何绕过相同的类型和签名呢?答案是应用InternalsVisibleToAttribute并为每个引用命名别名。在已使用的项目中,将属性添加到顶级语句文件中(我知道,这并不酷,而且有点违背目的,但您可以这样做)。
在使用项目(我称之为
ClassLibrary1
)中,将Aliases
元素添加到每个项目引用中。为了减少这种多项目体验的负担,您可以为用户创建一个模板,也许可以从C#Console Application Template对其进行自定义。在此模板中,您可以添加一个C#文件,例如包含以下内容的
InternalsVisibleTo.cs
:请参阅documentation以了解如何创建自定义模板以及如何安装它。一旦完成,用户就可以使用
dotnet new nameOfTemplate
或Visual Studio快速添加项目。创建的Program.cs
现在不会有这些样板行,用户可以忽略InternalsVisibleTo.cs
。要调用用户的每个顶级语句,可以使用反射和
extern alias
指令:当然,您可能需要一个变体,它可以通过反射找到所有用户的语句,我只是在这里提供了一个示例。
上面的测试已经过了,下面是一个简单的测试,它按预期运行。
这显然与编写插件、框架和尝试泛化代码时通常的(惯用的)做事方式不同,但它是有效的。
然而,
<Main>$
不再正确会发生什么)?和往常一样,这归结为编程中的常见权衡。