Skip to content

Commit 0bd482e

Browse files
committed
Treat records as classes for ordering
Fixes #3236
1 parent 9715fc0 commit 0bd482e

File tree

4 files changed

+151
-9
lines changed

4 files changed

+151
-9
lines changed

StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp9/OrderingRules/SA1201CSharp9UnitTests.cs

+130
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,139 @@
33

44
namespace StyleCop.Analyzers.Test.CSharp9.OrderingRules
55
{
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using Microsoft.CodeAnalysis.Testing;
69
using StyleCop.Analyzers.Test.CSharp8.OrderingRules;
10+
using Xunit;
11+
using static StyleCop.Analyzers.Test.Verifiers.StyleCopCodeFixVerifier<
12+
StyleCop.Analyzers.OrderingRules.SA1201ElementsMustAppearInTheCorrectOrder,
13+
StyleCop.Analyzers.OrderingRules.ElementOrderCodeFixProvider>;
714

815
public class SA1201CSharp9UnitTests : SA1201CSharp8UnitTests
916
{
17+
[Fact]
18+
[WorkItem(3236, "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3236")]
19+
public async Task TestOuterOrderWithRecordCorrectOrderAsync()
20+
{
21+
string testCode = @"namespace Foo { }
22+
public delegate void bar();
23+
public enum TestEnum { }
24+
public interface IFoo { }
25+
public struct FooStruct { }
26+
public record FooClass { }
27+
";
28+
29+
await VerifyCSharpDiagnosticAsync(testCode, DiagnosticResult.EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
30+
await VerifyCSharpDiagnosticAsync("namespace OuterNamespace { " + testCode + " }", DiagnosticResult.EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
31+
}
32+
33+
[Fact]
34+
[WorkItem(3236, "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3236")]
35+
public async Task TestOuterOrderWithRecordWrongOrderAsync()
36+
{
37+
string testCode = @"
38+
namespace Foo { }
39+
public enum TestEnum { }
40+
public delegate void {|#0:bar|}();
41+
public interface IFoo { }
42+
public record FooClass { }
43+
public struct {|#1:FooStruct|} { }
44+
";
45+
var expected = new[]
46+
{
47+
Diagnostic().WithLocation(0).WithArguments("delegate", "enum"),
48+
Diagnostic().WithLocation(1).WithArguments("struct", "record"),
49+
};
50+
51+
await VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
52+
await VerifyCSharpDiagnosticAsync("namespace OuterNamespace { " + testCode + " }", expected, CancellationToken.None).ConfigureAwait(false);
53+
}
54+
55+
[Fact]
56+
public async Task TestTypeMemberOrderCorrectOrderRecordAsync()
57+
{
58+
string testCode = @"public record OuterType
59+
{
60+
public string TestField;
61+
public OuterType(int argument) { TestField = ""foo""; TestProperty = """"; }
62+
public delegate void TestDelegate();
63+
public event TestDelegate TestEvent { add { } remove { } }
64+
public enum TestEnum { }
65+
public interface ITest { }
66+
public string TestProperty { get; set; }
67+
public string this[string arg] { get { return ""foo""; } set { } }
68+
public static explicit operator bool(OuterType t1) { return t1.TestField != null; }
69+
public static OuterType operator +(OuterType t1, OuterType t2) { return t1; }
70+
public void TestMethod () { }
71+
public struct TestStruct { }
72+
public class TestClass1 { }
73+
public record TestRecord1 { }
74+
public class TestClass2 { }
75+
public record TestRecord2 { }
76+
}
77+
";
78+
79+
await VerifyCSharpDiagnosticAsync(testCode, DiagnosticResult.EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
80+
}
81+
82+
[Fact]
83+
public async Task TestTypeMemberOrderWrongOrderRecordAsync()
84+
{
85+
string testCode = @"public record OuterType
86+
{
87+
public string TestField;
88+
public OuterType(int argument) { TestField = ""foo""; TestProperty = ""bar""; }
89+
public interface ITest { }
90+
public delegate void TestDelegate();
91+
public event TestDelegate TestEvent { add { } remove { } }
92+
public enum TestEnum { }
93+
public static OuterType operator +(OuterType t1, OuterType t2) { return t1; }
94+
public static explicit operator bool(OuterType t1) { return t1.TestField != null; }
95+
public string TestProperty { get; set; }
96+
public struct TestStruct { }
97+
public void TestMethod () { }
98+
public class TestClass { }
99+
public string this[string arg] { get { return ""foo""; } set { } }
100+
}
101+
";
102+
var expected = new[]
103+
{
104+
Diagnostic().WithLocation(6, 26).WithArguments("delegate", "interface"),
105+
Diagnostic().WithLocation(10, 5).WithArguments("conversion", "operator"),
106+
Diagnostic().WithLocation(11, 19).WithArguments("property", "conversion"),
107+
Diagnostic().WithLocation(13, 17).WithArguments("method", "struct"),
108+
Diagnostic().WithLocation(15, 19).WithArguments("indexer", "class"),
109+
};
110+
111+
string fixedCode = @"public record OuterType
112+
{
113+
public string TestField;
114+
public OuterType(int argument) { TestField = ""foo""; TestProperty = ""bar""; }
115+
public delegate void TestDelegate();
116+
public event TestDelegate TestEvent { add { } remove { } }
117+
public enum TestEnum { }
118+
public interface ITest { }
119+
public string TestProperty { get; set; }
120+
public string this[string arg] { get { return ""foo""; } set { } }
121+
public static explicit operator bool(OuterType t1) { return t1.TestField != null; }
122+
public static OuterType operator +(OuterType t1, OuterType t2) { return t1; }
123+
public void TestMethod () { }
124+
public struct TestStruct { }
125+
public class TestClass { }
126+
}
127+
";
128+
129+
var test = new CSharpTest
130+
{
131+
TestCode = testCode,
132+
FixedCode = fixedCode,
133+
NumberOfIncrementalIterations = 7,
134+
NumberOfFixAllIterations = 3,
135+
};
136+
137+
test.ExpectedDiagnostics.AddRange(expected);
138+
await test.RunAsync(CancellationToken.None).ConfigureAwait(false);
139+
}
10140
}
11141
}

StyleCop.Analyzers/StyleCop.Analyzers/Helpers/SyntaxKinds.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ namespace StyleCop.Analyzers.Helpers
66
using System.Collections.Immutable;
77
using Microsoft.CodeAnalysis.CSharp;
88
using Microsoft.CodeAnalysis.CSharp.Syntax;
9+
using StyleCop.Analyzers.Lightup;
910

1011
internal static class SyntaxKinds
1112
{
@@ -36,7 +37,8 @@ internal static class SyntaxKinds
3637
ImmutableArray.Create(
3738
SyntaxKind.ClassDeclaration,
3839
SyntaxKind.StructDeclaration,
39-
SyntaxKind.InterfaceDeclaration);
40+
SyntaxKind.InterfaceDeclaration,
41+
SyntaxKindEx.RecordDeclaration);
4042

4143
/// <summary>
4244
/// Gets a collection of <see cref="SyntaxKind"/> values which appear in the syntax tree as a

StyleCop.Analyzers/StyleCop.Analyzers/OrderingRules/SA1201ElementsMustAppearInTheCorrectOrder.cs

+16-8
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ namespace StyleCop.Analyzers.OrderingRules
1111
using Microsoft.CodeAnalysis.CSharp.Syntax;
1212
using Microsoft.CodeAnalysis.Diagnostics;
1313
using StyleCop.Analyzers.Helpers;
14+
using StyleCop.Analyzers.Lightup;
1415
using StyleCop.Analyzers.Settings.ObjectModel;
1516

1617
/// <summary>
@@ -120,9 +121,6 @@ internal class SA1201ElementsMustAppearInTheCorrectOrder : DiagnosticAnalyzer
120121
private static readonly DiagnosticDescriptor Descriptor =
121122
new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, AnalyzerCategory.OrderingRules, DiagnosticSeverity.Warning, AnalyzerConstants.EnabledByDefault, Description, HelpLink);
122123

123-
private static readonly ImmutableArray<SyntaxKind> TypeDeclarationKinds =
124-
ImmutableArray.Create(SyntaxKind.ClassDeclaration, SyntaxKind.StructDeclaration, SyntaxKind.InterfaceDeclaration);
125-
126124
// extern alias and usings are missing here because the compiler itself is enforcing the right order.
127125
private static readonly ImmutableArray<SyntaxKind> OuterOrder = ImmutableArray.Create(
128126
SyntaxKind.NamespaceDeclaration,
@@ -156,11 +154,13 @@ internal class SA1201ElementsMustAppearInTheCorrectOrder : DiagnosticAnalyzer
156154
[SyntaxKind.InterfaceDeclaration] = "interface",
157155
[SyntaxKind.StructDeclaration] = "struct",
158156
[SyntaxKind.ClassDeclaration] = "class",
157+
[SyntaxKindEx.RecordDeclaration] = "record",
159158
[SyntaxKind.FieldDeclaration] = "field",
160159
[SyntaxKind.ConstructorDeclaration] = "constructor",
161160
[SyntaxKind.DestructorDeclaration] = "destructor",
162161
[SyntaxKind.DelegateDeclaration] = "delegate",
163162
[SyntaxKind.EventDeclaration] = "event",
163+
[SyntaxKind.EventFieldDeclaration] = "event",
164164
[SyntaxKind.EnumDeclaration] = "enum",
165165
[SyntaxKind.InterfaceDeclaration] = "interface",
166166
[SyntaxKind.PropertyDeclaration] = "property",
@@ -186,7 +186,7 @@ public override void Initialize(AnalysisContext context)
186186

187187
context.RegisterSyntaxNodeAction(CompilationUnitAction, SyntaxKind.CompilationUnit);
188188
context.RegisterSyntaxNodeAction(NamespaceDeclarationAction, SyntaxKind.NamespaceDeclaration);
189-
context.RegisterSyntaxNodeAction(TypeDeclarationAction, TypeDeclarationKinds);
189+
context.RegisterSyntaxNodeAction(TypeDeclarationAction, SyntaxKinds.TypeDeclaration);
190190
}
191191

192192
private static void HandleTypeDeclaration(SyntaxNodeAnalysisContext context, StyleCopSettings settings)
@@ -287,12 +287,10 @@ private static void HandleMemberList(SyntaxNodeAnalysisContext context, Immutabl
287287
}
288288

289289
var elementSyntaxKind = members[i].Kind();
290-
elementSyntaxKind = elementSyntaxKind == SyntaxKind.EventFieldDeclaration ? SyntaxKind.EventDeclaration : elementSyntaxKind;
291-
int index = order.IndexOf(elementSyntaxKind);
290+
int index = order.IndexOf(GetSyntaxKindForOrdering(elementSyntaxKind));
292291

293292
var nextElementSyntaxKind = members[i + 1].Kind();
294-
nextElementSyntaxKind = nextElementSyntaxKind == SyntaxKind.EventFieldDeclaration ? SyntaxKind.EventDeclaration : nextElementSyntaxKind;
295-
int nextIndex = order.IndexOf(nextElementSyntaxKind);
293+
int nextIndex = order.IndexOf(GetSyntaxKindForOrdering(nextElementSyntaxKind));
296294

297295
if (index > nextIndex)
298296
{
@@ -304,5 +302,15 @@ private static void HandleMemberList(SyntaxNodeAnalysisContext context, Immutabl
304302
}
305303
}
306304
}
305+
306+
private static SyntaxKind GetSyntaxKindForOrdering(SyntaxKind syntaxKind)
307+
{
308+
return syntaxKind switch
309+
{
310+
SyntaxKind.EventFieldDeclaration => SyntaxKind.EventDeclaration,
311+
SyntaxKindEx.RecordDeclaration => SyntaxKind.ClassDeclaration,
312+
_ => syntaxKind,
313+
};
314+
}
307315
}
308316
}

documentation/SA1201.md

+2
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ Within a class, struct, or interface, elements should be positioned in the follo
4949
* Structs
5050
* Classes*
5151

52+
> 📝 For ordering purposes, C# 9 records are treated as classes.
53+
5254
Complying with a standard ordering scheme based on element type can increase the readability and maintainability of the file and encourage code reuse.
5355

5456
When implementing an interface, it is sometimes desirable to group all members of the interface next to one another. This will sometimes require violating this rule, if the interface contains elements of different types. This problem can be solved through the use of partial classes.

0 commit comments

Comments
 (0)