Roslyn 入门:使用 Roslyn 静态分析现有项目中的代码

Roslyn 入门:使用 Roslyn 静态分析现有项目中的代码

发布于 2018-03-18 12:45 更新于 2018-06-02 01:26

Roslyn 是微软为 C# 设计的一套分析器,它具有很强的扩展性。以至于我们只需要编写很少量的代码便能够分析我们的项目文件。

作为 Roslyn 入门篇文章,你将可以通过本文学习如何开始编写一个 Roslyn 扩展项目,如何开始分析一个解决方案(.sln)中项目(.csproj)的代码文件(.cs)。


本文是 Roslyn 入门系列之一:

如果你希望真实地静态分析一个实际项目,并且理解这样的分析过程是如何进行的(而不只是写个 demo),那么本文的所有内容都将是必要的。

准备工作

为了能够进行后面关键的操作,我们需要先有一个能跑起来的项目。

▲ 在 Visual Studio 新建项目,选择“控制台程序(.NET Framework)”

在目前(2018-06-02 01:26),如果我们需要像本文一样分析现有的解决方案和项目,那么 .NET Framework 是必须的;如果只是分析单个文件,那么也可以选择 .NET Core,参见 Roslyn 入门:使用 .NET Core 版本的 Roslyn 编译并执行跨平台的静态的源码

当然,如果你有一个现成的 .NET Core 项目,可以通过修改 .csproj 文件改成 .NET Framework 的:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <!-- 从 netcoreapp2.0 改成 net471,因为 NuGet 包中的 ValueTuple 与 net47 不兼容,所以只能选择 net471 或以上  -->
    <TargetFramework>net471</TargetFramework>
  </PropertyGroup>
</Project>

现在,我们有了一个可以开始写代码的 Program.cs 文件,接下来就可以正式开始入门了。

安装必要的 NuGet 包

在 NuGet 包管理器中搜索并安装 Microsoft.CodeAnalysis 包 —— 这是一个包含 Roslyn 所有 API 的各种 NuGet 包的合集。

当然,如果你只是做一些特定的事情,当然不需要安装这么全的 NuGet 包,像 Roslyn 静态分析 - 林德熙 的 demo 和 Roslyn 编译与执行 - 吕毅 中的教程就不需要安装所有 NuGet 包。

特别注意!!!如果前面你是通过 .NET Core 项目改过来的,那么还需要额外安装以下三个 NuGet 包,否则运行时会无法打开解决方案和项目

  • Microsoft.Build
  • Microsoft.Build.Tasks.Core
  • System.Threading.Tasks.Dataflow

打开一个解决方案/项目和其中的文件

现在,我们使用这些代码打开解决方案。我以 MSTestEnhancer 为例:

// 打开 MSTestEnhancer(https://github.com/dotnet-campus/MSTestEnhancer/) 解决方案文件。
// 注意这里的 MSBuildWorkspace.Create() 会返回 WorkSpace 的实例。
// 虽然 WorkSpace 是跨平台的,但是 MSBuildWorkspace 仅在 Windows 下可用。
var solution = await MSBuildWorkspace.Create().OpenSolutionAsync(
    @"D:\Developments\Open\MSTestEnhancer\MSTest.Extensions.sln");
    
// 从解决方案中选出 MSTest.Extensions 项目。
var project = solution.Projects.First(x => x.Name == "MSTest.Extensions");

// 从 MSTest.Extensions 项目中选出我们要分析的 ContractTestContext.cs 文件。
// 这里只是一个示例,所以我们只分析一个文件。你可以从 Documents 集合中找出这个项目的所有文件进行分析。
var document = project.Documents.First(x =>
        x.Name.Equals("ContractTestContext.cs", StringComparison.InvariantCultureIgnoreCase));

分析代码

我们要分析的代码大致是这样的:

// 这里是 using,省略。
// 这里是命名空间,省略。
public class ContractTestContext<T>
{
    // 这是代码的细节,省略。
}

现在,我们开始使用 Roslyn API 找出里面的泛型 T

这里,我们必须引入一个概念 —— Syntax Rewriter。

语法重写——Syntax Rewriter

Roslyn 对 C# 代码进行分析的一个非常关键的 API 是 CSharpSyntaxRewriter——这是一个专门用来给你继承的类。CSharpSyntaxRewriter访问者模式中访问者的一个实现,如果你不了解访问者模式,推荐阅读 23种设计模式(9):访问者模式 - CSDN博客 进行了解,否则我们后面的代码你将只能跟着我写,而不能明白其中的含义。

当你阅读到这里时,我开始假设你已经了解了访问者模式了。

我们每个人都可能会写出不同的基于 Roslyn 的分析器,这些分析器通常都会对不同文件的 C# 语法树进行不同的操作;于是,我们通过重写 CSharpSyntaxRewriter 可以实现各种各样不同的操作。在访问者模式中,由于 C# 的语法在一个 C# 版本发布之后就会确定,其中各种各样类型的语法对应访问者模式中的各种不同类型的数据,Roslyn 为我们构建的语法树对应访问者模式中需要访问的庞大的数据结构。由于 Roslyn 的语法树是非常庞大的,以至于对其进行遍历也是一个非常复杂的操作;所以 Roslyn 通过访问者模式为我们封装了这种复杂的遍历过程,我们只需要重写 CSharpSyntaxRewriter 就可以实现对某种特定语法节点的操作。

现在,我们编写一个用于找出泛型参数 T 的 Syntax Rewriter。

class TypeParameterVisitor : CSharpSyntaxRewriter
{
    public override SyntaxNode VisitTypeParameterList(TypeParameterListSyntax node)
    {
        var lessThanToken = this.VisitToken(node.LessThanToken);
        var parameters = this.VisitList(node.Parameters);
        var greaterThanToken = this.VisitToken(node.GreaterThanToken);
        return node.Update(lessThanToken, parameters, greaterThanToken);
    }
}

其实这段代码就是 CSharpSyntaxRewriter 基类中的代码,我把它贴出来可以帮助我们理解它。你也依然需要将他放入到我们的项目中,因为我们接下来的代码就开始要使用它了。

如果你想了解更多语法节点,推荐另一篇入门文章:Roslyn 入门:使用 Visual Studio 的语法可视化(Syntax Visualizer)窗格查看和了解代码的语法树

访问泛型参数

现在,我们继续在之前打开解决方案和项目文件的代码后面增添代码:

// 从我们一开始打开的项目文件中获取语法树。
var tree = await document.GetSyntaxTreeAsync();
var syntax = tree.GetCompilationUnitRoot();

// 使用我们刚刚重写 CSharpSyntaxRewriter 的类来访问语法树。
var visitor = new TypeParameterVisitor();
var node = visitor.Visit(syntax);

// 得到的 node 是新的语法树节点,
// 如果我们在 `TypeParameterVisitor` 中修改了语法树,
// 那么这里就会得到修改后的 node 节点。
// 我们可以通过这个 node 节点做各种后续的操作。

如果我们使用 node 的方式是修改代码,那么可以使用 var text = node.GetText(); 来得到新的语法树生成的代码,使用这段文本替换之前的文本可以达到修改代码的目的。不过,这不是本文的重点,本文的重点依然在入门。

现在,整合以上的三大段代码,你的项目应该能够完整地跑起来了。哪三段?1. 打开项目文件;2. TypeParameterVisitor;3. 访问泛型参数。其中 1 和 3 写在一个方法中,2 是一个新类。

分析这个泛型参数

直到现在,我们所写的任何代码都还只是为了使使用 Roslyn API 的代码能够跑起来,没有进行任何实质上的分析。接下来,我们会修改 CSharpSyntaxRewriter 以进行真正的分析。不过在此之前,我假设上面的代码你是能正常跑起来而且没有错误的。(如果不行,就在下面留言吧!留言有邮件通知的,我会在第一时间回复你。

如果你不了解 Roslyn,强烈建议去 VisitTypeParameterList 重写方法中打一个断点观察 lessThanToken parameters greaterThanToken 这几个实例的含义。lessThanToken 就是 <greaterThanToken 就是 >;而 parameters 是一个泛型参数列表,在这里,是一个 T

现在,我们构造一个自己的泛型参数列表试试,名字不是 T 了,而是 TParameter

var parameters = new SeparatedSyntaxList<TypeParameterSyntax>();
parameters = parameters.Add(SyntaxFactory.TypeParameter("TParameter"));

特别注意:SeparatedSyntaxListAdd 操作不会修改原集合,而是会返回一个新的集合!所以上面 Add 之后的赋值语句不能少!这样的设计应该是为了避免遍历语法树的时候语法树被修改导致遍历不可控。

于是,我们的 TypeParameterVisitor 变成了这样:

class TypeParameterVisitor : CSharpSyntaxRewriter
{
    public override SyntaxNode VisitTypeParameterList(TypeParameterListSyntax node)
    {
        // 构造一个自己的泛型列表,名字改为了 TParameter。
        var parameters = new SeparatedSyntaxList<TypeParameterSyntax>();
        parameters = parameters.Add(SyntaxFactory.TypeParameter("TParameter"));

        // 依然保留之前的更新语法节点的方法。
        // 这样,我们将会在语法树访问结束后得到新的语法树。
        var lessThanToken = this.VisitToken(node.LessThanToken);
        var greaterThanToken = this.VisitToken(node.GreaterThanToken);
        return node.Update(lessThanToken, parameters, greaterThanToken);
    }
}

总结

我们总共编写了两个关键类:

  • Program
    • Main(用于打开项目和文件,并调用 TypeParameterVisitor 遍历语法树) 需要注意,Main 函数只有 C#7.2 及以上才支持 async,如果没有这么高,需要再编写一个新函数,然后在 Main 里面调用它。
  • TypeParameterVisitor
    • VisitTypeParameterList(用于遍历和修改语法树中的泛型参数列表)

以上便是分析和修改 Roslyn 语法树的简单实例了,我将整个 Program.cs 文件贴在下面,以便整体查看。

using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.MSBuild;

namespace Walterlv.Demo.Roslyn
{
    class Program
    {
        static void Main(string[] args)
        {
            RunAsync().Wait();
        }

        private static async Task RunAsync()
        {
            var solution = await MSBuildWorkspace.Create().OpenSolutionAsync(
                @"D:\Developments\Open\MSTestEnhancer\MSTest.Extensions.sln");
            var project = solution.Projects.First(x => x.Name == "MSTest.Extensions");
            var document = project.Documents.First(x =>
                x.Name.Equals("ContractTestContext.cs", StringComparison.InvariantCultureIgnoreCase));

            var tree = await document.GetSyntaxTreeAsync();
            var syntax = tree.GetCompilationUnitRoot();

            var visitor = new TypeParameterVisitor();
            var node = visitor.Visit(syntax);

            var text = node.GetText();
            File.WriteAllText(document.FilePath, text.ToString());
        }
    }

    class TypeParameterVisitor : CSharpSyntaxRewriter
    {
        public override SyntaxNode VisitTypeParameterList(TypeParameterListSyntax node)
        {
            var syntaxList = new SeparatedSyntaxList<TypeParameterSyntax>();
            syntaxList = syntaxList.Add(SyntaxFactory.TypeParameter("TParameter"));

            var lessThanToken = this.VisitToken(node.LessThanToken);
            var greaterThanToken = this.VisitToken(node.GreaterThanToken);
            return node.Update(lessThanToken, syntaxList, greaterThanToken);
        }
    }
}

参考资料

本文会经常更新,请阅读原文: https://walterlv.com/post/analysis-code-of-existed-projects-using-roslyn.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 吕毅 (包含链接: https://walterlv.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 (walter.lv@qq.com)

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏MasiMaro 的技术博文

Windows资源

Windows资源是一种二进制数据,由链接器链接进程序成为程序的一部分,通过资源的方式可以很方便的对应用程序进行扩展。在Windows中资源可以是系统自定义的,...

21010
来自专栏me的随笔

.NET Core中的包、元包与框架

.NET Core是一个由NuGet包组成的平台。一些产品受益于细粒度包的定义,也有一些受益于粗粒度包的定义。为了适应这种二重性,.NET Core平台被分为一...

13540
来自专栏林德熙的博客

WPF Frame 的 DataContext 不能被 Page 继承

如果使用下面的代码启动另一个软件,那么在启动的软件路径不存在时,就会出现异常System.ComponentModel.Win32Exception,没有其他信...

21310
来自专栏逸鹏说道

图片在保存的时候===》出现这个异常:GDI+ 中发生一般性错误

异常处理汇总-后端系列 http://www.cnblogs.com/dunitian/p/4523006.html ? 一般这种情况都是没有权限,比如目录没有...

39480
来自专栏张善友的专栏

Contact Manager Web API 示例[3] 分页和查询(Paging and Querying)

联系人管理器web API是一个Asp.net web api示例程序,演示了通过ASP.NET Web API 公开联系信息,并允许您添加和删除联系人,示例地...

22960
来自专栏Coding01

说一说 Laravel 邮件发送流程

我们使用阿里云提供的免费邮,和采用「smtp」驱动,作为测试,参考 .env 配置:

23350
来自专栏『不羁阁』 | 行走少年郎专栏

iOS多线程:『pthread、NSThread』详尽总结

22450
来自专栏程序员互动联盟

【编程基础】跟我学创建Windows动态库

如何简单快速创建Win32平台下的动态链接库?但是有的创建出来用C/C++调用时没有问题,但是在其它编程语言调用时可能会出现问题,下面我们就按四个傻瓜式的步骤创...

30590
来自专栏ASP.NETCore

查看.NET Core源代码通过Autofac实现依赖注入到Controller属性

  在之前的文章【ASP.NET Core 整合Autofac和Castle实现自动AOP拦截】中,我们讲过除了ASP.NETCore自带的IOC容器外,如何使...

65170
来自专栏逍遥剑客的游戏开发

C#脚本实践(二): Unity脚本机制分析

24120

扫码关注云+社区

领取腾讯云代金券