Add analyzer (currently disabled) to enforce a newline policy for `=>`
This commit is contained in:
parent
479f151bbb
commit
2ffb897b11
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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.
Loading…
Reference in New Issue