Add analyzer (currently disabled) to enforce a newline policy for `=>`

This commit is contained in:
YoshiRulz 2024-07-07 12:26:00 +10:00
parent 479f151bbb
commit 2ffb897b11
No known key found for this signature in database
GPG Key ID: C4DE31C245353FB7
4 changed files with 209 additions and 0 deletions

View File

@ -24,6 +24,8 @@ dotnet_diagnostic.BHI1102.severity = error
dotnet_diagnostic.BHI1103.severity = error
# Brackets of collection expression should be separated with spaces
dotnet_diagnostic.BHI1110.severity = warning
# Expression-bodied member should be flowed to next line correctly
dotnet_diagnostic.BHI1120.severity = warning
# Check result of IDictionary.TryGetValue, or discard it if default(T) is desired
dotnet_diagnostic.BHI1200.severity = error

View File

@ -0,0 +1,124 @@
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 ExprBodiedMemberFlowAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor DiagExprBodiedMemberFlow = new(
id: "BHI1120",
title: "Expression-bodied member should be flowed to next line correctly",
messageFormat: "{0}",
category: "Usage",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(DiagExprBodiedMemberFlow);
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterCompilationStartAction(initContext =>
{
var ARROW_ONE_LINE = (' ', ' ');
// var ARROW_POST_SIG = (' ', '\n');
var ARROW_PRE_BODY = ('\n', ' ');
initContext.RegisterSyntaxNodeAction(
snac =>
{
var aecs = (ArrowExpressionClauseSyntax) snac.Node;
(char Before, char After) expectedWhitespace;
string kind;
var parent = aecs.Parent;
if (parent is null)
{
snac.ReportDiagnostic(Diagnostic.Create(DiagExprBodiedMemberFlow, aecs.GetLocation(), "Syntax node for expression-bodied member was orphaned?"));
return;
}
void Flag(string message)
=> snac.ReportDiagnostic(Diagnostic.Create(DiagExprBodiedMemberFlow, parent.GetLocation(), message));
switch (parent)
{
case MethodDeclarationSyntax:
expectedWhitespace = ARROW_PRE_BODY;
kind = "method";
break;
case PropertyDeclarationSyntax:
expectedWhitespace = ARROW_PRE_BODY;
kind = "get-only prop";
break;
case AccessorDeclarationSyntax ads:
expectedWhitespace = ARROW_ONE_LINE;
switch (ads.Keyword.Text)
{
case "get":
kind = ads.Parent?.Parent is IndexerDeclarationSyntax ? "get-indexer" : "getter";
break;
case "set":
kind = ads.Parent?.Parent is IndexerDeclarationSyntax ? "set-indexer" : "setter";
break;
case "init":
kind = "setter";
break;
case "add":
kind = "event sub";
break;
case "remove":
kind = "event unsub";
break;
default:
Flag($"Expression-bodied accessor was of an unexpected kind: {ads.Parent!.Parent!.GetType().FullName}");
return;
}
break;
case ConstructorDeclarationSyntax:
expectedWhitespace = ARROW_PRE_BODY;
kind = "constructor";
break;
case LocalFunctionStatementSyntax:
expectedWhitespace = ARROW_PRE_BODY;
kind = "local method";
break;
case IndexerDeclarationSyntax:
expectedWhitespace = ARROW_PRE_BODY;
kind = "get-only indexer";
break;
case OperatorDeclarationSyntax:
expectedWhitespace = ARROW_PRE_BODY;
kind = "overloaded operator";
break;
case ConversionOperatorDeclarationSyntax:
expectedWhitespace = ARROW_PRE_BODY;
kind = "overloaded cast operator";
break;
case DestructorDeclarationSyntax:
expectedWhitespace = ARROW_PRE_BODY;
kind = "finalizer";
break;
default:
Flag($"Expression-bodied member was of an unexpected kind: {parent.GetType().FullName}");
return;
}
static string EscapeChar(char c)
=> c is '\n' ? "\\n" : c.ToString();
void Fail()
=> Flag($"Whitespace around {kind} arrow syntax should be `{EscapeChar(expectedWhitespace.Before)}=>{EscapeChar(expectedWhitespace.After)}`");
if ((aecs.ArrowToken.HasLeadingTrivia ? '\n' : ' ') != expectedWhitespace.Before)
{
Fail();
return;
}
var hasLineBreakAfterArrow = aecs.ArrowToken.HasTrailingTrivia && aecs.ArrowToken.TrailingTrivia.ToFullString().Contains('\n');
if ((hasLineBreakAfterArrow ? '\n' : ' ') != expectedWhitespace.After) Fail();
},
SyntaxKind.ArrowExpressionClause);
});
}
}

View File

@ -0,0 +1,83 @@
namespace BizHawk.Tests.Analyzers;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Verify = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<
BizHawk.Analyzers.ExprBodiedMemberFlowAnalyzer,
Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
[TestClass]
public sealed class ExprBodiedMemberFlowAnalyzerTests
{
[TestMethod]
public Task CheckMisuseOfExpressionBodies()
=> Verify.VerifyAnalyzerAsync("""
public sealed class Cases {
private int GetOnlyProp
=> default;
{|BHI1120:private int BadGetOnlyProp => default;|}
private int GetSetProp {
get => default;
set => _ = value;
}
private int BadGetSetProp {
{|BHI1120:get =>
default;|}
{|BHI1120:set
=> _ = value;|}
}
private int GetInitProp {
get => default;
init => _ = value;
}
private int BadGetInitProp {
{|BHI1120:get
=> default;|}
{|BHI1120:init =>
_ = value;|}
}
private event System.EventHandler Event {
add => DummyMethod();
remove => _ = value;
}
private event System.EventHandler BadEvent {
{|BHI1120:add =>
DummyMethod();|}
{|BHI1120:remove
=> _ = value;|}
}
{|BHI1120:public Cases() => DummyMethod();|}
{|BHI1120:~Cases() => DummyMethod();|}
private int this[char good] {
get => default;
set => _ = value;
}
private int this[int bad] {
{|BHI1120:get
=> default;|}
{|BHI1120:set =>
_ = value;|}
}
private int ExprBodyMethod()
=> default;
{|BHI1120:private int BadExprBodyMethod() => default;|}
private void DummyMethod() {
int LocalMethod()
=> default;
{|BHI1120:int BadLocalMethod() => default;|}
}
}
public sealed class GoodCtorDtor {
public GoodCtorDtor()
=> DummyMethod();
~GoodCtorDtor()
=> DummyMethod();
private void DummyMethod() {}
}
namespace System.Runtime.CompilerServices {
public static class IsExternalInit {} // this sample is compiled for lowest-common-denominator of `netstandard2.0`, so `init` accessor gives an error without this
}
""");
}

Binary file not shown.