BizHawk/ExternalProjects/BizHawk.Analyzer/FeatureNotImplementedAnalyz...

103 lines
5.0 KiB
C#

namespace BizHawk.Analyzers;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class FeatureNotImplementedAnalyzer : DiagnosticAnalyzer
{
private const string ERR_MSG_DOES_NOT_THROW = "Throw NotImplementedException in [FeatureNotImplemented] method/prop body (or remove attribute)";
private const string ERR_MSG_METHOD_THROWS_UNKNOWN = "Indeterminable exception type in [FeatureNotImplemented] method/prop body, should be NotImplementedException";
private const string ERR_MSG_THROWS_WRONG_TYPE = "Incorrect exception type in [FeatureNotImplemented] method/prop body, should be NotImplementedException";
private const string ERR_MSG_UNEXPECTED_INCANTATION = "It seems [FeatureNotImplemented] should not be applied to whatever this is";
private static readonly DiagnosticDescriptor DiagShouldThrowNIE = new(
id: "BHI3300",
title: "Throw NotImplementedException from methods/props marked [FeatureNotImplemented]",
messageFormat: "{0}",
category: "Usage",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(DiagShouldThrowNIE);
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterCompilationStartAction(initContext =>
{
var featureNotImplementedAttrSym = initContext.Compilation.GetTypeByMetadataName("BizHawk.Emulation.Common.FeatureNotImplementedAttribute");
if (featureNotImplementedAttrSym is null) return; // project does not have BizHawk.Emulation.Common dependency
var notImplementedExceptionSym = initContext.Compilation.GetTypeByMetadataName("System.NotImplementedException")!;
initContext.RegisterSyntaxNodeAction(
snac =>
{
void Wat(Location location)
=> snac.ReportDiagnostic(Diagnostic.Create(DiagShouldThrowNIE, location, ERR_MSG_UNEXPECTED_INCANTATION));
void MaybeReportFor(ITypeSymbol? thrownExceptionType, Location location)
{
if (thrownExceptionType is null) snac.ReportDiagnostic(Diagnostic.Create(DiagShouldThrowNIE, location, ERR_MSG_METHOD_THROWS_UNKNOWN));
else if (!notImplementedExceptionSym.Matches(thrownExceptionType)) snac.ReportDiagnostic(Diagnostic.Create(DiagShouldThrowNIE, location, ERR_MSG_THROWS_WRONG_TYPE));
// else correct usage, do not flag
}
bool IncludesFNIAttribute(SyntaxList<AttributeListSyntax> mds)
=> mds.SelectMany(static als => als.Attributes).Any(aSyn => featureNotImplementedAttrSym.Matches(snac.SemanticModel.GetTypeInfo(aSyn).Type));
void CheckBlockBody(BlockSyntax bs, Location location)
{
if (bs.Statements.Count is not 1) snac.ReportDiagnostic(Diagnostic.Create(DiagShouldThrowNIE, location, ERR_MSG_DOES_NOT_THROW));
else if (bs.Statements[0] is not ThrowStatementSyntax tss) snac.ReportDiagnostic(Diagnostic.Create(DiagShouldThrowNIE, location, ERR_MSG_DOES_NOT_THROW));
else MaybeReportFor(snac.SemanticModel.GetThrownExceptionType(tss), tss.GetLocation());
}
void CheckExprBody(ArrowExpressionClauseSyntax aecs, Location location)
{
if (aecs.Expression is not ThrowExpressionSyntax tes) snac.ReportDiagnostic(Diagnostic.Create(DiagShouldThrowNIE, location, ERR_MSG_DOES_NOT_THROW));
else MaybeReportFor(snac.SemanticModel.GetThrownExceptionType(tes), tes.GetLocation());
}
void CheckAccessor(AccessorDeclarationSyntax ads)
{
if (!IncludesFNIAttribute(ads.AttributeLists)) return;
if (ads.ExpressionBody is not null) CheckExprBody(ads.ExpressionBody, ads.GetLocation());
else if (ads.Body is not null) CheckBlockBody(ads.Body, ads.GetLocation());
else Wat(ads.GetLocation());
}
switch (snac.Node)
{
case AccessorDeclarationSyntax ads:
CheckAccessor(ads);
break;
case MethodDeclarationSyntax mds:
if (!IncludesFNIAttribute(mds.AttributeLists)) return;
if (mds.ExpressionBody is not null) CheckExprBody(mds.ExpressionBody, mds.GetLocation());
else if (mds.Body is not null) CheckBlockBody(mds.Body, mds.GetLocation());
else Wat(mds.GetLocation());
break;
case PropertyDeclarationSyntax pds:
if (pds.ExpressionBody is not null)
{
if (IncludesFNIAttribute(pds.AttributeLists)) CheckExprBody(pds.ExpressionBody, pds.GetLocation());
}
else
{
if (IncludesFNIAttribute(pds.AttributeLists)) Wat(pds.GetLocation());
else foreach (var accessor in pds.AccessorList!.Accessors) CheckAccessor(accessor);
}
break;
}
},
SyntaxKind.GetAccessorDeclaration,
SyntaxKind.MethodDeclaration,
SyntaxKind.PropertyDeclaration,
SyntaxKind.SetAccessorDeclaration);
});
}
}