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

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


本文是 Roslyn 入门系列之一:

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

 

准备工作

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


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

在目前({% include date.html date=page.date %}),如果我们需要像本文一样分析现有的解决方案和项目,那么 .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);
}
}
}

参考资料

Roslyn 入门:使用 Roslyn 静态分析现有项目中的代码的更多相关文章

  1. 现有项目中集成Flutter

    本文列举了项目开发使用Flutter会遇到的问题,以及如何使用Flutter module在现有项目中集成Flutter,并对其原理进行了分析. 最近在做的一个商业项目,完全的使用Flutter编写的 ...

  2. React Native 在现有项目中的探路

    移动开发中,native开发性能和效果上无疑是最好的. 但是在众多的情况下,native开发并不是最优的选择.当需求经常改动的时候,当预算有限的时候,当deadline很近的时候,native开发的成 ...

  3. [git]安装git-pylint-commit-hook提高python项目中的代码质量

    什么是'git-pylint-commit-hook' 我在工作中,团队为了保证代码和提高代码的质量,要求每个项目都要求安装git-pylint-commit-hook,它是个钩子,会在你提交代码到本 ...

  4. 吐槽一下项目中的代码坏味道:滥用java常量

    我们的项目中是否充斥着类似以下的代码呢?定义一个专门存放常量的java类(接口),非常多其它类依赖该常量类. public interface IConstant { int ZERO = 0; St ...

  5. 最新广商小助手 项目进展 OpenGL ES 3D在我项目中引用 代码太多只好选重要部分出来

    package com.example.home; import java.io.IOException; import java.io.InputStream; import javax.micro ...

  6. 针对MSHFlexGrid的一系列通用方法-项目中实践代码分享

    1.给MSHFlexGrid填充数据通用方法 '自定义报表填充程序 fgrid Public Function ShowformfData(Resultset As ADODB.Recordset, ...

  7. 使用eslint将项目中的代码修改统一的缩进

    背景 继承了组里师兄师姐写的项目的前端代码,但是是两个人写的,有两格缩进的,有四格缩进的,有字符串外用单引号的,有用双引号的. 于是搜索了一下,可以用eslint强制转化. eslint在github ...

  8. 集成activiti到现有项目中

    1.在lib中添加相关的jar包 2.找到一个activiti.cfg.xml,若是想用现有的数据库需要配置 <?xml version="1.0" encoding=&qu ...

  9. Metalama简介3.自定义.NET项目中的代码分析

    本系列其它文章 使用基于Roslyn的编译时AOP框架来解决.NET项目的代码复用问题 Metalama简介1. 不止是一个.NET跨平台的编译时AOP框架 Metalama简介2.利用Aspect在 ...

随机推荐

  1. Poi中getPhysicalNumberOfCells 与 getLastCellNum的差异

    getPhysicalNumberOfCells 与 getLastCellNum的区别 用org.apache.poi的包做excel导入,无意间发明若是excel文件中有空列,空列后面的数据全部读 ...

  2. ABP 学习问题集锦

    一:Update-Database : 无法将“Update-Database”项识别为 cmdlet.函数.脚本文件或可运行程序的名称的问题 解决: 这是因为没有引用EntityFramework命 ...

  3. day37 爬虫2(web微信、高性能相关、Scrapy)

    s16day37 爬虫2 参考博客:http://www.cnblogs.com/wupeiqi/articles/6229292.html 课堂代码:https://github.com/liyon ...

  4. 让你的ansible飞起来

    一.SSH Multiplexing 1 配置 vim /etc/ssh/ssh_config Host * GSSAPIAuthentication yes # If this option is ...

  5. Java环境搭建---(基础)

    首先下载eclipse开发工具,下载地址:http://www.eclipse.org/downloads/,界面如下: 选择eclipse juno(4.2)的版本进入界面 点击Downloads, ...

  6. centos 7安装jenkins

    1. 安装java yum install java 2. 安装jenkins wget -O /etc/yum.repos.d/jenkins.repo http://pkg.jenkins-ci. ...

  7. Linux下的ASLR(PIE)内存保护机制

    1.1    Linux下的ASLR内存保护机制 1.1.1    Linux下的ASLR工作原理 工作原理与window下的aslr类似 1.1.2 Linux下利用内存地址泄露绕过ASLR ⑴.  ...

  8. 单例类singleton自动释放

    body, table{font-family: 微软雅黑; font-size: 10pt} table{border-collapse: collapse; border: solid gray; ...

  9. scala学习之实现RPC通信

    最近学习scala,个人感觉非常灵活,实现rpc通信非常简单,函数式编程比较烧脑 1.搭建工程 创建scala maven 工程 项目pom文件 <project xmlns="htt ...

  10. PHP工作笔记:离线执行php任务

    直接上代码,主要函数 ignore_user_abort(true);这个函数忽略了终端被关闭(打开的网页被关闭),后面 getfiles()这函数是执行采集任务的自定义函数,后面又配置了下路径打开写 ...