BizHawk/ExternalProjects/BizHawk.Analyzer/HawkSourceAnalyzer.cs

134 lines
5.6 KiB
C#

namespace BizHawk.Analyzers;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class HawkSourceAnalyzer : DiagnosticAnalyzer
{
private const string ERR_MSG_SWITCH_THROWS_UNKNOWN = "Indeterminable exception type in default switch branch, should be InvalidOperationException/SwitchExpressionException";
private const string ERR_MSG_SWITCH_THROWS_WRONG_TYPE = "Incorrect exception type in default switch branch, should be InvalidOperationException/SwitchExpressionException";
private static readonly DiagnosticDescriptor DiagInterpStringIsDollarAt = new(
id: "BHI1004",
title: "Verbatim interpolated strings should begin $@, not @$",
messageFormat: "Swap @ and $ on interpolated string",
category: "Usage",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
private static readonly DiagnosticDescriptor DiagNoAnonClasses = new(
id: "BHI1002",
title: "Do not use anonymous types (classes)",
messageFormat: "Replace anonymous class with tuple or explicit type",
category: "Usage",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
private static readonly DiagnosticDescriptor DiagNoAnonDelegates = new(
id: "BHI1001",
title: "Do not use anonymous delegates",
messageFormat: "Replace anonymous delegate with lambda or local method",
category: "Usage",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
private static readonly DiagnosticDescriptor DiagNoDiscardingLocals = new(
id: "BHI1006",
title: "Do not discard local variables",
messageFormat: "RHS is a local, so this discard has no effect, and is at best unhelpful for humans",
category: "Usage",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
private static readonly DiagnosticDescriptor DiagNoQueryExpression = new(
id: "BHI1003",
title: "Do not use query expression syntax",
messageFormat: "Use method chain for LINQ instead of query expression syntax",
category: "Usage",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
private static readonly DiagnosticDescriptor DiagSwitchShouldThrowIOE = new(
id: "BHI1005",
title: "Default branch of switch expression should throw InvalidOperationException/SwitchExpressionException or not throw",
messageFormat: "{0}",
category: "Usage",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(
DiagInterpStringIsDollarAt,
DiagNoAnonClasses,
DiagNoAnonDelegates,
DiagNoDiscardingLocals,
DiagNoQueryExpression,
DiagSwitchShouldThrowIOE);
public override void Initialize(AnalysisContext context)
{
static bool IsDiscard(AssignmentExpressionSyntax aes)
=> aes.OperatorToken.RawKind is (int) SyntaxKind.EqualsToken && aes.Left is IdentifierNameSyntax { Identifier.Text: "_" };
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
INamedTypeSymbol? invalidOperationExceptionSym = null;
INamedTypeSymbol? switchExpressionExceptionSym = null;
context.RegisterSyntaxNodeAction(
snac =>
{
if (invalidOperationExceptionSym is null)
{
invalidOperationExceptionSym = snac.Compilation.GetTypeByMetadataName("System.InvalidOperationException")!;
switchExpressionExceptionSym = snac.Compilation.GetTypeByMetadataName("System.Runtime.CompilerServices.SwitchExpressionException");
}
switch (snac.Node)
{
case AnonymousMethodExpressionSyntax:
snac.ReportDiagnostic(Diagnostic.Create(DiagNoAnonDelegates, snac.Node.GetLocation()));
break;
case AnonymousObjectCreationExpressionSyntax:
snac.ReportDiagnostic(Diagnostic.Create(DiagNoAnonClasses, snac.Node.GetLocation()));
break;
case AssignmentExpressionSyntax aes when IsDiscard(aes) && snac.SemanticModel.GetSymbolInfo(aes.Right).Symbol?.Kind is SymbolKind.Local:
snac.ReportDiagnostic(Diagnostic.Create(DiagNoDiscardingLocals, snac.Node.GetLocation()));
break;
case InterpolatedStringExpressionSyntax ises:
if (ises.StringStartToken.Text[0] is '@') snac.ReportDiagnostic(Diagnostic.Create(DiagInterpStringIsDollarAt, ises.GetLocation()));
break;
case QueryExpressionSyntax:
snac.ReportDiagnostic(Diagnostic.Create(DiagNoQueryExpression, snac.Node.GetLocation()));
break;
case SwitchExpressionArmSyntax { WhenClause: null, Pattern: DiscardPatternSyntax, Expression: ThrowExpressionSyntax tes }:
var thrownExceptionType = snac.SemanticModel.GetThrownExceptionType(tes);
if (thrownExceptionType is null)
{
snac.ReportDiagnostic(Diagnostic.Create(
DiagSwitchShouldThrowIOE,
tes.GetLocation(),
DiagnosticSeverity.Warning,
additionalLocations: null,
properties: null,
ERR_MSG_SWITCH_THROWS_UNKNOWN));
}
else if (!invalidOperationExceptionSym.Matches(thrownExceptionType) && switchExpressionExceptionSym?.Matches(thrownExceptionType) != true)
{
snac.ReportDiagnostic(Diagnostic.Create(DiagSwitchShouldThrowIOE, tes.GetLocation(), ERR_MSG_SWITCH_THROWS_WRONG_TYPE));
}
// else correct usage, do not flag
break;
}
},
SyntaxKind.AnonymousObjectCreationExpression,
SyntaxKind.AnonymousMethodExpression,
SyntaxKind.InterpolatedStringExpression,
SyntaxKind.QueryExpression,
SyntaxKind.SimpleAssignmentExpression,
SyntaxKind.SwitchExpressionArm);
}
}