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

(我正在尝试通过从vs生成的设置文件生成接口和包装类来解决Application.Settings / MVVM问题。)

我想做的是:

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

我的问题是双重的:

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

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

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

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

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

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

 var originalClass = compilationUnit.DescendantNodes().OfType().Single(); string originalClassName = originalClass.Identifier.ValueText; var properties = originalClass.DescendantNodes().OfType(); 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()); 

我认为罗斯林是解决这个问题的好方法。 关于我将使用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(Syntax.ParseTypeName("ISettings")))); private ClassDeclarationSyntax @class = Syntax.ClassDeclaration("SettingsClass") .WithBaseList(Syntax.BaseList( Syntax.SeparatedList(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. } } 

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

各自的优点/缺点:

Roslyn ParseText

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

Roslyn SyntaxNode构建

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

T4模板

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

Roslyn代码提示

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

      private static TSyntax ParseText(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().First(); } 
  • 手动构建SyntaxNodes ,通常需要对SyntaxTree.NormalizeWhitespace(elasticTrivia: true)进行最终调用,以使代码“可以循环访问”。
  • 通常,您需要使用SyntaxNode.ToFullString()来获取包含琐事的实际代码文本。
  • 使用SyntaxTree.WithFilePath()作为存储最终文件名的方便位置,以便在编写代码时使用。
  • 如果您的目标是输出源文件,那么最终游戏最终将使用有效的CompilationUnitSyntaxs
  • 不要忘记使用Formatter.Format进行漂亮打印作为最后一步。

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