GCが発生するIReadOnlyList<T>等のforeachに警告を出すRoslynAnalyzer
IReadOnlyList<T>をforeachで回すとGCが発生するのは周知の事実ですが、
外部にプロパティを公開するならIReadOnlyListやIReadOnlyDictionaryを使いたいし、
そもそも他にもGCが発生するforeachのパターンあるよな・・・と考え出すと、
気軽にforeachも書けないわけです。
ということで、RoslynAnalyzerでGCが発生するforeachを書いたら警告が出るようにしてみました。
これで本当に条件が網羅出来ているか不安...
もっと良い判定方法ご存知の方いたら教えてください。
ForeachGcAnalyzer.cs
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace MyAnalyzer;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class ForeachGcAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "GCFOREACH001";
private static readonly LocalizableString Title = "GC アロケーションを伴う foreach";
private static readonly LocalizableString MessageFormat = "この foreach は GC ヒープに IEnumerator を確保します。for ループや Span<T> の利用を検討してください";
private static readonly LocalizableString Description = "参照型 IEnumerator または boxing が発生する foreach を検出して警告します。";
private const string Category = "Performance";
private static readonly DiagnosticDescriptor Rule = new(
DiagnosticId,
Title,
MessageFormat,
Category,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: Description
);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeForeach, SyntaxKind.ForEachStatement);
}
private static void AnalyzeForeach(SyntaxNodeAnalysisContext context)
{
var foreachStmt = (ForEachStatementSyntax) context.Node;
var model = context.SemanticModel;
var info = model.GetForEachStatementInfo(foreachStmt);
var collectionType = model.GetTypeInfo(foreachStmt.Expression).Type;
if (collectionType == null) return;
if (ShouldWarn(info, collectionType))
{
var diag = Diagnostic.Create(Rule, foreachStmt.ForEachKeyword.GetLocation());
context.ReportDiagnostic(diag);
}
}
private static bool ShouldWarn(ForEachStatementInfo info, ITypeSymbol collectionType)
{
if (collectionType is IArrayTypeSymbol)
{
return false;
}
if (collectionType.SpecialType == SpecialType.System_String)
{
return false;
}
var enumeratorType = info.GetEnumeratorMethod?.ReturnType;
if (enumeratorType == null)
{
return false;
}
if (enumeratorType.IsReferenceType)
{
return true;
}
if (enumeratorType.IsValueType)
{
switch (collectionType.TypeKind)
{
case TypeKind.Interface:
case TypeKind.Dynamic:
return true;
}
if (collectionType.SpecialType == SpecialType.System_Object)
{
return true;
}
return false;
}
return true;
}
}netstandard2.0のクラスライブラリプロジェクトとしてビルドし、
できたdllをUnityプロジェクトのAssets以下に配置、
Asset LabelsにRoslynAnalyzerを追加するとUnityプロジェクトでも動作します。