Roslyn、Kotlin、Xamarin、リンリンシリーズだけど関連性はない。
ぺやんぐ(@peyangu485)です。
日本語情報少なすぎ問題に対応するため、いくつかの簡単な例をメモ代わりに残しておきます。
基本的にはここを参考にすれば一応はできます。
https://www.buildinsider.net/enterprise/roslynextension/02
他にも参考にさせていただいた記事。
ただ、情報が古いところがあったりするので、まんまだと微妙に動かない部分も……。
この記事も2019年8月時点で動くコードになります。
Roslynについての基本は知ってる前提で書いていきます。
導入方法とかも上の記事に書いてますし。
VS2019では、まだできません。(できませんでした)
なので、VS2017 Communityでやります。
- クラス名で使用出来ない文字を使用した時に警告を出す
- メソッド名で使用出来ない文字を使用した時に警告を出す
- イベント名で使用出来ない文字を使用した時に警告を出す
- 変数名で使用出来ない文字を使用した時に警告を出す
- 変数名予約語チェック
- constチェック
- SyntaxKindについて
- 終わり
クラス名で使用出来ない文字を使用した時に警告を出す
Class名のチェックをしたい時は、「RegisterSymbolAction」を使います。
呼び出すチェックメソッドは「SymbolAnalysisContext」を引数に指定します。
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Collections.Immutable;
using System.Text.RegularExpressions;
namespace Analyzer1
{
/// <summary>
/// クラス名に使用可能な文字をチェックします。
/// 0~9、a~z、A~Z以外を使用した場合は警告を出します。
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class CharactersAvailableClassNamesAnalyzer : DiagnosticAnalyzer
{
// これは出力時に「コード」の部分に表示される内容なので、プロジェクト内で「Hoge0001」みたいなコード体系を決めておくとよい
// ↓のような長ったらしいのは不向き
public const string DiagnosticId = "CharactersAvailableClassNamesAnalyzer";
private const string Category = "Naming";
// 警告なのかエラーなのかインフォメーションなのかのルール決め
// DiagnosticSeverityの値に応じて、出力される種別が変わる
// (2019/8現在)Hidden,Warning,Error,Infoの4種類
private static DiagnosticDescriptor Rule = new DiagnosticDescriptor(
DiagnosticId,
"クラス名に使用できない文字を使用しています。",
"クラス名'{0}'に使用できない文字を使用しています。",
Category,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "クラス名には「0-9,a-z,A-Z」以外使用しないでください。");
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }
public override void Initialize(AnalysisContext context)
{
context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);
}
private static void AnalyzeSymbol(SymbolAnalysisContext context)
{
var symbol = context.Symbol as INamedTypeSymbol;
// Analyzer自体を落とさないためのnullチェック
if (symbol == null)
return;
// クラス名のチェックなので、クラスじゃなければ処理を中断する
if (symbol.TypeKind != TypeKind.Class)
return;
// 中身がある、かつ正規表現でマッチしないものがあれば、警告を出力する。
if (!string.IsNullOrEmpty(symbol.Name) && !Regex.IsMatch(symbol.Name, "^[0-9a-zA-Z]+$"))
{
// エラー情報の作成(適応するルールと対象の名前と対象の場所を設定する)
var diagnostic = Diagnostic.Create(Rule, symbol.Locations[0], symbol.Name);
// 警告として出力する
context.ReportDiagnostic(diagnostic);
}
}
}
}
メソッド名で使用出来ない文字を使用した時に警告を出す
メソッド名をチェックする時は、「RegisterSyntaxNodeAction
呼び出すチェックメソッドは「SyntaxNodeAnalysisContext」を引数に指定します。
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Collections.Immutable;
using System.Text.RegularExpressions;
namespace Analyzer1
{
/// <summary>
/// メソッド名に使用可能な文字をチェックします。
/// 0~9、a~z、A~Z以外を使用した場合は警告を出します。
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class CharactersAvailableMethodNamesAnalyzer : DiagnosticAnalyzer
{
// これは出力時に「コード」の部分に表示される内容なので、プロジェクト内で「Hoge0001」みたいなコード体系を決めておくとよい
// ↓のような長ったらしいのは不向き
public const string DiagnosticId = "CharactersAvailableMethodNamesAnalyzer";
private const string Category = "Naming";
// 警告なのかエラーなのかインフォメーションなのかのルール決め
// DiagnosticSeverityの値に応じて、出力される種別が変わる
// (2019/8現在)Hidden,Warning,Error,Infoの4種類
private static DiagnosticDescriptor Rule = new DiagnosticDescriptor(
DiagnosticId,
"メソッド名に使用できない文字を使用しています。",
"メソッド名'{0}'に使用できない文字を使用しています。",
Category,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "メソッド名には「0-9,a-z,A-Z」以外使用しないでください。");
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }
public override void Initialize(AnalysisContext context)
{
context.RegisterSyntaxNodeAction<SyntaxKind>(AnalyzeSymbol, SyntaxKind.MethodDeclaration);
}
private static void AnalyzeSymbol(SyntaxNodeAnalysisContext context)
{
var syntax = context.Node as MethodDeclarationSyntax;
// Analyzer自体を落とさないためのnullチェック
if (syntax == null)
return;
// メソッド名はIdentifierを見る。
var identifier = syntax.Identifier;
if (identifier != null && !string.IsNullOrEmpty(identifier.Text) && !Regex.IsMatch(identifier.Text, "^[0-9a-zA-Z]+$"))
{
// エラー情報の作成(適応するルールと対象の名前と対象の場所を設定する)
var diagnostic = Diagnostic.Create(Rule, syntax.Identifier.GetLocation(), syntax.Identifier.Text);
// 警告として出力する
context.ReportDiagnostic(diagnostic);
}
}
}
}
イベント名で使用出来ない文字を使用した時に警告を出す
イベント名をチェックする時は、「RegisterSyntaxNodeAction
呼び出すチェックメソッドは「SyntaxNodeAnalysisContext」を引数に指定します。
「EventFieldDeclarationSyntax」は、少し形が違うのでforeach使ってChildNodeを見ます。(なんでなのか、よくわかってない)
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Collections.Immutable;
using System.Text.RegularExpressions;
namespace Analyzer1
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class CharactersAvailableEventNamesAnalyzer : DiagnosticAnalyzer
{
// これは出力時に「コード」の部分に表示される内容なので、プロジェクト内で「Hoge0001」みたいなコード体系を決めておくとよい
// ↓のような長ったらしいのは不向き
public const string DiagnosticId = "CharactersAvailableEventNamesAnalyzer";
private const string Category = "Naming";
// 警告なのかエラーなのかインフォメーションなのかのルール決め
// DiagnosticSeverityの値に応じて、出力される種別が変わる
// (2019/8現在)Hidden,Warning,Error,Infoの4種類
private static DiagnosticDescriptor Rule = new DiagnosticDescriptor(
DiagnosticId,
"イベント名に使用できない文字を使用しています。",
"イベント名'{0}'に使用できない文字を使用しています。",
Category,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "イベント名には「0-9,a-z,A-Z」以外使用しないでください。");
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }
public override void Initialize(AnalysisContext context)
{
context.RegisterSyntaxNodeAction<SyntaxKind>(AnalyzeSymbol, SyntaxKind.EventFieldDeclaration);
}
private static void AnalyzeSymbol(SyntaxNodeAnalysisContext context)
{
var syntax = context.Node as EventFieldDeclarationSyntax;
// Analyzer自体を落とさないためのnullチェック
if (syntax == null)
return;
foreach (var childNode in syntax.ChildNodes())
{
// VariableDeclarationでなければ次のchildNodeへ
if (childNode.Kind() != SyntaxKind.VariableDeclaration)
continue;
var childSyntax = childNode as VariableDeclarationSyntax;
if (childSyntax == null)
break;
if (childSyntax.Variables.Count == 0)
break;
var identifier = childSyntax.Variables[0].Identifier;
if (identifier != null && !string.IsNullOrEmpty(identifier.Text) && !Regex.IsMatch(identifier.Text, "^[0-9a-zA-Z]+$"))
{
// エラー情報の作成(適応するルールと対象の名前と対象の場所を設定する)
var diagnostic = Diagnostic.Create(Rule, identifier.GetLocation(), identifier.Text);
// 警告として出力する
context.ReportDiagnostic(diagnostic);
}
break;
}
}
}
}
変数名で使用出来ない文字を使用した時に警告を出す
変数名をチェックする時は、「RegisterSyntaxNodeAction
呼び出すチェックメソッドは「SyntaxNodeAnalysisContext」を引数に指定します。
定数チェックも入れています。
定数も含めて同じ内容のチェックであれば、不要です。
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Collections.Immutable;
using System.Text.RegularExpressions;
namespace Analyzer1
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class CharactersAvailableVariableNamesAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "CharactersAvailableVariableNamesAnalyzer";
internal const string Category = "CharactersAvailableVariableNamesAnalyzer Category";
private static DiagnosticDescriptor Rule = new DiagnosticDescriptor(
DiagnosticId,
"変数名に使用できない文字を使用しています。",
"変数名'{0}'に使用できない文字を使用しています。",
Category,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "変数名には「0-9,a-z,A-Z,_」以外使用しないでください。");
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }
public override void Initialize(AnalysisContext context)
{
context.RegisterSyntaxNodeAction<SyntaxKind>(Analyze, SyntaxKind.VariableDeclaration);
}
private static void Analyze(SyntaxNodeAnalysisContext context)
{
var syntax = context.Node as VariableDeclarationSyntax;
if (syntax == null || syntax.Variables.Count == 0)
return;
// 定数ならチェック対象外
// メソッド化して外に出すことになると思う。
var field = syntax.Parent as FieldDeclarationSyntax;
if (field == null)
return;
if (field.Modifiers.IndexOf(SyntaxKind.ConstKeyword) != -1)
return;
// static readonly対策
if (field.Modifiers.IndexOf(SyntaxKind.StaticKeyword) != -1
&& field.Modifiers.IndexOf(SyntaxKind.ReadOnlyKeyword) != -1)
return;
var identifier = syntax.Variables[0].Identifier;
if (identifier != null && !string.IsNullOrEmpty(identifier.Text) && !Regex.IsMatch(identifier.Text, "^[0-9a-zA-Z_]+$"))
{
// エラー情報の作成(適応するルールと対象の名前と対象の場所を設定する)
var diagnostic = Diagnostic.Create(Rule, identifier.GetLocation(), identifier.Text);
// 警告として出力する
context.ReportDiagnostic(diagnostic);
}
}
}
}
変数名予約語チェック
変数名が予約語を使用しているかチェックする時は、「RegisterSyntaxNodeAction
呼び出すチェックメソッドは「SyntaxNodeAnalysisContext」を引数に指定します。
予約語はこちらを参照。 https://ufcpp.net/study/csharp/ap_reserved.html
「SyntaxFacts.GetKeywordKind(string)」は、指定した文字列と一致するSyntaxKindの型を返すメソッドです。
予約語の大半がカバーされています。(全部っぽい?ソースがアルファベット順に並んでないから分かりません)
メソッドの実装はこちらです。
http://source.roslyn.io/#Microsoft.CodeAnalysis.CSharp/Syntax/SyntaxKindFacts.cs,839cebe3d0d4c8e2,references
「SyntaxFacts.GetKeywordKind(string)」の戻り値で判断します。
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Analyzer1
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ReservedWordAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "ReservedWordAnalyzer";
internal const string Category = "ReservedWordAnalyzer Category";
private static DiagnosticDescriptor Rule = new DiagnosticDescriptor(
DiagnosticId,
"変数名に予約語は使用できません。",
"変数名'{0}'に予約語は使用できません。",
Category,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "変数名には予約語を使用しないでください。");
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }
public override void Initialize(AnalysisContext context)
{
context.RegisterSyntaxNodeAction<SyntaxKind>(Analyze, SyntaxKind.VariableDeclaration);
}
private void Analyze(SyntaxNodeAnalysisContext context)
{
var syntax = context.Node as VariableDeclarationSyntax;
if (syntax == null || syntax.Variables.Count == 0)
return;
var field = syntax.Parent as FieldDeclarationSyntax;
if (field == null)
return;
var identifier = syntax.Variables[0].Identifier;
if (identifier != null && SyntaxFacts.GetKeywordKind(identifier.Text) != SyntaxKind.None)
{
// エラー情報の作成(適応するルールと対象の名前と対象の場所を設定する)
var diagnostic = Diagnostic.Create(Rule, identifier.GetLocation(), identifier.Text);
// 警告として出力する
context.ReportDiagnostic(diagnostic);
}
}
}
}
constチェック
その変数が定数かどうかの判断をする部分です。
変数名のチェックから抜粋。
var field = syntax.Parent as FieldDeclarationSyntax;
if (field == null)
return;
if (field.Modifiers.IndexOf(SyntaxKind.ConstKeywo != -1)
return;
// static readonly対策
if (field.Modifiers.IndexOf(SyntaxKind.StaticKeywo != -1
&& field.Modifiers.IndexOf(SyntaxKind.ReadOnlyKeyword) != -1)
return;
constの有無は以下のコード。
今回は、constがあればチェックしないようにしたいので、constがあればreturnします。
if (field.Modifiers.IndexOf(SyntaxKind.ConstKeyword) != -1)
return;
FieldDeclarationSyntaxのModifiersの中に「SyntaxKind.ConstKeyword」が存在するかどうかで、constの有無が確認できます。
基本的には定数はこれで問題ないのですが、「static readonly」の考慮が必要です。
static readonlyの有無は以下のコード。
constと同じくチェックしないようにしたいので、static readonlyがあればreturnします。
if (field.Modifiers.IndexOf(SyntaxKind.StaticKeyword) != -1
&& field.Modifiers.IndexOf(SyntaxKind.ReadOnlyKeyword) != -1)
return;
SyntaxKindについて
解析において、重要なSyntaxKindについては、以下のリファレンスを参照してください。 ConstKeywordとかのKeyword系からVariableDeclarationとかのDeclaration系などなど。
http://source.roslyn.io/#Microsoft.CodeAnalysis.CSharp/Syntax/SyntaxKind.cs
これを知っていないとスタート地点にすら立てないです。
見つけるまでは、本当にどこに「const」やら「class」を表すものがどこにあるのか分からなくて困りました。
ちなみに、classかどうか判断するのに使うのは「TypeKind.Class」なので、TypeKindも見ておいた方がいいです。
Roslyn全体のソースコードリファレンスはこちらです。 http://source.roslyn.io/#Microsoft.CodeAnalysis.CSharp
終わり
日本語情報なさすぎて、できあがればそんなに難しくないコードでもうんうんうなされるRoslynちゃんでした。
簡単な例を紹介してみましたが、これでまだできることの半分(?)です……。 ここまでの実装だと、警告を出すだけなんですよね。この後に、CodeFixProviderを実装して、「警告→修正」までを行えるようにすると、チェッカーとしては完璧な出来になります。CodeFixProviderについては、まだ勉強中ということで一旦ここまでとさせていただきます。
これを使えば、コーディング規約の一部のチェックは自動化できるので、レビューの負担が相当下がりますね。かなり魅力的だなぁと思いますが、あまり盛り上がりに欠けています。(使う人が少ないのか、観測範囲の問題か)
SI系は、観測範囲内ではだいたいVS使ってるんだから、どんどん使われるといいのになぁと思いました。(まだJavaの方が多いかな?)
これが、誰かの役に立つことを願って……。(数週間後の自分が一番可能性が高い)