[译]立即了解Roslyn:快速提示

作者 : Josh Varty Github 原文地址

使用“区域”Regions

在Rolsyn里有一个有趣的小角落——结构化的 Trivia,请看下面的语法树,它表示一个带有区域(Regins)的简单程序。

class MyClass
{
  #region MyRegion
  #endregion                                                                                                              
}

通常,Roslyn语法树的末端是一些Trivia。但是,在上面的程序中,我们可以看到RegionDirectiveTrivia 节点有一个RegionDirectiveTriviaSyntax 的子语法节点。

那么,我们为什么不尝试访问这些语法节点呢?最初,我尝试了以下方法:

var tree = CSharpSyntaxTree.ParseText(@"
  public class MyClass{
	  #region MyRegion
	  #endregion
  }");

//Try finding them as nodes.
var regionNodes = tree.GetRoot().DescendantNodes().OfType<RegionDirectiveTriviaSyntax>();
//Try finding them as trivia? 
var regionTrivia = tree.GetRoot().DescendantTrivia().OfType<RegionDirectiveTriviaSyntax>();

奇怪的是(至少对我来说),这两种方法都不起作用, 没有返回任何内容。它们也没有抛出异常,也没有指出我的任何错误。感谢@Pilchie指出的问题,后代节点(DescendantNodes)有几个可选参数。

Func<SyntaxNode, bool>descendIntoChildren – 一个决定我们是否应该下放到给定节点的子节点的函数。我们可以使用这个来避免下降到我们不关心的节点。

bool descendantotrivia – 一个简单的布尔值,它指示我们在搜索节点时是否希望下行至Trivia的子节点。

在我们的例子中,我们希望在Trivia中搜索语法节点,因此我们将发出信号。

var tree = CSharpSyntaxTree.ParseText(@"
  public class MyClass{
    #region MyRegion
	  #endregion
  }");

//Descend into all node children
//Descend into all trivia children
var regionNodes = tree.GetRoot().DescendantNodes(descendIntoChildren: null, descendIntoTrivia: true).OfType<RegionDirectiveTriviaSyntax>();

这一次,一切都如我们所期望的那样工作,并且我们可以按预期访问regiondirectiveritiviasyntax节点。

那么,为什么Roslyn在默认情况下避免下行至Trivia呢?我猜是为了性能。Roslyn的大多数用户将寻找节点,如方法、属性和字段。这些都不包含在Syntax Trivia中,因此考虑它们的子级将浪费CPU效率。

这是在处理结构化Trivia时需要注意的一点,并且不是很明显。


字段和符号 Fields and Symbols

我见过有人在使用RoslynNET时遇到的一个反复出现的问题——使用字段和符号。请先思考下面的:

class MyClass
{
    int myField = 0;
    public int MyProperty {get; set;}
    public void MyMethod() { }
}

上面的程序由一个ClassDecalationSyntax组成,它带有子节点:FieldDeclarationSyntax、PropertyDeclarationSyntax和MethodDeclarationSyntax。

在以前的博客文章中,我们讨论了如何使用SemanticModel.GetDeclaredSymbol (SyntaxNode)检索声明语法片段的符号。因此,如果我们能用同样的方法得到我们字段、属性和方法的符号,这将是有意义的。

通常会尝试以下操作:

var tree = CSharpSyntaxTree.ParseText(@"
    class MyClass
    {
        int myField = 0;
        public int MyProperty {get; set;}
        public void MyMethod() { }
    }");

var Mscorlib = PortableExecutableReference.CreateFromAssembly(typeof(object).Assembly);
var compilation = CSharpCompilation.Create("MyCompilation",
    syntaxTrees: new[] { tree }, references: new[] { Mscorlib });
var model = compilation.GetSemanticModel(tree);

//Get declarations
var property = tree.GetRoot().DescendantNodes().OfType<PropertyDeclarationSyntax>().Single();
var method = tree.GetRoot().DescendantNodes().OfType<PropertyDeclarationSyntax>().Single();
var field = tree.GetRoot().DescendantNodes().OfType<FieldDeclarationSyntax>().Single();
//Get symbols
var propertySymbol = model.GetDeclaredSymbol(property);
var methodSymbol = model.GetDeclaredSymbol(method);
var fieldSymbol = model.GetDeclaredSymbol(field);

不过,这里有个问题。fieldSymbol为空!我们的方法适用于方法和属性,但不适用于字段。原因其实很简单:字段可以包含多个符号。例如:

class MyClass
{
    int myField1, myField2, myField3;
}

当我们查看语法树(我忽略了标记和Trivia)时,这就更加清楚了。

上面的FieldDeclarationSyntax可以返回什么符号?为了访问这些符号,我们改为查看字段中的各个变量,如下所示:

var tree = CSharpSyntaxTree.ParseText(@"
  class MyClass
  class MyClass
  {
  	int myField1, myField2, myField3;
  }");

var Mscorlib = PortableExecutableReference.CreateFromAssembly(typeof(object).Assembly);
var compilation = CSharpCompilation.Create("MyCompilation",
syntaxTrees: new[] { tree }, references: new[] { Mscorlib });
var model = compilation.GetSemanticModel(tree);
var field = tree.GetRoot().DescendantNodes().OfType<FieldDeclarationSyntax>().Single();

foreach (var variable in field.Declaration.Variables)
{
	//Now we can access each of the symbols within the field
	var fieldSymbol = model.GetDeclaredSymbol(variable);
}

事实证明,字段并不是唯一不能转换成符号的“特殊语法”。如果你感兴趣的话,你可以在Roslyn网上的参考资料中看到。他们是:

  • 全局语句 Global Statements —全局语句不声明任何内容,即使它们继承自MemberDeclarationSyntax。
  • 不完整成员 IncompleteMembers  -不完整成员不声明任何符号。
  • 事件字段声明 Event Field Declaration –可以包含多个变量声明符。GetDeclaredSymbol应该直接调用它们(声明器)。
  • 字段声明 Field Declaration –可以包含多个变量声明符。GetDeclaredSymbol应该直接调用它们(声明器)。

八月份的时候我被这个问题困扰了,我向Roslyn团队提交了一个issue。我原本以为在这种情况下应该抛出一个异常,但后来我改变了想法——与之相反,我认为需要有更清晰的关于GetDeclaredSymbol()函数的文档。对于某些人来说,创建一个分析器来检测人们何时这样做并警告他们也是合适的。


使用 nameof

伴随着Roslyn团队努力的确定语法和语义,当前nameof操作符已经经历了五次迭代,现在已经完成了运算符nameof的设计,我们可以看一些简单的例子。

在C#中,nameof是一个上下文关键字。这意味着无法将nameof关键字与恰好名为nameof的方法的调用区分开。

Lucian Wischik阐述:

In C#, nameof is stored in a normal InvocationExpressionSyntax node with a single argument. That is because in C# ‘nameof’ is a contextual keyword, which will only become the “nameof” operator if it doesn’t already bind to a programmatic symbol named “nameof”

在C#中,nameof存储在带有单个参数的普通调用表达式syntax节点中。这是因为在C中,“nameof”是一个上下文关键字,只有当它还没有绑定到名为“nameof”的编程符号时,它才会成为“nameof”运算符。

识别 nameof 表达式

这意味着我们只能在语义层识别表达式的名称。我们通过查找所有不绑定到任何符号的“nameof”调用来实现。这些调用也必须是独立的(即,不属于MyClass.nameof()之类的成员访问的一部分)

var tree = CSharpSyntaxTree.ParseText(@"
class C
{
    void M()
    {
        int variable = 0;
        //Does not bind to a symbol (as there is no class called MissingClass)
        //but it is not a true nameof expression
        MissingClass.nameof(x);
    }
}");


var Mscorlib = PortableExecutableReference.CreateFromAssembly(typeof(object).Assembly);
var compilation = CSharpCompilation.Create("MyCompilation",
    syntaxTrees: new[] { tree }, references: new[] { Mscorlib });
var model = compilation.GetSemanticModel(tree);

var nameofInvocations = tree.GetRoot().DescendantNodes().OfType<InvocationExpressionSyntax>();
var validNameOfs = nameofInvocations.Where(n => n.Ancestors().OfType<MemberAccessException>().Count() == 0);

foreach (var validNameOfSyntax in validNameOfs)
{
    if (model.GetSymbolInfo(validNameOfSyntax).Symbol == null)
    {
    	//validNameOfSyntax is the nameof operator
        Console.WriteLine("We've found a nameof!");
    }
}

与所有上下文关键字一样,使用它有点麻烦。 但这就是我们为向后兼容所付出的代价。

创建 nameof 表达式

以前版本的Roslyn API允许直接创建nameof 的 expressionsyntax。现在我们必须创建一个 带有标识符“nameof”的 InvocationExpressionSyntax 。

例如,我们可以生成以下表达式名称:

 string result = nameof(result); 
var nameOfExpression = SyntaxFactory.LocalDeclarationStatement(
    SyntaxFactory.VariableDeclaration(
        SyntaxFactory.PredefinedType(
            SyntaxFactory.Token(
                SyntaxKind.StringKeyword)))
    .WithVariables(
        SyntaxFactory.SingletonSeparatedList<VariableDeclaratorSyntax>(
            SyntaxFactory.VariableDeclarator(
                SyntaxFactory.Identifier(
                    @"result"))
            .WithInitializer(
                SyntaxFactory.EqualsValueClause(
                    SyntaxFactory.InvocationExpression(
                        SyntaxFactory.IdentifierName(
                            @"nameof"))
                    .WithArgumentList(
                        SyntaxFactory.ArgumentList(
                            SyntaxFactory.SingletonSeparatedList<ArgumentSyntax>(
                                SyntaxFactory.Argument(
                                    SyntaxFactory.IdentifierName(
                                        @"result"))))))))));

附带说明:作为一个普通人,我不知道如何使用SyntaxFactory API。通过使用Kirill Osenkov的Roslyn Quoter神奇工具,我生成了上面的代码。

重要的收获是(在语法级别上)表达式的名称与常规调用没有区别。

 


查看评论

不要信任 SyntaxNode.ToFullString

在Code Connect 代码连接中,我们一直在致力于一个项目,该项目重写用户的代码,然后编译重写的解决方案。在不付出太多代价的情况下,它实际上充当了一个记录器,可以截取所有变量赋值。

所以下面的代码:

class MyClass
{
    void MyMethod()
    {
        int x = 5;                                                                               
    }
}

重写为:

class MyClass
{
    void MyMethod()
    {
        int x = LogValue("x", 5);
    }
    public static T LogValue<T>(string name, T value)                                                                        
    {
        //Write to file
        //...
        return value;
    }
}

在某个时候,我们决定不再希望将LogValue方法直接注入我们正在检测的代码中。 我们将其放置在一个完全不同的命名空间中:CodeConnect.Instrumentation。

这是我们最初创建对LogValue的调用的方式:

var newNode = SyntaxFactory.InvocationExpression(
                SyntaxFactory.IdentifierName(                                                           
                    "LogValue"))
    .WithArgumentList(
    //...
                    );

所以我们想合并新的名称空间如下:

ar newNode = SyntaxFactory.InvocationExpression(
                SyntaxFactory.IdentifierName(
                    "CodeConnect.Instrumentation.LogValue"))
    .WithArgumentList(
    //...
                    );

乍一看,所有都查出来了。在这个节点上调用.ToFullString()显示了一个正确的调用:

CodeConnect.Instrumentation.LogValue("x", 5);

但是,请再尝试尝试,我们可能无法编译我们的代码。不断出现错误,告诉我们CodeConnect.Instrumentation类型不存在:

CS0103 at line 12 (character 19): The name 'CodeConnect.Instrumentation' does not exist in the current context.

我们检查了引用,检查了重写的解决方案,并且至少检查了12次拼写。 一切都确认了了。 最终,我们做了经典的“哪些更改破坏了此功能?”的 Git 的历史回顾 。(注意:如果我初次编写重写器时编写了更完整的单元测试,那么我们会更快地发现它!)

我们意识到是重写器的改变导致了这个问题,我们花了一些时间才真正意识到出了什么问题。让我们再一次使用了真正的语法可视化工具(现已与.NET编译工具SDK打包在一起)并向我们展示了有效调用CodeConnect.Instrumentation.LogValue(“x”,5)的语法树。 看起来像:

看完上面的树后,我们意识到应该创建一个SimpleMemberAccessExpressions链( a chain of ),而不是一个名称为“ CodeConnect. Instrumentation.LogValue”的调用表达式 InvocationExpression。 当绑定器将此调用绑定到符号时,它失败了,因为无法使用此名称声明任何方法。 我们创建的树无效,并且永远不会从用户的源代码中解析出来。

这是创建或重写语法树时要理解的一个关键点:

No one will stop you from creating invalid syntax trees.
没有人会阻止您创建无效的语法树。

或请了解Kevin Pilch-Bisson的Stack Overflow上的:

> The Roslyn Syntax construction API does not guarantee that you can only build valid programs.

Roslyn语法构造API不能保证只能生成有效的程序。

我们可以走向逻辑极端——并创建没有任何意义的树。 例如,我们可以创建值为“public void M() { }” 的WhitespaceTrivia。

//Make some impossible non-whitespace trivia
var trivia = SyntaxFactory.SyntaxTrivia(SyntaxKind.WhitespaceTrivia,
@"
public void M() {
}");
var node = SyntaxFactory.ClassDeclaration(" test")
.WithOpenBraceToken(
    SyntaxFactory.Token(SyntaxKind.OpenBraceToken)
    .WithTrailingTrivia(trivia)
    );

Console.WriteLine(node.ToFullString());

上面打印了以下误导性的字符串:

class test{
public void M() {

}}

输出是误导性的,因为它导致读者对语法树必须如何形成做出了假设。在我的经验中,关于语法树的唯一真正的仲裁者是Roslyn Syntax Visualizer,我希望在调试时将其扩展为在内存树中可视化。

请拿走这些:

  • 不要信任.ToString()或.ToFullString ()
  • 了解您可能会意外生成的无效的树
  • 编写测试程序


查看评论

PCL 引用 和 MSBuildWorkspace

2015/07/23编辑:所有这些问题现在应该在最新的Roslyn NuGet程序包中修复了。


连接Visual Studio和Roslyn

今天我在做一个Visual Studio扩展示例,我有以下问题:

给定一个Visual Studio ITextSnapshot或ITextBuffer,如何获取相应的Roslyn文档?

事实证明,有很多扩展方法可以简化这一过程。Microsoft.CodeAnalysis NuGet软件包中没有提供它们,因此您需要手动将它们拉下来:

  • 安装NuGet包EditorFeatures :
 Install-Package Microsoft.CodeAnalysis.EditorFeatures.Text 
  • 包含正确的 using 语句:
 using Microsoft.CodeAnalysis.Text; 

现在,您将能够从ITextBuffer和ITextSnapshot映射回Roslyn的Document,SourceText和Workspace对象。


在Roslyn中启用C# 7.0的功能

截至11月,Roslyn团队之外的个人已经能够构建并修改编译器和语言服务。现在,各种特性分支已经赶上了,我们可以开始为C#使用一些建议的特性。

如果你想了解这些特性,那么我将介绍一些有关二进制文字数字分隔符局部函数的视频。

我还准备了一个关于如何用Roslyn测试C#7功能的视频:

GitHub上当前可用的分支包括:

  • features/Annotated Types
  • features/Nullable Reference Types
  • features/constVar
  • features/local-functions
  • features/multi-Var
  • features/openGenericNameInNameof
  • features/patterns
  • features/privateprotected
  • features/ref-returns
  • features/tuples

/future分支是所有这些功能在接近完成并准备好接受更多反馈审查之后结束的地方。今天(2015年2月9日),这里有二进制文字、数字分隔符和本地函数。

今天,我们将研究构建/ future 分支所需的步骤,并让我们测试一下新功能。

克隆和构建Roslyn

第一步与Roslyn的“在Windows上构建调试和测试”指南中的步骤相同。

  1. 克隆 https://github.com/dotnet/roslyn
  2. 确认/features分支
  3. 从开始菜单运行“Developer Command Prompt for VS2015”
  4. 导航到Git克隆的目录
  5. 在命令提示符下运行Restore.cmd以还原NuGet包。(注意:这有时需要30分钟才能完成,如果没有,可能会冻结)
  6. 在Visual Studio中打开之前在命令行上生成。运行msbuild/v:m/m Roslyn.sln
  7. 打开Roslyn.sln

在Visual Studio中启用C#7功能

  • 导航到CSharpParseOptions.cs并找到IsFeatureEnabled()
  • 强制它 return true 以启用所有可用功能
  • 在解决方案资源管理器中,将VisualStudioSetup项目设置为启动项目,然后按F5运行。
  • Visual Studio的一个新实例将打开,其中包含可在VS中使用的C#7功能。

注意:尽管在编辑器中不会出现错误提示,但在将更改部署到进程外编译器之前,您将无法执行完整的生成。

在进程外编译器中启用C#7功能

要在实验性的Visual Studio中启用完全生成,请执行以下操作:

  1. 进行以上更改。
  2. 将它们部署到CompilerExtension项目。

有了它,您可以测试局部函数,二进制文字和数字分隔符。 您也可以使用类似的方法来尝试其他一些功能分支。


发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注

15 + 18 =