当前位置: 首页 > 工具软件 > Roslyn > 使用案例 >

生成项目有roslyn_Roslyn的代码生成:UML的骨架类

郑俊彦
2023-12-01

生成项目有roslyn

我们已经看到了使用Roslyn 转换分析 C#代码的一些示例。 现在,我们将看到如何使用Roslyn创建更复杂的代码生成示例以及如何使用Sprache进行解析 。 我们将根据PlantUML文件创建一个骨架类。 简而言之,我们所做的与我们所做的相反。 当然,第一步是解析。

@startuml
class ClassDiagramGenerator {
    - writer : TextWriter
    - indent : string
    - nestingDepth : int
    + ClassDiagramGenerator(writer:TextWriter, indent:string)
    + {{static}} VisitInterfaceDeclaration(node:Node) : void    
    + {{static}} VisitStructDeclaration(node:Node) : void
    + VisitEnumDeclaration(node:Node) : void
    - WriteLine(line:string) : void
    - GetTypeModifiersText(modifiers:SyntaxTokenList) : string
    - GetMemberModifiersText(modifiers:SyntaxTokenList) : string
}
@enduml

如您所见,此示例中有四个实体:PlantUML开始和结束标签以及类,变量和函数声明。

解析所有东西

我们将逐行解析文件,而不是一sw而就,这部分是由于Sprache的局限性,而且还因为一次正确解析一件东西而不是尝试正确处理它更容易。一口气。

public static Parser<string> UmlTags =
     Parse.Char('@').Once().Then(_ => Parse.CharExcept('\n').Many()).Text().Token();
 
public static Parser<string> Identifier =
     Parse.CharExcept(" ,):").Many().Text().Token();

使用CharExcept我们可以解析除指示的字符外的所有字符,这是一种方便但不精确的方式来收集标识符的所有文本。 这个过程的难度很明显,因为我们被迫排除了标识符后面的所有字符。 如果在本文开头查看文件.plantuml,则会看到字段名称后有一个空格,修饰符static后有一个'}',在参数后有一个':'来划分标识符及其标识符类型,最后是类型后的右括号。 您可能会说,我们只需要检查“字母”,它就可以在这种特定情况下使用,但是会排除合法的C#名称作为标识符。

public static Parser<string> Modifier = 
  Parse.Char('+').Once().Return("public")
  .Or(Parse.Char('-').Once().Return("private"))
  .Or(Parse.Char('#').Once().Return("protected"))
  .Or(from start in StartBracket.Then(_ => StartBracket).Once()
      from modifier in Parse.CharExcept('}').Many().Text().Token()
      from end in EndBracket.Then(_ => EndBracket).Once()
      select modifier
  )
  .Or(from start in LessThen.Then(_ => LessThen).Once()
      from modifier in Parse.CharExcept('>').Many().Text().Token()
      from end in GreaterThen.Then(_ => GreaterThen).Once()
      select modifier
  )
  .Text().Token();
        
public static Parser<Field> Field =
  from modifiers in Parse.Ref(() => Modifier).DelimitedBy(Parse.Char(' ').Many().Token()).Optional()  
  from name in Identifier
  from delimeter in Parse.Char(':')
  from type in Identifier
  select new Field(name, type, modifiers.IsDefined ? modifiers.Get() : null);
 
public static Parser<Method> Method =
  from modifiers in Parse.Ref(() => Modifier).DelimitedBy(Parse.Char(' ').Many().Token()).Optional()
  from name in Parse.CharExcept('(').Many().Text().Token()
  from startArg in Parse.Char('(')
  from arguments in Parse.Ref(() => Field).DelimitedBy(Parse.Char(',').Many().Token()).Optional()
  from endArg in Parse.Char(')')
  from delimeter in Parse.String(" : ").Optional()
  from returnType in Identifier.Optional()
  select new Method(modifiers.IsDefined ? modifiers.Get() : null,
                    name, arguments.IsDefined ? arguments.Get() : null
                    returnType.IsDefined ? returnType.Get() : null);

Modifier解析器非常有趣,除了第6行和第11行,我们看到刚才提到的用于标识正确名称的相同问题。 最后一种情况是指一些不在这个例子中发生,但在其他UML图可能发生: override 修饰符。 真正的问题是在第18和22行,在这里我们看到Ref解析器,正如文档所说,该解析器用于:“间接引用另一个解析器。 这允许解析器之间的循环编译时依赖性。 DelimitedBy用于选择由指定规则定界的许多相同项目,最后Optional指的是不需要正确解析但有可能出现的规则。 由于规则是可选的,因此该值可能是不确定的,必须使用第22行显示的方法对其进行访问。规则Method稍微复杂一些,但是它使用相同的方法。 如果您想知道,没有返回类型的方法就是构造函数。

逐行解析

foreach (var line in lines)
{
    var attemptedClass = UmlParser.Class.TryParse(line);
    if (attemptedClass.WasSuccessful)
    {
        currentClass.Name = attemptedClass.Value;                                
    }
 
    var attemptedMethod = UmlParser.Method.TryParse(line);
    if (attemptedMethod.WasSuccessful)
    {
        currentClass.Declarations.Add(attemptedMethod.Value);
        continue;
    }
 
    var attemptedField = UmlParser.Field.TryParse(line);
    if (attemptedField.WasSuccessful)
    {
        currentClass.Declarations.Add(attemptedField.Value);                                
    }
 
    var attempted = UmlParser.EndBracket.TryParse(line);
    if (attempted.WasSuccessful)
    {
        currentClass.Generate();
        currentClass = new UmlClass(writer, (new DirectoryInfo(outputDir)).Name, Path.GetFileNameWithoutExtension(file));                                
    }
}

我们可以看到解析器在main方法上起作用,在该方法中,我们尝试使用每个解析器解析每一行,如果成功,则将值添加到自定义类型中,稍后再介绍。 我们需要一个自定义类型,因为代码生成需要将所有元素都放在适当的位置,我们不能一行一行地进行操作,至少如果我们要使用Roslyn的格式化程序,则无法这样做。 我们可以只获取信息并自己打印它们,这对于小型项目来说已经足够了,但是对于大型项目来说就很复杂了。 此外,我们会错过所有不错的格式化自动选项。 在第13行,如果找到了方法,我们将跳过一个循环,因为方法也可能被不正确地解析为字段,因此为避免风险,我们跳过了。

代码生成

public void Generate()
{
   CompilationUnitSyntax cu = SyntaxFactory.CompilationUnit()
        .AddUsings(SyntaxFactory.UsingDirective
            (SyntaxFactory.IdentifierName("System")))
        .AddUsings(SyntaxFactory.UsingDirective
            (SyntaxFactory.IdentifierName("System.Collections.Generic")))
        .AddUsings(SyntaxFactory.UsingDirective
            (SyntaxFactory.IdentifierName("System.Linq")))
        .AddUsings(SyntaxFactory.UsingDirective
            (SyntaxFactory.IdentifierName("System.Text")))
        .AddUsings(SyntaxFactory.UsingDirective
            (SyntaxFactory.IdentifierName("System.Threading.Tasks")));
 
    NamespaceDeclarationSyntax localNamespace = SyntaxFactory.NamespaceDeclaration(SyntaxFactory.IdentifierName(directoryName));
 
    ClassDeclarationSyntax localClass = SyntaxFactory.ClassDeclaration(Name);

如果您还记得有关Roslyn的第一课,那么它很冗长,因为它非常强大。 您还必须记住,我们不能修改节点,即使是我们自己创建的节点,也不能从文件中解析出来。 一旦可以使用SyntaxFactory进行所有操作,一切就很明显了,您只需要找到正确的方法即可。 using指令只是Visual Studio默认情况下通常插入的指令。

方法的产生

foreach (var member in Declarations)
{                
 switch (member.DeclarationType)
 {
   case "method":                        
     var currentMethod = member as Method;
          
     MethodDeclarationSyntax method =
       SyntaxFactory.MethodDeclaration(
         SyntaxFactory.IdentifierName(SyntaxFactory.Identifier(currentMethod.Type)),
         currentMethod.Name);
          
     List<SyntaxToken> mods = new List<SyntaxToken>();
          
     foreach (var modifier in currentMethod.Modifiers)
       mods.Add(SyntaxFactory.ParseToken(modifier));
          
     method = method.AddModifiers(mods.ToArray());
          
     SeparatedSyntaxList<ParameterSyntax> ssl =
       SyntaxFactory.SeparatedList<ParameterSyntax>();
     foreach (var param in currentMethod.Arguments)
     {                          
       ParameterSyntax ps = SyntaxFactory.Parameter(
         new SyntaxList<AttributeListSyntax>(),
         new SyntaxTokenList(),
         SyntaxFactory.IdentifierName(SyntaxFactory.Identifier(param.Type)),
         SyntaxFactory.Identifier(param.Name), null);
          
         ssl = ssl.Add(ps);
      }
 
      method = method.AddParameterListParameters(ssl.ToArray());
                                      
      ThrowStatementSyntax notReady =
        SyntaxFactory.ThrowStatement(
          SyntaxFactory.ObjectCreationExpression(
            SyntaxFactory.IdentifierName("NotImplementedException"),
            SyntaxFactory.ArgumentList(), null));                        
 
      method = method.AddBodyStatements(notReady);                        
 
      localClass = localClass.AddMembers(method);
    break;

让我们首先说一下, DeclarationsDeclarationType是我们的自定义类中的字段(未显示),但是您可以在源代码中查看它。 然后,我们继续生成骨架C#类的方法。 MethodDeclaration允许我们选择方法本身的名称和返回类型。 mod指的是修饰符,显然可能不止一个,所以它们在列表中。 然后,我们创建参数,在我们的情况下只需要一个名称和一个类型。

我们选择抛出一个异常,因为显然不能仅通过UML图来确定方法的主体。 因此,我们创建了一个throw语句和一个NotImplementedException类型的新对象。 这也使我们可以为该方法添加有意义的主体。 如果使用格式化程序,则无论如何都应添加一个主体,因为否则它将无法创建正确的方法:将没有主体或花括号。

场的产生

svd = svd.Add(SyntaxFactory.VariableDeclarator(currentField.Name));

“字段”的情况比较容易,因为“方法”和唯一真正的新事物是在第12行,在这里我们使用一种方法从解析器填充的字符串中解析类型。

localNamespace = localNamespace.AddMembers(localClass);
  cu = cu.AddMembers(localNamespace);
 
  AdhocWorkspace cw = new AdhocWorkspace();
  OptionSet options = cw.Options;
  cw.Options.WithChangedOption(CSharpFormattingOptions.IndentBraces, true);
  SyntaxNode formattedNode = Formatter.Format(cu, cw, options);
 
  formattedNode.WriteTo(writer);            
}

Generate方法的末尾,我们添加了for循环创建的类,并使用Formatter 。 请注意, cu是我们在此方法开始时创建的CompilationUnitSyntax

这个例子的局限性

未显示单元测试,因为它们不包含任何值得注意的内容,尽管我不得不说Sprache确实很容易测试,这是一件好事。 如果您运行该程序,则会发现生成的代码是正确的,但仍然缺少某些内容。 它缺少一些必要的using指令,因为我们无法仅从UML图开始检测到它们。 在现实情况中,有许多文件和类,并且没有原始源代码,您可以事先确定程序集,然后可以使用反射来找到它们的名称空间。 同样,我们显然没有实现PlantUML的许多功能,例如类之间的关系,因此请记住这一点。

结论

使用Roslyn进行代码生成并不困难,但是它需要确切地知道您在做什么。 最好事先了解正在生成的代码,否则您将不得不考虑每种可能的情况,这将使每一步都很难完成。 我认为它最适合特定场景和短代码,对于它们来说可能会非常有用。 在这种情况下,只要您不改变工具或工作习惯,就可以在很短的时间内创建对您的项目或自己有用且富有成效的工具,并从中受益。 例如,如果您是一名教授,则可以创建一个自动代码生成器,以在真正的C#中转换简短算法的伪代码。 如果您考虑一下,那么这种复杂性是一件好事,否则,如果任何人都可以从头开始生成整个程序,那么美国程序员将失去工作。

您可能会认为对这样的项目使用Sprache可能不是一个好主意,但实际上它是解析单行的好工具。 尽管存在局限性,但这种方法使在短时间内完成某些工作变得容易得多,而不必等待为“真实”解析器创建完整的语法。 在我看来,对于代码生成最有用的情况,特定的场景等,这实际上是最好的方法,因为它使您可以轻松地选择要使用的部分,而跳过其余部分。

翻译自: https://www.javacodegeeks.com/2017/01/code-generation-roslyn-skeleton-class-uml.html

生成项目有roslyn

 类似资料: