使用Roslyn解析/转换/生成代码:我的目标是太高还是太低?

Ben*_*jol 22 c# code-generation roslyn

(我要做的是通过从vs生成的设置文件生成接口和包装类来解决Application.Settings/MVVM问题.)

我想做的是:

  • 从文件中解析类声明
  • 仅基于类的(非静态)属性生成接口声明
  • 生成一个实现此接口的包装类,在构造函数中获取原始类的实例,并将所有属性"管道"到实例.
  • 生成另一个直接实现接口的类.

我的问题是双重的:

  • 我吠叫错了树吗?我会更好地使用Code-Dom,T4,Regex(!)来实现这个目标,还是部分原因?(我不介意一些额外的工作,因为这主要是一种学习经历.)
  • 如果罗斯林是要走的路,我应该关注哪一点?我有点天真地希望有一些方法可以走树并吐出我想要的东西,但是我无法理解是否/如何使用SyntaxRewriter来实现它,或者是否要使用流畅的结构,多次查询源我需要的位.

如果你想评论MVVM方面,你可以,但这不是问题的主旨:)

svi*_*ick 13

如果您的要求是解析C#源代码,那么我认为Roslyn是一个不错的选择.如果您打算将它用于此部分,我认为将它用于代码生成也是有意义的.

使用Roslyn生成代码可能非常冗长(特别是与CodeDom相比时),但我认为这对您来说不是一个大问题.

我认为SyntaxRewriter最适合在代码中进行本地化更改.但是你要问的是解析整个类并基于它生成类型,我认为,直接查询语法树最有效.

例如,为类中的所有属性生成只读接口的最简单示例可能如下所示:

var originalClass =
    compilationUnit.DescendantNodes().OfType<ClassDeclarationSyntax>().Single();
string originalClassName = originalClass.Identifier.ValueText;
var properties =
    originalClass.DescendantNodes().OfType<PropertyDeclarationSyntax>();

var generatedInterface =
    SyntaxFactory.InterfaceDeclaration('I' + originalClassName)
          .AddMembers(
              properties.Select(
                  p =>
                  SyntaxFactory.PropertyDeclaration(p.Type, p.Identifier)
                        .AddAccessorListAccessors(
                            SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
                                  .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))))
                        .ToArray());
Run Code Online (Sandbox Code Playgroud)


Kev*_*lch 5

我认为罗斯林是解决这个问题的好方法.关于我将使用Roslyn的哪个部分 - 我可能会使用SyntaxWalker原始类,然后使用Fluent API为SyntaxNodes您要生成的新类型构建新的.您可以在生成的代码中重用原始树的某些部分(例如,参数列表等).

这可能是一个简单的示例:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Roslyn.Compilers;
using Roslyn.Compilers.CSharp;
using Roslyn.Services;
using Roslyn.Services.CSharp;

    class Program
    {
        static void Main(string[] args)
        {
            var syntaxTree = SyntaxTree.ParseText(@"
class C
{
    internal void M(string s, int i)
    {
    }
}");


        }
    }


class Walker : SyntaxWalker
{
    private InterfaceDeclarationSyntax @interface = Syntax.InterfaceDeclaration("ISettings");

    private ClassDeclarationSyntax wrapperClass = Syntax.ClassDeclaration("SettingsWrapper")
        .WithBaseList(Syntax.BaseList(
            Syntax.SeparatedList<TypeSyntax>(Syntax.ParseTypeName("ISettings"))));

    private ClassDeclarationSyntax @class = Syntax.ClassDeclaration("SettingsClass")
        .WithBaseList(Syntax.BaseList(
            Syntax.SeparatedList<TypeSyntax>(Syntax.ParseTypeName("ISettings"))));

    public override void VisitMethodDeclaration(MethodDeclarationSyntax node)
    {
        var parameters = node.ParameterList.Parameters.ToArray();
        var typeParameters = node.TypeParameterList.Parameters.ToArray();
        @interface = @interface.AddMembers(
            Syntax.MethodDeclaration(node.ReturnType, node.Identifier.ToString())
                .AddParameterListParameters(parameters)
                .AddTypeParameterListParameters(typeParameters));

        // More code to add members to the classes too.
    }
}
Run Code Online (Sandbox Code Playgroud)


tst*_*ter 5

我正在做非常相似的事情,并且我也在使用Roslyn来解析现有的C#代码。但是,我正在使用T4模板来生成新代码。T4模板是为文本生成而设计的,并且提供了非常好的抽象,因此您可以实际指定看起来像代码的东西,而不是这个疯狂的对象树。


Jac*_*eja 5

关于代码生成的问题,我的建议是实际使用内联代码片段(使用解析CSharpSyntaxTree.ParseText)和手动生成的组合SyntaxNodes,但强烈偏好前者.我过去也使用过T4,但由于普遍缺乏集成和功能,我正在远离它们.

各自的优点/缺点:

Roslyn ParseText

  • 生成可识别的更易读的代码生成器代码.
  • 允许"文本模板化"方法,例如使用C#6字符串插值.
  • 不那么冗长.
  • 保证有效的语法树.
  • 可以更高效.
  • 更容易上手.
  • SyntaxNodes如果多数是程序性的,文本可能变得难以阅读.

Roslyn SyntaxNode构建

  • 更好地转换现有的语法树 - 无需从头开始.
    • 但现有的琐事可能使这种混乱/复杂.
  • 更冗长.可能难以阅读和构建.
    • 语法树通常比您想象的更复杂
  • SyntaxFactory API提供有效语法的指导.
  • Roslyn Quoter可帮助您将文本代码转换为工厂代码.
  • 语法树不一定有效.
  • 一旦编写,代码可能更强大.

T4模板

  • 如果要生成的大部分代码都是锅炉板,那就好了.
  • 没有适当的CI支持.
  • 没有第三方扩展的语法高亮或智能感知.
  • 输入和输出文件之间的一对一映射.
    • 如果您正在进行更复杂的生成,例如基于单个输入的整个类层次结构,则不理想.
  • 仍然可能想使用Roslyn来"反映"输入类型,否则你将遇到System.Reflection和文件锁等问题.
  • 不易发现的API.T4包括,参数等可能令人困惑学习.

Roslyn代码提示

  • 如果您只是解析代码片段,例如方法语句,那么您将需要使用CSharpParseOptions.Default.WithKind(SourceCodeKind.Script)以获取正确的语法节点.
  • 如果要解析方法体的整个代码块,则需要将其解析为a GlobalStatementSyntax,然后Statement以a的身份访问该属性BlockSyntax.
  • 使用帮助器方法解析单个SyntaxNodes:

        private static TSyntax ParseText<TSyntax>(string code, bool asScript = false)
        {
            var options = asScript
                ? CSharpParseOptions.Default.WithKind(SourceCodeKind.Script)
                : CSharpParseOptions.Default;
    
            var syntaxNodes =
                CSharpSyntaxTree.ParseText(code, options)
                    .GetRoot()
                    .ChildNodes();
    
            return syntaxNodes.OfType<TSyntax>().First();
        }
    
    Run Code Online (Sandbox Code Playgroud)
  • SyntaxNodes手动构建时,您通常需要进行最后的调用,SyntaxTree.NormalizeWhitespace(elasticTrivia: true)以使代码"可以绕过".
  • 通常,您需要使用SyntaxNode.ToFullString()来获取实际的代码文本,包括琐事.
  • 使用SyntaxTree.WithFilePath()作为存储,当你来到写出来的代码最终的文件名一个方便的地方.
  • 如果你的目标是输出源文件,最终游戏最终会有效CompilationUnitSyntaxs.
  • 不要忘记使用漂亮的印刷Formatter.Format作为最后的步骤之一.