Roslyn为现有类添加新方法

10

我正在研究在使用VisualStudioWorkspace更新现有代码的Visual Studio扩展(VSIX)中使用Roslyn编译器的用法。经过最近几天的阅读,似乎有几种方法可以实现这一点... 我只是不确定哪种方法对我来说最好。

好的,假设用户在Visual Studio 2015中打开他们的解决方案。他们点击我的扩展程序,然后通过一个表单告诉我他们想要添加以下方法定义到接口中:

GetSomeDataResponse GetSomeData(GetSomeDataRequest request);

他们还告诉我接口的名称,它是ITheInterface

接口中已经有一些代码:

namespace TheProjectName.Interfaces
{
    using System;
    public interface ITheInterface
    {
        /// <summary>
        ///    A lonely method.
        /// </summary>
        LonelyMethodResponse LonelyMethod(LonelyMethodRequest request);
    }
}

好的,我可以使用以下方法加载接口文档:

Document myInterface = this.Workspace.CurrentSolution?.Projects?
    .FirstOrDefault(p 
        => p.Name.Equals("TheProjectName"))
    ?.Documents?
        .FirstOrDefault(d 
            => d.Name.Equals("ITheInterface.cs"));

那么,现在将我的新方法添加到现有接口中的最佳方式是什么,最好也写在XML注释(三斜杠注释)中?请注意,请求和响应类型(GetSomeDataRequest和GetSomeDataResponse)可能尚不存在。我很新手,如果您可以提供代码示例,那就太棒了。

更新

我决定(可能)最好的方法是简单地注入一些文本,而不是尝试以编程方式构建方法声明。

我尝试了以下内容,但最终遇到了一个我不理解的异常:

SourceText sourceText = await myInterface.GetTextAsync();
string text = sourceText.ToString();
var sb = new StringBuilder();

// I want to all the text up to and including the last
// method, but without the closing "}" for the interface and the namespace
sb.Append(text.Substring(0, text.LastIndexOf("}", text.LastIndexOf("}") - 1)));

// Now add my method and close the interface and namespace.
sb.AppendLine("GetSomeDataResponse GetSomeData(GetSomeDataRequest request);");
sb.AppendLine("}");
sb.AppendLine("}");

检查了一下,一切正常(我的实际代码添加了格式和XML注释,但为了清晰起见删除了)。

因此,知道这些是不可变的,我尝试将其保存如下:

var updatedSourceText = SourceText.From(sb.ToString());
var newInterfaceDocument = myInterface.WithText(updatedSourceText);
var newProject = newInterfaceDocument.Project;
var newSolution = newProject.Solution;
this.Workspace.TryApplyChanges(newSolution);

但这会创建以下异常:

bufferAdapter is not a VsTextDocData 
在 Microsoft.VisualStudio.Editor.Implementation.VsEditorAdaptersFactoryService 中,通过传入的 IVsTextBuffer 缓冲适配器获取适配器。然后通过缓冲适配器获取文档缓冲区,并在 Microsoft.VisualStudio.LanguageServices.RoslynVisualStudioWorkspace 类中打开一个不可见的编辑器 IVisualStudioHostDocument。接下来,在 Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem.DocumentProvider.StandardTextDocument 类中更新文本内容,以及在 Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem.VisualStudioWorkspaceImpl 类中应用文档更改。
最后,将这些更改应用到项目系统和解决方案中,以确保修改生效。

你可能需要通过调用 SourceText.WithChanges(new TextChange(...)) 来更改现有的 SourceText(它附带了其他源文件信息),参见此答案 中的示例。 - m0sa
1个回答

11

如果我是你,我会充分利用Roslyn的所有优势,即我会使用DocumentSyntaxTree而不是处理文件文本(你可以完全不使用Roslyn来处理后者)。

例如:

...
SyntaxNode root = await document.GetSyntaxRootAsync().ConfigureAwait(false);
var interfaceDeclaration = root.DescendantNodes(node => node.IsKind(SyntaxKind.InterfaceDeclaration)).FirstOrDefault() as InterfaceDeclarationSyntax;
if (interfaceDeclaration == null) return;

var methodToInsert= GetMethodDeclarationSyntax(returnTypeName: "GetSomeDataResponse ", 
          methodName: "GetSomeData", 
          parameterTypes: new[] { "GetSomeDataRequest" }, 
          paramterNames: new[] { "request" });
var newInterfaceDeclaration = interfaceDeclaration.AddMembers(methodToInsert);

var newRoot = root.ReplaceNode(interfaceDeclaration, newInterfaceDeclaration);

// this will format all nodes that have Formatter.Annotation
newRoot = Formatter.Format(newRoot, Formatter.Annotation, workspace);
workspace.TryApplyChanges(document.WithSyntaxRoot(newRoot).Project.Solution);
...

public MethodDeclarationSyntax GetMethodDeclarationSyntax(string returnTypeName, string methodName, string[] parameterTypes, string[] paramterNames)
{
    var parameterList = SyntaxFactory.ParameterList(SyntaxFactory.SeparatedList(GetParametersList(parameterTypes, paramterNames)));
    return SyntaxFactory.MethodDeclaration(attributeLists: SyntaxFactory.List<AttributeListSyntax>(), 
                  modifiers: SyntaxFactory.TokenList(), 
                  returnType: SyntaxFactory.ParseTypeName(returnTypeName), 
                  explicitInterfaceSpecifier: null, 
                  identifier: SyntaxFactory.Identifier(methodName), 
                  typeParameterList: null, 
                  parameterList: parameterList, 
                  constraintClauses: SyntaxFactory.List<TypeParameterConstraintClauseSyntax>(), 
                  body: null, 
                  semicolonToken: SyntaxFactory.Token(SyntaxKind.SemicolonToken))
          // Annotate that this node should be formatted
          .WithAdditionalAnnotations(Formatter.Annotation);
}

private IEnumerable<ParameterSyntax> GetParametersList(string[] parameterTypes, string[] paramterNames)
{
    for (int i = 0; i < parameterTypes.Length; i++)
    {
        yield return SyntaxFactory.Parameter(attributeLists: SyntaxFactory.List<AttributeListSyntax>(),
                                                 modifiers: SyntaxFactory.TokenList(),
                                                 type: SyntaxFactory.ParseTypeName(parameterTypes[i]),
                                                 identifier: SyntaxFactory.Identifier(paramterNames[i]),
                                                 @default: null);
    }
}

请注意,这是相当原始的代码,Roslyn API 在分析/处理语法树、获取符号信息/引用等方面非常强大。我建议您查看此页面和此页面以供参考。


1
哇,你当然是对的。以这种方式注入包含许多模板代码行的具体实现的代码文件的想法可能需要一些时间来构建。我猜我可以加载一个字符串模板并解析它到语法树,然后将该“子”树注入到现有树中。只需找出如何做到这一点... - DrGriff
1
这真的帮了我很多。不过由于某些原因,分号没有添加,我还在努力弄清楚原因。 - Arwin
找到了,不知道为什么必须添加 declaration = declaration.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)); 声明。我猜建造者模式有更少的错误? ;) - Arwin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接