Add Analyzer rule to enforce throwing in `[FeatureNotImplemented]`

This commit is contained in:
YoshiRulz 2022-07-15 20:51:27 +10:00
parent 26e02033f2
commit 3fddcdb2c5
No known key found for this signature in database
GPG Key ID: C4DE31C245353FB7
25 changed files with 158 additions and 81 deletions

View File

@ -15,6 +15,9 @@
<!-- Default branch of switch expression should throw InvalidOperationException/SwitchExpressionException or not throw -->
<Rule Id="BHI1005" Action="Error" />
<!-- Throw NotImplementedException from methods/props marked [FeatureNotImplemented] -->
<Rule Id="BHI3300" Action="Error" />
</Rules>
<Rules AnalyzerId="DocumentationAnalyzers" RuleNamespace="DocumentationAnalyzers.StyleRules">
<!-- Place text in paragraphs -->

View File

@ -0,0 +1,111 @@
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();
var skipThisProject = false;
INamedTypeSymbol? featureNotImplementedAttrSym = null;
INamedTypeSymbol? notImplementedExceptionSym = null;
context.RegisterSyntaxNodeAction(
snac =>
{
if (skipThisProject) return;
if (featureNotImplementedAttrSym is null)
{
featureNotImplementedAttrSym = snac.Compilation.GetTypeByMetadataName("BizHawk.Emulation.Common.FeatureNotImplementedAttribute");
if (featureNotImplementedAttrSym is null)
{
// project does not have BizHawk.Emulation.Common dependency
skipThisProject = true;
return;
}
notImplementedExceptionSym = snac.Compilation.GetTypeByMetadataName("System.NotImplementedException")!;
}
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);
}
}

View File

@ -13,6 +13,9 @@ internal static class RoslynUtils
public static ITypeSymbol? GetThrownExceptionType(this SemanticModel model, ThrowExpressionSyntax tes)
=> model.GetThrownExceptionType(tes.Expression);
public static ITypeSymbol? GetThrownExceptionType(this SemanticModel model, ThrowStatementSyntax tss)
=> model.GetThrownExceptionType(tss.Expression!);
public static bool Matches(this ITypeSymbol expected, ITypeSymbol? actual)
=> SymbolEqualityComparer.Default.Equals(expected, actual);
}

Binary file not shown.

View File

@ -17,15 +17,9 @@ namespace BizHawk.Emulation.Cores.Computers.AppleII
public int BackgroundColor => 0;
public int VsyncNumerator
{
[FeatureNotImplemented] // TODO: precise numbers or confirm the default is okay
get => NullVideo.DefaultVsyncNum;
}
=> NullVideo.DefaultVsyncNum; //TODO precise numbers or confirm the default is okay
public int VsyncDenominator
{
[FeatureNotImplemented] // TODO: precise numbers or confirm the default is okay
get => NullVideo.DefaultVsyncDen;
}
=> NullVideo.DefaultVsyncDen; //TODO precise numbers or confirm the default is okay
}
}

View File

@ -1,6 +1,8 @@
using BizHawk.Emulation.Common;
using System;
using System.IO;
using BizHawk.Emulation.Common;
namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
{
/// <summary>
@ -64,9 +66,7 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
[FeatureNotImplemented]
public void DisassembleCDL(Stream s, ICodeDataLog cdl)
{
}
=> throw new NotImplementedException();
public enum CDLType
{

View File

@ -471,16 +471,10 @@ namespace BizHawk.Emulation.Cores.ColecoVision
public int BackgroundColor => 0;
public int VsyncNumerator
{
[FeatureNotImplemented]
get => NullVideo.DefaultVsyncNum;
}
=> NullVideo.DefaultVsyncNum; //TODO precise numbers or confirm the default is okay
public int VsyncDenominator
{
[FeatureNotImplemented]
get => NullVideo.DefaultVsyncDen;
}
=> NullVideo.DefaultVsyncDen; //TODO precise numbers or confirm the default is okay
private readonly int[] PaletteTMS9918 =
{

View File

@ -23,9 +23,7 @@ namespace BizHawk.Emulation.Cores.Consoles.Vectrex
[FeatureNotImplemented]
public void DisassembleCDL(Stream s, ICodeDataLog cdl)
{
}
=> throw new NotImplementedException();
private enum CDLog_AddrType
{

View File

@ -83,16 +83,10 @@ namespace BizHawk.Emulation.Cores.Intellivision
public int BackgroundColor => 0;
public int VsyncNumerator
{
[FeatureNotImplemented]
get => NullVideo.DefaultVsyncNum;
}
=> NullVideo.DefaultVsyncNum; //TODO precise numbers or confirm the default is okay
public int VsyncDenominator
{
[FeatureNotImplemented]
get => NullVideo.DefaultVsyncDen;
}
=> NullVideo.DefaultVsyncDen; //TODO precise numbers or confirm the default is okay
public void Reset()
{

View File

@ -1,3 +1,4 @@
using System;
using System.IO;
using BizHawk.Emulation.Common;
using BizHawk.Emulation.Cores.Components.I8048;
@ -31,9 +32,8 @@ namespace BizHawk.Emulation.Cores.Consoles.O2Hawk
}
[FeatureNotImplemented]
void ICodeDataLogger.DisassembleCDL(Stream s, ICodeDataLog cdl)
{
}
public void DisassembleCDL(Stream s, ICodeDataLog cdl)
=> throw new NotImplementedException();
public void SetCDL(I8048.eCDLogMemFlags flags, string type, int cdladdr)
{

View File

@ -50,7 +50,6 @@ namespace BizHawk.Emulation.Cores.Nintendo.GBA
}
}
[FeatureNotImplemented]
public IMemoryCallbackSystem MemoryCallbacks { get; }
public bool CanStep(StepType type) => false;

View File

@ -175,7 +175,6 @@ namespace BizHawk.Emulation.Cores.Nintendo.GBA
return _gpumem;
}
[FeatureNotImplemented]
public void SetScanlineCallback(Action callback, int scanline)
{
_scanlinecb = callback;

View File

@ -1,3 +1,4 @@
using System;
using System.IO;
using BizHawk.Emulation.Common;
using BizHawk.Emulation.Cores.Components.LR35902;
@ -31,9 +32,8 @@ namespace BizHawk.Emulation.Cores.Nintendo.GBHawk
}
[FeatureNotImplemented]
void ICodeDataLogger.DisassembleCDL(Stream s, ICodeDataLog cdl)
{
}
public void DisassembleCDL(Stream s, ICodeDataLog cdl)
=> throw new NotImplementedException();
public void SetCDL(LR35902.eCDLogMemFlags flags, string type, int cdladdr)
{

View File

@ -1,3 +1,4 @@
using System;
using System.IO;
using BizHawk.Emulation.Common;
@ -32,9 +33,8 @@ namespace BizHawk.Emulation.Cores.Nintendo.GBHawkLink
}
[FeatureNotImplemented]
void ICodeDataLogger.DisassembleCDL(Stream s, ICodeDataLog cdl)
{
}
public void DisassembleCDL(Stream s, ICodeDataLog cdl)
=> throw new NotImplementedException();
public void SetCDL(LR35902.eCDLogMemFlags flags, string type, int cdladdr)
{

View File

@ -1,3 +1,4 @@
using System;
using System.IO;
using BizHawk.Emulation.Common;
@ -32,9 +33,8 @@ namespace BizHawk.Emulation.Cores.Nintendo.GBHawkLink3x
}
[FeatureNotImplemented]
void ICodeDataLogger.DisassembleCDL(Stream s, ICodeDataLog cdl)
{
}
public void DisassembleCDL(Stream s, ICodeDataLog cdl)
=> throw new NotImplementedException();
public void SetCDL(LR35902.eCDLogMemFlags flags, string type, int cdladdr)
{

View File

@ -1,3 +1,4 @@
using System;
using System.IO;
using BizHawk.Emulation.Common;
@ -32,9 +33,8 @@ namespace BizHawk.Emulation.Cores.Nintendo.GBHawkLink4x
}
[FeatureNotImplemented]
void ICodeDataLogger.DisassembleCDL(Stream s, ICodeDataLog cdl)
{
}
public void DisassembleCDL(Stream s, ICodeDataLog cdl)
=> throw new NotImplementedException();
public void SetCDL(LR35902.eCDLogMemFlags flags, string type, int cdladdr)
{

View File

@ -33,9 +33,8 @@ namespace BizHawk.Emulation.Cores.Nintendo.Gameboy
}
[FeatureNotImplemented]
void ICodeDataLogger.DisassembleCDL(Stream s, ICodeDataLog cdl)
{
}
public void DisassembleCDL(Stream s, ICodeDataLog cdl)
=> throw new NotImplementedException();
private ICodeDataLog _cdl;
private string _which;

View File

@ -1,7 +1,8 @@
using BizHawk.Emulation.Common;
using System;
using System.IO;
using BizHawk.Emulation.Common;
namespace BizHawk.Emulation.Cores.Nintendo.Gameboy
{
partial class GambatteLink : ICodeDataLogger
@ -23,8 +24,9 @@ namespace BizHawk.Emulation.Cores.Nintendo.Gameboy
}
[FeatureNotImplemented]
void ICodeDataLogger.DisassembleCDL(Stream s, ICodeDataLog cdl)
public void DisassembleCDL(Stream s, ICodeDataLog cdl)
{
throw new NotImplementedException();
// this doesn't actually do anything
/*
for (int i = 0; i < _numCores; i++)

View File

@ -39,6 +39,6 @@ namespace BizHawk.Emulation.Cores.Nintendo.N64
}
// TODO: optimize managed to unmanaged using the ActiveChanged event
public IInputCallbackSystem InputCallbacks { [FeatureNotImplemented] get; }
public IInputCallbackSystem InputCallbacks { get; }
}
}

View File

@ -28,9 +28,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES
[FeatureNotImplemented]
public void DisassembleCDL(Stream s, ICodeDataLog cdl)
{
}
=> throw new NotImplementedException();
public enum CDLog_AddrType
{

View File

@ -445,15 +445,9 @@ namespace BizHawk.Emulation.Cores.PCEngine
public int BackgroundColor => vce.Palette[256];
public int VsyncNumerator
{
[FeatureNotImplemented]
get => NullVideo.DefaultVsyncNum;
}
=> NullVideo.DefaultVsyncNum; //TODO precise numbers or confirm the default is okay
public int VsyncDenominator
{
[FeatureNotImplemented]
get => NullVideo.DefaultVsyncDen;
}
=> NullVideo.DefaultVsyncDen; //TODO precise numbers or confirm the default is okay
}
}

View File

@ -534,15 +534,9 @@ namespace BizHawk.Emulation.Cores.PCEngine
public int BackgroundColor => VCE.Palette[0];
public int VsyncNumerator
{
[FeatureNotImplemented]
get => NullVideo.DefaultVsyncNum;
}
=> NullVideo.DefaultVsyncNum; //TODO precise numbers or confirm the default is okay
public int VsyncDenominator
{
[FeatureNotImplemented]
get => NullVideo.DefaultVsyncDen;
}
=> NullVideo.DefaultVsyncDen; //TODO precise numbers or confirm the default is okay
}
}

View File

@ -42,9 +42,7 @@ namespace BizHawk.Emulation.Cores.Sega.GGHawkLink
[FeatureNotImplemented]
public void DisassembleCDL(Stream s, ICodeDataLog cdl)
{
}
=> throw new NotImplementedException();
private enum CDLog_AddrType
{

View File

@ -40,9 +40,7 @@ namespace BizHawk.Emulation.Cores.Sega.MasterSystem
[FeatureNotImplemented]
public void DisassembleCDL(Stream s, ICodeDataLog cdl)
{
}
=> throw new NotImplementedException();
public enum CDLog_AddrType
{

View File

@ -918,7 +918,6 @@ namespace BizHawk.Emulation.Cores.Sony.PSX
get => throw new NotImplementedException();
}
[FeatureNotImplemented]
public bool DeterministicEmulation => true;
public int[] GetVideoBuffer() => frameBuffer;