语义分析入门

本教程假定你熟悉语法 API。 语法分析入门一文提供了详细介绍。

在本教程中,你将了解“符号”和“绑定 API”。 这些 API 提供关于程序语义含义的信息。 它们帮助你就程序中的符号所代表的类型进行问答。

需要安装 .NET Compiler Platform SDK

安装说明 - Visual Studio 安装程序

在“Visual Studio 安装程序”中查找“.NET Compiler Platform SDK”有两种不同的方法

使用 Visual Studio 安装程序进行安装 - 工作负荷视图

Visual Studio 扩展开发工作负荷中不会自动选择 .NET Compiler Platform SDK。 必须将其作为可选组件进行选择。

  1. 运行“Visual Studio 安装程序”
  2. 选择“修改”
  3. 检查“Visual Studio 扩展开发”工作负荷
  4. 在摘要树中打开“Visual Studio 扩展开发”节点
  5. 选中“.NET Compiler Platform SDK”框。 将在可选组件最下面找到它。

还可使“DGML 编辑器”在可视化工具中显示关系图

  1. 在摘要树中打开“单个组件”节点
  2. 选中“DGML 编辑器”框

使用 Visual Studio 安装程序进行安装 - 各组件选项卡

  1. 运行“Visual Studio 安装程序”
  2. 选择“修改”
  3. 选择“单个组件”选项卡
  4. 选中“.NET Compiler Platform SDK”框。 将在“编译器、生成工具和运行时”部分最上方找到它。

还可使“DGML 编辑器”在可视化工具中显示关系图

  1. 选中“DGML 编辑器”框。 将在“代码工具”部分下找到它

了解编译和符号

随着 .NET 编译器 SDK 的使用越来越多,你会越来越熟悉语法 API 和语义 API 之间的区别。 “语法 API”使你可以看到程序的结构。 但是经常会需要更多关于程序的语义或含义的信息。 虽然可以独立地对松散的代码文件或 Visual Basic 或 C# 代码的代码片段进行语法上的分析,但是凭空提出类似“这是什么类型的变量?”这样的问题毫无意义。 类型名称的含义可能取决于程序集引用、命名空间导入或其他代码文件。 使用语义 API 回答这些问题,特别是 Microsoft.CodeAnalysis.Compilation 类。

Compilation 实例类似于编译器所看见的单个项目,且代表编译 Visual Basic 或 C# 程序所需的一切。 编译包括一组要编译的源文件、程序集引用和编译器选项。 可以使用此上下文中所有的其他信息来推断代码的含义。 Compilation 允许你查找“符号” - 类似名称和其他表达式引用的类型、命名空间、成员和变量的实体。 将名称和表达式与“符号”进行关联的过程被称为“绑定”

Microsoft.CodeAnalysis.SyntaxTree 类似,Compilation 是一个带有语言特定派生类的抽象类。 在创建编译实例时,必须在 Microsoft.CodeAnalysis.CSharp.CSharpCompilation(或 Microsoft.CodeAnalysis.VisualBasic.VisualBasicCompilation)类上调用工厂方法。

查询符号

在本教程中,你会再次看到“Hello World”程序。 这次你将在该程序中查询符号,以理解这些符号所代表的类型。 在命名空间中查询类型,并学习如何查找类型上可用的方法。

可以在我们的 GitHub 存储库中看到此示例的已完成代码。

注意

语法树类型使用继承描述不同的语法元素,这些语法元素在程序中的不同位置生效。 使用这些 API 通常意味着将属性或集合成员强制转换为特定的派生类型。 在以下示例中,作业和强制转换分别是独立的语句,采用显式类型化变量。 你可以读取代码以查看 API 的返回类型以及所返回对象的运行时类型。 在实践中,更常见的是使用隐式类型化变量并靠 API 名称来描述要检查的对象的类型。

新建 C#“独立代码分析工具”项目 :

  • 在 Visual Studio 中,选择“文件” >“新建” >“项目” ,显示新建项目对话框。
  • 在“Visual C#” >“扩展性” 下,选择“独立代码分析工具” 。
  • 将项目命名为“SemanticQuickStart”并单击“确定”

你将分析之前展示过的基本“Hello World!”程序。 为 Hello World 程序添加文本,作为 Program 类中的常量:

        const string programText =
@"using System;
using System.Collections.Generic;
using System.Text;

namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(""Hello, World!"");
        }
    }
}";

下一步,添加以下代码,为 programText 常量中的代码文本生成语法树。 将下面这行代码添加到 Main 方法中:

SyntaxTree tree = CSharpSyntaxTree.ParseText(programText);

CompilationUnitSyntax root = tree.GetCompilationUnitRoot();

接下来,从已创建的树生成 CSharpCompilation。 “Hello World”示例依赖于 StringConsole 类型。 需要引用在编译中声明这两种类型的程序集。 将下面这行代码添加到 Main 方法以创建语法树的编译,包括对相应程序集的引用:

var compilation = CSharpCompilation.Create("HelloWorld")
    .AddReferences(MetadataReference.CreateFromFile(
        typeof(string).Assembly.Location))
    .AddSyntaxTrees(tree);

CSharpCompilation.AddReferences 方法将引用添加到编译。 MetadataReference.CreateFromFile 方法加载程序集作为引用。

查询语义模型

如果有 Compilation,你可以向它查询 Compilation 所包含的任何 SyntaxTreeSemanticModel。 你可将语义模型看作通常从智能感知中获取的所有信息的来源。 SemanticModel 可回答“此位置中范围内的名称是什么?”、“可通过此方法访问哪些成员?”、“此文本块中使用了什么变量?”和“此名称/表达式指的是什么?”等问题。添加此声明以创建语义模型:

SemanticModel model = compilation.GetSemanticModel(tree);

绑定名称

CompilationSyntaxTree 创建 SemanticModel。 创建模型后,你可以查询以找到第一个 using 指令,并检索 System 命名空间的符号信息。 将这两行代码添加到 Main 方法以创建语义模型,并检索第一个 using 语句的符号:

// Use the syntax tree to find "using System;"
UsingDirectiveSyntax usingSystem = root.Usings[0];
NameSyntax systemName = usingSystem.Name;

// Use the semantic model for symbol information:
SymbolInfo nameInfo = model.GetSymbolInfo(systemName);

上述代码示例演示如何绑定第一个 using 指令中的名称以检索 System 命名空间的 Microsoft.CodeAnalysis.SymbolInfo。 上述代码还说明,使用语法模型查找代码的结构;使用语义模型理解它的含义System语法模型在 using 语句中找到字符串 System语义模型具有关于 命名空间中所定义类型的全部信息。

可以从 SymbolInfo 对象获取使用 SymbolInfo.Symbol 属性的 Microsoft.CodeAnalysis.ISymbol。 此属性返回此表达式所引用的符号。 对于不引用任何内容的表达式(例如数字参数),此属性为 null。 若 SymbolInfo.Symbol 不为 null,ISymbol.Kind 表示符号的类型。 在此示例中,ISymbol.Kind 属性是 SymbolKind.Namespace。 将以下代码添加到 Main 方法。 它检索 System 命名空间的符号,然后将 System 命名空间中声明的所有子命名空间显示出来:

var systemSymbol = (INamespaceSymbol?)nameInfo.Symbol;
if (systemSymbol?.GetNamespaceMembers() is not null)
{
    foreach (INamespaceSymbol ns in systemSymbol?.GetNamespaceMembers()!)
    {
        Console.WriteLine(ns);
    }
}

运行该程序,然后应看到以下输出:

System.Collections
System.Configuration
System.Deployment
System.Diagnostics
System.Globalization
System.IO
System.Numerics
System.Reflection
System.Resources
System.Runtime
System.Security
System.StubHelpers
System.Text
System.Threading
Press any key to continue . . .

注意

该输出不包含 System 命名空间的每个子命名空间。 它显示此编译中存在的每个命名空间,只引用声明了 System.String 的程序集。 此编译不了解其他程序集中所声明的任何命名空间

绑定表达式

上述代码显示如何通过绑定到名称来查找符号。 在可以绑定的 C# 程序中还有其他不是名称的表达式。 为演示此功能,我们绑定到简单的字符串文本。

“Hello World”程序包含 Microsoft.CodeAnalysis.CSharp.Syntax.LiteralExpressionSyntax,即向控制台显示的“Hello, World!”字符串。

通过在程序中查找单个字符串文本,可以找到“Hello, World!”字符串。 找到语法节点后,从语义模型中获取该节点的类型信息。 将以下代码添加到 Main 方法:

// Use the syntax model to find the literal string:
LiteralExpressionSyntax helloWorldString = root.DescendantNodes()
.OfType<LiteralExpressionSyntax>()
.Single();

// Use the semantic model for type information:
TypeInfo literalInfo = model.GetTypeInfo(helloWorldString);

Microsoft.CodeAnalysis.TypeInfo 结构包括 TypeInfo.Type 属性,此属性可启用对关于文本类型的语义信息的访问。 在此例中为 string 类型。 添加将此属性分配至本地变量的声明:

var stringTypeSymbol = (INamedTypeSymbol?)literalInfo.Type;

要完成本教程,让我们生成 LINQ 查询,该查询会创建一个在 string 类型上声明并返回 string 的所有公共方法的序列。 此查询比较复杂,我们将逐行生成,然后将其重新构造为单个查询。 此查询的源是 string 类型上所声明全部成员的序列:

var allMembers = stringTypeSymbol?.GetMembers();

该源序列包含所有成员(包括属性和字段),使用 ImmutableArray<T>.OfType 方法对其进行筛选以查找属于 Microsoft.CodeAnalysis.IMethodSymbol 对象的元素:

var methods = allMembers?.OfType<IMethodSymbol>();

接下来,添加另一个筛选器,只返回这些属于公共方法并返回 string 的方法:

var publicStringReturningMethods = methods?
    .Where(m => SymbolEqualityComparer.Default.Equals(m.ReturnType, stringTypeSymbol) &&
    m.DeclaredAccessibility == Accessibility.Public);

只选择名称属性,且只通过删除任何重载来区分名称:

var distinctMethods = publicStringReturningMethods?.Select(m => m.Name).Distinct();

还可以使用 LINQ 查询语法生成完整查询,然后在控制台中显示所有方法名称:

foreach (string name in (from method in stringTypeSymbol?
                         .GetMembers().OfType<IMethodSymbol>()
                         where SymbolEqualityComparer.Default.Equals(method.ReturnType, stringTypeSymbol) &&
                         method.DeclaredAccessibility == Accessibility.Public
                         select method.Name).Distinct())
{
    Console.WriteLine(name);
}

生成并运行程序。 应会看到以下输出:

Join
Substring
Trim
TrimStart
TrimEnd
Normalize
PadLeft
PadRight
ToLower
ToLowerInvariant
ToUpper
ToUpperInvariant
ToString
Insert
Replace
Remove
Format
Copy
Concat
Intern
IsInterned
Press any key to continue . . .

你已使用语义 API 来查找并显示关于属于此程序的符号的信息。