语义分析入门
本教程假定你熟悉语法 API。 语法分析入门一文提供了详细介绍。
在本教程中,你将了解“符号”和“绑定 API”。 这些 API 提供关于程序语义含义的信息。 它们帮助你就程序中的符号所代表的类型进行问答。
需要安装 .NET Compiler Platform SDK:
安装说明 - Visual Studio 安装程序
在“Visual Studio 安装程序”中查找“.NET Compiler Platform SDK”有两种不同的方法:
使用 Visual Studio 安装程序进行安装 - 工作负荷视图
Visual Studio 扩展开发工作负荷中不会自动选择 .NET Compiler Platform SDK。 必须将其作为可选组件进行选择。
- 运行“Visual Studio 安装程序”
- 选择“修改”
- 检查“Visual Studio 扩展开发”工作负荷。
- 在摘要树中打开“Visual Studio 扩展开发”节点。
- 选中“.NET Compiler Platform SDK”框。 将在可选组件最下面找到它。
还可使“DGML 编辑器”在可视化工具中显示关系图:
- 在摘要树中打开“单个组件”节点。
- 选中“DGML 编辑器”框
使用 Visual Studio 安装程序进行安装 - 各组件选项卡
- 运行“Visual Studio 安装程序”
- 选择“修改”
- 选择“单个组件”选项卡
- 选中“.NET Compiler Platform SDK”框。 将在“编译器、生成工具和运行时”部分最上方找到它。
还可使“DGML 编辑器”在可视化工具中显示关系图:
- 选中“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”示例依赖于 String 和 Console 类型。 需要引用在编译中声明这两种类型的程序集。 将下面这行代码添加到 Main
方法以创建语法树的编译,包括对相应程序集的引用:
var compilation = CSharpCompilation.Create("HelloWorld")
.AddReferences(MetadataReference.CreateFromFile(
typeof(string).Assembly.Location))
.AddSyntaxTrees(tree);
CSharpCompilation.AddReferences 方法将引用添加到编译。 MetadataReference.CreateFromFile 方法加载程序集作为引用。
查询语义模型
如果有 Compilation,你可以向它查询 Compilation 所包含的任何 SyntaxTree 的 SemanticModel。 你可将语义模型看作通常从智能感知中获取的所有信息的来源。 SemanticModel 可回答“此位置中范围内的名称是什么?”、“可通过此方法访问哪些成员?”、“此文本块中使用了什么变量?”和“此名称/表达式指的是什么?”等问题。添加此声明以创建语义模型:
SemanticModel model = compilation.GetSemanticModel(tree);
绑定名称
Compilation 从 SyntaxTree 创建 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 来查找并显示关于属于此程序的符号的信息。