-
Notifications
You must be signed in to change notification settings - Fork 4
/
Men006RegionsShouldBeUsed.cs
165 lines (137 loc) · 6.21 KB
/
Men006RegionsShouldBeUsed.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
namespace Menees.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class Men006RegionsShouldBeUsed : Analyzer
{
#region Public Constants
public const string DiagnosticId = "MEN006";
#endregion
#region Private Data Members
private static readonly LocalizableString Title =
new LocalizableResourceString(nameof(Resources.Men006Title), Resources.ResourceManager, typeof(Resources));
private static readonly LocalizableString MessageFormat =
new LocalizableResourceString(nameof(Resources.Men006MessageFormat), Resources.ResourceManager, typeof(Resources));
private static readonly LocalizableString Description =
new LocalizableResourceString(nameof(Resources.Men006Description), Resources.ResourceManager, typeof(Resources));
private static readonly DiagnosticDescriptor Rule =
new(DiagnosticId, Title, MessageFormat, Rules.Layout, Rules.InfoSeverity, Rules.DisabledByDefault, Description);
private static readonly HashSet<SyntaxKind> SupportedTypeDeclarationKinds =
[
SyntaxKind.ClassDeclaration,
SyntaxKind.StructDeclaration,
SyntaxKind.InterfaceDeclaration,
SyntaxKind.EnumDeclaration,
SyntaxKind.RecordDeclaration,
// Note: We don't care about SyntaxKind.DelegateDeclaration because those will usually be one-liners.
];
private static readonly HashSet<string> DesignerGeneratedRegions =
[
// VS 2002/3 put the designer-generated code in the main file (since partial classes didn't exist until VS 2005).
// We'll ignore those designer-generated regions.
"Windows Form Designer generated code",
"Component Designer generated code",
];
#endregion
#region Public Properties
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
#endregion
#region Public Methods
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.RegisterSyntaxTreeActionHonorExclusions(this, this.HandleSyntaxTree);
}
#endregion
#region Private Methods
private static bool ContainsMultipleTypeDeclarations(SyntaxNode root)
{
bool result = root.DescendantNodesAndSelf()
.Where(node => SupportedTypeDeclarationKinds.Contains(node.Kind()))
.Skip(1)
.Any();
return result;
}
private static bool HasExistingRegions(SyntaxTreeAnalysisContext context, SyntaxNode root)
{
IEnumerable<TextSpan> regionSpans = GetExistingRegionSpans(root);
bool result = regionSpans.Any();
if (result)
{
// We know something in the text uses a #region, so make sure any types use #regions around all their members.
IEnumerable<SyntaxNode> typeNodes = root.DescendantNodesAndSelf()
.Where(node => SupportedTypeDeclarationKinds.Contains(node.Kind()));
foreach (SyntaxNode typeNode in typeNodes)
{
// Use ChildNodes instead of DescendentNodes because we don't want to pick up any class XML comments.
// Also, look after the open brace to make sure we ignore base class references, class constraints, etc.
BaseTypeDeclarationSyntax baseType = (BaseTypeDeclarationSyntax)typeNode;
int openBraceEnd = baseType.OpenBraceToken.Span.End;
IEnumerable<SyntaxNode> unregionedNodes = typeNode.ChildNodes()
.Where(node => node.SpanStart > openBraceEnd && !regionSpans.Any(regionSpan => regionSpan.Contains(node.Span)));
if (unregionedNodes.Any())
{
string message = " around all members in " + baseType.Identifier.ValueText;
Location firstLineLocation = typeNode.GetFirstLineLocation();
context.ReportDiagnostic(Diagnostic.Create(Rule, firstLineLocation, message));
continue;
}
}
// Also, make sure that all using directives are in #regions.
IEnumerable<SyntaxNode> unregionedUsingNodes = root.DescendantNodesAndSelf()
.Where(node => node.IsKind(SyntaxKind.UsingDirective) && !regionSpans.Any(regionSpan => regionSpan.Contains(node.Span)));
if (unregionedUsingNodes.Any())
{
Location location = unregionedUsingNodes.First().GetFirstLineLocation();
context.ReportDiagnostic(Diagnostic.Create(Rule, location, " around using directives"));
}
}
return result;
}
private static IEnumerable<TextSpan> GetExistingRegionSpans(SyntaxNode root)
{
IEnumerable<SyntaxNode> regionNodes = root.DescendantNodesAndSelf(descendIntoTrivia: true)
.Where(node => node.IsKind(SyntaxKind.RegionDirectiveTrivia) || node.IsKind(SyntaxKind.EndRegionDirectiveTrivia));
// In a perfect world, the #region and #endregion directives will be balanced (even if they're nested).
// But we have to gracefully handle if they're mismatched or out of order.
var regionBounds = new List<Tuple<RegionDirectiveTriviaSyntax, EndRegionDirectiveTriviaSyntax>>();
Stack<RegionDirectiveTriviaSyntax> regionStack = new();
foreach (SyntaxNode node in regionNodes)
{
if (node.IsKind(SyntaxKind.RegionDirectiveTrivia))
{
regionStack.Push((RegionDirectiveTriviaSyntax)node);
}
else if (regionStack.Count > 0)
{
RegionDirectiveTriviaSyntax regionStart = regionStack.Pop();
regionBounds.Add(Tuple.Create(regionStart, (EndRegionDirectiveTriviaSyntax)node));
}
}
IEnumerable<TextSpan> result = regionBounds
.Where(tuple => !DesignerGeneratedRegions.Contains(tuple.Item1.DirectiveNameToken.ValueText ?? string.Empty))
.Select(tuple => TextSpan.FromBounds(tuple.Item1.FullSpan.Start, tuple.Item2.FullSpan.End));
return result;
}
private void HandleSyntaxTree(SyntaxTreeAnalysisContext context)
{
SyntaxTree tree = context.Tree;
SyntaxNode root = tree.GetRoot(context.CancellationToken);
if (root != null && !HasExistingRegions(context, root) && tree.TryGetText(out SourceText? text))
{
int maxLength = this.Settings.MaxUnregionedLines;
int fileLength = text.Lines.Count;
if (fileLength > maxLength)
{
var fileLocation = Rules.GetFileLocation(tree, text);
string message = $" because {fileLocation.Item1} is longer than {maxLength} lines (now {fileLength})";
context.ReportDiagnostic(Diagnostic.Create(Rule, fileLocation.Item2, message));
}
else if (ContainsMultipleTypeDeclarations(root))
{
var fileLocation = Rules.GetFileLocation(tree, text);
string message = $" because {fileLocation.Item1} contains multiple type declarations";
context.ReportDiagnostic(Diagnostic.Create(Rule, fileLocation.Item2, message));
}
}
}
#endregion
}