Visual Studio 2015 から追加された .NET Compiler Platform(Roslyn)を使って、ASP.NET Web API 向けの Code Analyzer を作ってみました。
環境構築
Roslyn で Code Analyzer を開発するには、Visual Studio のオプション機能と .NET Compiler Platform SDK をインストールする必要があります。Build Insider の記事が分かりやすいので、こちらを参照してください。
www.buildinsider.net
SDK と一緒に、Syntax Visualizer というツールもインストールされます。SyntaxTree をリアルタイムに表示してくれるので、実装する際に役立ちます。
プロジェクトの作成
開発環境が整うと、Visual Studio のプロジェクト テンプレートの Extensibility に Roslyn 用のテンプレートが表示されます。
「Analyzer with Code Fix(NuGet + VSIX)」を選択して作成します。Analyzer プロジェクトには、クラス名に小文字が含まれていたら警告を表示するサンプルが入っています。Analyzer.Vsix プロジェクトをスタートアップにしてデバッグ実行すると、別の Visual Studio が起動するので、そこで開いたプロジェクトでコード解析とリファクタリングのコードをデバッグできます。
コード解析
コードを解析するには、DiagnosticAnalyzer クラスを継承したクラスを作成します。ID やタイトルなどのルールを設定した後、Initialize() メソッドの実装が肝です。
- RegisterXXX() メソッドがたくさんあるけど、どれを使えばいいのか?
- Analyze () メソッドの中で、どうやってコードを解析するのか?
Roslyn の敷居が高いと思うのは、これらに関するドキュメントが少なく、何をどう書けば分からないことです。
まずは、neue さんのブログを読みましょう。
neue cc - VS2015のRoslynでCode Analyzerを自作する(ついでにUnityコードも解析する)
次に、自分がやりたいことに近いサンプルコードを見つける。そして、Syntax Visualizer でひたすら頑張る。これしかなさそうです。
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ApiAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "ApiAnalyzerId";
internal const string Title = "POST メソッドで HttpStatusCode.NoContent(204)が返されています。";
internal const string MessageFormat = "POST メソッドで HttpStatusCode.NoContent(204)が返されています。";
internal const string Category = "Usage";
internal static DiagnosticDescriptor Rule = new DiagnosticDescriptor(
DiagnosticId,
Title,
MessageFormat,
Category,
DiagnosticSeverity.Warning, isEnabledByDefault: true);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
{
get
{
return ImmutableArray.Create(Rule);
}
}
public override void Initialize(AnalysisContext context)
{
context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.ClassDeclaration);
}
static void Analyze(SyntaxNodeAnalysisContext context)
{
var syntax = (ClassDeclarationSyntax)context.Node;
if (syntax.BaseList == null)
{
return;
}
if (!syntax.BaseList.Types.Any(x => x.GetFirstToken().ValueText == "ApiController"))
{
return;
}
var methods = syntax.Members.OfType<MethodDeclarationSyntax>().
Where(x => (x.Identifier.ValueText.StartsWith("Post") ||
x.AttributeLists.Any(a => a.Attributes.Any(s => s.Name.ToString() == "HttpPost"))));
methods = methods.Where(x => (x.ReturnType as PredefinedTypeSyntax) != null &&
(x.ReturnType as PredefinedTypeSyntax).IsKind(SyntaxKind.VoidKeyword));
foreach (var method in methods)
{
var diagnostic = Diagnostic.Create(Rule, method.GetLocation());
context.ReportDiagnostic(diagnostic);
}
}
}
解析ロジックにコメントを入れてあります。こんな感じで警告を出したいコードを見つけることができます。この Analyzer を動かすと警告が表示されます。
リファクタリング
コードをリファクタリングするには、CodeFixProvider クラスを継承したクラスを作成します。CodeAction.Create() メソッドの引数に渡す ReplaceToHttpStatusCodeCreated() メソッドの実装が肝です。コード解析と同じく、試行錯誤しながら頑張るしかなさそうです。
[ExportCodeFixProvider("ApiCodeFixProvider", LanguageNames.CSharp), Shared]
public class ApiCodeFixProvider : CodeFixProvider
{
internal const string Title = "HttpStatusCode.Created(201)を返すように変更する。";
public override ImmutableArray<string> FixableDiagnosticIds
{
get
{
return ImmutableArray.Create(ApiAnalyzer.DiagnosticId);
}
}
public sealed override FixAllProvider GetFixAllProvider()
{
return WellKnownFixAllProviders.BatchFixer;
}
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
var diagnostic = context.Diagnostics.First();
var diagnosticSpan = diagnostic.Location.SourceSpan;
var method = (MethodDeclarationSyntax)root.FindNode(diagnosticSpan);
var codeAction = CodeAction.Create(Title, x => ReplaceToHttpStatusCodeCreated(context.Document, root, method, x), nameof(ApiCodeFixProvider));
context.RegisterCodeFix(codeAction, diagnostic);
}
static Task<Document> ReplaceToHttpStatusCodeCreated(Document document, SyntaxNode root, MethodDeclarationSyntax method, CancellationToken cancellationToken)
{
var newReturnType = SyntaxFactory.ParseTypeName("HttpResponseMessage").WithTrailingTrivia(SyntaxFactory.Space);
var expression = SyntaxFactory.ParseExpression("new HttpResponseMessage(HttpStatusCode.Created)").WithLeadingTrivia(SyntaxFactory.Space);
var returnStatement = SyntaxFactory.ReturnStatement(SyntaxFactory.Token(SyntaxKind.ReturnKeyword), expression, SyntaxFactory.Token(SyntaxKind.SemicolonToken));
var oldBody = method.Body;
var statements = oldBody.Statements.Where(x => x.GetType() != typeof(ReturnStatementSyntax)).ToList();
var syntaxListStatements = new SyntaxList<StatementSyntax>();
syntaxListStatements = statements.Aggregate(syntaxListStatements, (current, syntaxListStatement) => current.Add(syntaxListStatement));
syntaxListStatements = syntaxListStatements.Add(returnStatement);
var newBody = SyntaxFactory.Block(SyntaxFactory.Token(SyntaxKind.OpenBraceToken), syntaxListStatements, SyntaxFactory.Token(SyntaxKind.CloseBraceToken));
var newMethod = SyntaxFactory.MethodDeclaration(
method.AttributeLists,
method.Modifiers,
newReturnType,
method.ExplicitInterfaceSpecifier,
method.Identifier,
method.TypeParameterList,
method.ParameterList,
method.ConstraintClauses,
newBody,
method.SemicolonToken);
var newRoot = root.ReplaceNode(method, newMethod);
var newDocument = document.WithSyntaxRoot(newRoot);
return Task.FromResult(newDocument);
}
}
置換ロジックにはコメントを入れてあります。この CodeFixProvider を動かすとリファクタリングできます。
プレビューでリファクタリング内容を確認したら、コードを置換することができます。