Skip to content

Commit 9a89a06

Browse files
fix: Diagnostic on empty regions (#715)
Co-authored-by: Brian Collamore <57269455+bcollamore@users.noreply.github.com>
1 parent a4abef1 commit 9a89a06

17 files changed

Lines changed: 152 additions & 73 deletions
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# PH2141: Avoid Empty Regions
2+
3+
| Property | Value |
4+
|--|--|
5+
| Package | [Philips.CodeAnalysis.MaintainabilityAnalyzers](https://www.nuget.org/packages/Philips.CodeAnalysis.MaintainabilityAnalyzers) |
6+
| Diagnostic ID | PH2141 |
7+
| Category | [Readability](../Readability.md) |
8+
| Analyzer | [EnforceRegionsAnalyzer](https://github.com/philips-software/roslyn-analyzers/blob/main/Philips.CodeAnalysis.MaintainabilityAnalyzers/Readability/EnforceRegionsAnalyzer.cs)
9+
| CodeFix | No |
10+
| Severity | Error |
11+
| Enabled By Default | Yes |
12+
13+
## Introduction
14+
15+
Avoid writing regions with no code inside.
16+
17+
## How to solve
18+
19+
Remove the empty region.
20+
21+
## Example
22+
23+
Code that triggers a diagnostic:
24+
``` cs
25+
class BadExample
26+
{
27+
#region Put some stuff here later
28+
#endregion
29+
}
30+
31+
```
32+
33+
And the replacement code:
34+
``` cs
35+
class GoodExample
36+
{
37+
#region Public Interface
38+
39+
public void GoodMethod()
40+
{
41+
return;
42+
}
43+
44+
#endregion
45+
}
46+
47+
```
48+
49+
## Configuration
50+
51+
This analyzer does not offer any special configuration. The general ways of [suppressing](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/suppress-warnings) diagnostics apply.

Philips.CodeAnalysis.Common/DiagnosticId.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ public enum DiagnosticId
134134
AvoidVoidReturn = 2138,
135135
EnableDocumentationCreation = 2139,
136136
AvoidExcludeFromCodeCoverage = 2140,
137+
AvoidEmptyRegions = 2141,
137138
AvoidCastToString = 2142,
138139
AvoidAssemblyGetEntryAssembly = 2143,
139140
}

Philips.CodeAnalysis.MaintainabilityAnalyzers/LocationRangeModel.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
// © 2019 Koninklijke Philips N.V. See License.md in the project root for license information.
22

3+
using Microsoft.CodeAnalysis;
4+
35
namespace Philips.CodeAnalysis.MaintainabilityAnalyzers
46
{
57
public class LocationRangeModel
68
{
7-
public LocationRangeModel(int startLine, int endLine)
9+
public LocationRangeModel(Location location, int startLine, int endLine)
810
{
11+
Location = location;
912
StartLine = startLine;
1013
EndLine = endLine;
1114
}
1215

16+
public Location Location { get; }
17+
1318
public int StartLine { get; }
1419

1520
public int EndLine { get; set; }

Philips.CodeAnalysis.MaintainabilityAnalyzers/Philips.CodeAnalysis.MaintainabilityAnalyzers.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@
123123
* Introduce PH2136: Avoid duplicate strings.
124124
* Introduce PH2139: Enable documentation creation.
125125
* Introduce PH2140: Avoid ExcludeFromCodeCoverage attribute.
126+
* Introduce PH2141: Avoid Empty Regions.
126127
* Introduce PH2142: Avoid casting to String.
127128
* Introduce PH2143: Avoid Assembly.GetEntryAssembly().
128129
</PackageReleaseNotes>

Philips.CodeAnalysis.MaintainabilityAnalyzers/Philips.CodeAnalysis.MaintainabilityAnalyzers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,5 +90,5 @@
9090
| [PH2138](../Documentation/Diagnostics/PH2138.md) | Avoid void returns | Methods that return void are mysterious. |
9191
| [PH2139](../Documentation/Diagnostics/PH2139.md) | Enable documentation creation | Add XML documentation generation to the project file, to be able to see Diagnostics for XML documentation. |
9292
| [PH2140](../Documentation/Diagnostics/PH2140.md) | Avoid ExcludeFromCodeCoverage attribute | Avoid the usage of the 'ExcludeFromCodeCoverage' attribute. |
93-
| [PH2142](../Documentation/Diagnostics/PH2142.md) | Avoid Casting to String | Avoid casting to int, use `object.ToString()` or a serialization solution instead. |
93+
| [PH2141](../Documentation/Diagnostics/PH2141.md) | Avoid Empty Regions | Avoid writing regions that have no code inside. |
9494
| [PH2143](../Documentation/Diagnostics/PH2143.md) | Avoid Assembly.GetEntryAssembly() | Avoid using Assembly.GetEntryAssembly(), as it might give different results when your code runs under a TestRunner. |

Philips.CodeAnalysis.MaintainabilityAnalyzers/Readability/EnforceRegionsAnalyzer.cs

Lines changed: 61 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Microsoft.CodeAnalysis.CSharp;
99
using Microsoft.CodeAnalysis.CSharp.Syntax;
1010
using Microsoft.CodeAnalysis.Diagnostics;
11+
using Microsoft.CodeAnalysis.Text;
1112
using Philips.CodeAnalysis.Common;
1213

1314
namespace Philips.CodeAnalysis.MaintainabilityAnalyzers.Readability
@@ -28,13 +29,19 @@ public class EnforceRegionsAnalyzer : DiagnosticAnalyzerBase
2829
private const string EnforceNonDuplicateRegionCategory = Categories.Readability;
2930

3031
private const string NonCheckedRegionMemberTitle = @"Members location relative to regions (Enforce Region Analyzer).";
31-
public const string NonCheckedRegionMemberTitleMessageFormat = @"Member's location relative to region {0} should be verified in Enforce Regions Analyzer";
32-
private const string NonCheckedRegionMemberTitleDescription = @"Member's location relative to region {0} should be verified. Implement the check in enforce region analyer";
33-
private const string NonCheckedRegionMemberTitleCategory = Categories.Readability;
32+
public const string NonCheckedRegionMemberMessageFormat = @"Member's location relative to region {0} should be verified in Enforce Regions Analyzer";
33+
private const string NonCheckedRegionMemberDescription = @"Member's location relative to region {0} should be verified. Implement the check in enforce region analyer";
34+
private const string NonCheckedRegionMemberCategory = Categories.Readability;
35+
36+
private const string AvoidEmptyRegionTitle = @"Avoid empty regions";
37+
public const string AvoidEmptyRegionMessageFormat = @"Remove the empty region";
38+
private const string AvoidEmptyRegionDescription = @"Remove the empty region";
39+
private const string AvoidEmptyRegionCategory = Categories.Readability;
3440

3541
private const string NonPublicDataMembersRegion = "Non-Public Data Members";
3642
private const string NonPublicPropertiesAndMethodsRegion = "Non-Public Properties/Methods";
3743
private const string PublicInterfaceRegion = "Public Interface";
44+
private const string EmptyRegion = "Empty region";
3845

3946
private static readonly Dictionary<string, Func<IReadOnlyList<MemberDeclarationSyntax>, SyntaxNodeAnalysisContext, bool>> RegionChecks = new()
4047
{
@@ -49,38 +56,51 @@ public class EnforceRegionsAnalyzer : DiagnosticAnalyzerBase
4956
EnforceNonDuplicateRegionTitle, EnforceNonDuplicateRegionMessageFormat, EnforceNonDuplicateRegionCategory,
5057
DiagnosticSeverity.Error, isEnabledByDefault: true, description: EnforceNonDuplicateRegionDescription);
5158
private static readonly DiagnosticDescriptor NonCheckedMember = new(DiagnosticId.NonCheckedRegionMember.ToId(),
52-
NonCheckedRegionMemberTitle, NonCheckedRegionMemberTitleMessageFormat, NonCheckedRegionMemberTitleCategory,
53-
DiagnosticSeverity.Info, isEnabledByDefault: true, description: NonCheckedRegionMemberTitleDescription);
59+
NonCheckedRegionMemberTitle, NonCheckedRegionMemberMessageFormat, NonCheckedRegionMemberCategory,
60+
DiagnosticSeverity.Info, isEnabledByDefault: true, description: NonCheckedRegionMemberDescription);
61+
private static readonly DiagnosticDescriptor AvoidEmpty = new(DiagnosticId.AvoidEmptyRegions.ToId(), AvoidEmptyRegionTitle,
62+
AvoidEmptyRegionMessageFormat, AvoidEmptyRegionCategory, DiagnosticSeverity.Error, isEnabledByDefault: true, description: AvoidEmptyRegionDescription);
5463

5564

56-
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(EnforceMemberLocation, EnforceNonDupliateRegion, NonCheckedMember);
65+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(EnforceMemberLocation, EnforceNonDupliateRegion, NonCheckedMember, AvoidEmpty);
5766

5867
protected override void InitializeCompilation(CompilationStartAnalysisContext context)
5968
{
60-
context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.ClassDeclaration);
69+
context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.ClassDeclaration, SyntaxKind.StructDeclaration);
6170
}
6271

6372
private static void Analyze(SyntaxNodeAnalysisContext context)
6473
{
65-
var classDeclaration = (ClassDeclarationSyntax)context.Node;
74+
var typeDeclaration = (TypeDeclarationSyntax)context.Node;
6675

6776
RegionVisitor visitor = new();
6877

69-
visitor.Visit(classDeclaration);
78+
visitor.Visit(typeDeclaration);
7079

7180
IReadOnlyList<DirectiveTriviaSyntax> regions = visitor.Regions;
7281

82+
// Region directives should come in pairs
83+
if (regions.Count == 1)
84+
{
85+
return;
86+
}
87+
7388
Dictionary<string, LocationRangeModel> regionLocations = PopulateRegionLocations(regions, context);
7489

7590
if (regionLocations.Count == 0)
7691
{
7792
return;
7893
}
7994

80-
SyntaxList<MemberDeclarationSyntax> members = classDeclaration.Members;
95+
SyntaxList<MemberDeclarationSyntax> members = typeDeclaration.Members;
8196

8297
foreach (KeyValuePair<string, LocationRangeModel> pair in regionLocations)
8398
{
99+
if (!MemberPresentInRegion(typeDeclaration, pair.Value))
100+
{
101+
CheckEmptyRegion(pair.Value, members, context);
102+
}
103+
84104
if (RegionChecks.TryGetValue(pair.Key, out Func<IReadOnlyList<MemberDeclarationSyntax>, SyntaxNodeAnalysisContext, bool> functionToCall))
85105
{
86106
IReadOnlyList<MemberDeclarationSyntax> membersOfRegion = GetMembersOfRegion(members, pair.Value);
@@ -94,7 +114,7 @@ private static string GetRegionName(DirectiveTriviaSyntax region)
94114
{
95115
var regionName = string.Empty;
96116

97-
Microsoft.CodeAnalysis.Text.TextLineCollection lines = region.GetText().Lines;
117+
TextLineCollection lines = region.GetText().Lines;
98118

99119
if (lines.Count > 0)
100120
{
@@ -119,21 +139,19 @@ private static void PopulateRegionLocation(ref string regionStartName, Dictionar
119139
return;
120140
}
121141

122-
if (RegionChecks.ContainsKey(regionName))
142+
if (regionLocations.Remove(regionName))
123143
{
124-
if (regionLocations.Remove(regionName))
125-
{
126-
Location memberLocation = region.DirectiveNameToken.GetLocation();
127-
CreateDiagnostic(memberLocation, context, regionName, EnforceNonDupliateRegion);
128-
}
129-
else
130-
{
131-
Location location = region.GetLocation();
132-
var lineNumber = GetMemberLineNumber(location);
133-
134-
regionLocations.Add(regionName, new LocationRangeModel(lineNumber, lineNumber));
135-
regionStartName = regionName;
136-
}
144+
Location memberLocation = region.DirectiveNameToken.GetLocation();
145+
CreateDiagnostic(memberLocation, context, regionName, EnforceNonDupliateRegion);
146+
}
147+
else
148+
{
149+
Location location = region.GetLocation();
150+
Location memberLocation = region.DirectiveNameToken.GetLocation();
151+
var lineNumber = GetMemberLineNumber(location);
152+
153+
regionLocations.Add(regionName, new LocationRangeModel(memberLocation, lineNumber, lineNumber));
154+
regionStartName = regionName;
137155
}
138156
}
139157
else
@@ -162,7 +180,7 @@ private static void PopulateRegionLocation(ref string regionStartName, Dictionar
162180
private static Dictionary<string, LocationRangeModel> PopulateRegionLocations(IReadOnlyList<DirectiveTriviaSyntax> regions, SyntaxNodeAnalysisContext context)
163181
{
164182
Dictionary<string, LocationRangeModel> regionLocations = [];
165-
var regionStartName = "";
183+
var regionStartName = string.Empty;
166184
for (var i = 0; i < regions.Count; i++)
167185
{
168186
DirectiveTriviaSyntax region = regions[i];
@@ -188,11 +206,11 @@ private static List<MemberDeclarationSyntax> GetMembersOfRegion(SyntaxList<Membe
188206
/// <param name="member"></param>
189207
/// <param name="locationRange"></param>
190208
/// <returns>true if member is inside the given region, else false</returns>
191-
private static bool MemberPresentInRegion(MemberDeclarationSyntax member, LocationRangeModel locationRange)
209+
private static bool MemberPresentInRegion(SyntaxNode member, LocationRangeModel locationRange)
192210
{
193211
Location location = member.GetLocation();
194-
var memberLocation = GetMemberLineNumber(location);
195-
return memberLocation > locationRange.StartLine && memberLocation < locationRange.EndLine;
212+
var memberLine = GetMemberLineNumber(location);
213+
return memberLine > locationRange.StartLine && memberLine < locationRange.EndLine;
196214
}
197215

198216
/// <summary>
@@ -217,13 +235,25 @@ private static bool CheckMembersOfPublicInterfaceRegion(IReadOnlyList<MemberDecl
217235
return true;
218236
}
219237

238+
/// <summary>
239+
/// Checks whether the regions are empty, contain no statements.
240+
/// </summary>
241+
private static void CheckEmptyRegion(LocationRangeModel locationRange, IReadOnlyList<MemberDeclarationSyntax> members, SyntaxNodeAnalysisContext context)
242+
{
243+
var foundMemberInside = members.Any(m => MemberPresentInRegion(m, locationRange));
244+
245+
if (!foundMemberInside)
246+
{
247+
// Empty region
248+
CreateDiagnostic(locationRange.Location, context, EmptyRegion, AvoidEmpty);
249+
}
250+
}
251+
220252
/// <summary>
221253
/// Verify whether the given member belongs to the public interface region
222254
/// If the member is of type field, then throw an error
223255
/// If the member is of type method, then check if its non-public
224256
/// </summary>
225-
/// <param name="member"></param>
226-
/// <param name="context"></param>
227257
private static void VerifyMemberForPublicInterfaceRegion(MemberDeclarationSyntax member, SyntaxNodeAnalysisContext context)
228258
{
229259
if (TryGetModifiers(member, true, out SyntaxTokenList modifiers))

Philips.CodeAnalysis.Test/Maintainability/Maintainability/ProhibitDynamicKeywordAnalyzerTest.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,6 @@ namespace Philips.CodeAnalysis.Test.Maintainability.Maintainability
1313
[TestClass]
1414
public class ProhibitDynamicKeywordAnalyzerTest : DiagnosticVerifier
1515
{
16-
#region Non-Public Data Members
17-
18-
#endregion
19-
2016
#region Non-Public Properties/Methods
2117

2218
protected override DiagnosticAnalyzer GetDiagnosticAnalyzer()

Philips.CodeAnalysis.Test/Maintainability/Readability/EnforceRegionsTest.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,37 @@ class Foo
195195
await VerifyError(baseline, given, isError, 6, 2).ConfigureAwait(false);
196196
}
197197

198+
[TestMethod]
199+
[TestCategory(TestDefinitions.UnitTests)]
200+
public async Task AvoidEmptyRegion()
201+
{
202+
var givenText = @"
203+
class C {{
204+
#region Some Small Region
205+
206+
#endregion
207+
}}
208+
";
209+
await VerifyDiagnostic(givenText, DiagnosticId.AvoidEmptyRegions, regex: EnforceRegionsAnalyzer.AvoidEmptyRegionMessageFormat, line: 3, column: 4).ConfigureAwait(false);
210+
}
211+
212+
[TestMethod]
213+
[TestCategory(TestDefinitions.UnitTests)]
214+
public async Task AvoidRegionInsideMethod()
215+
{
216+
var givenText = @"
217+
class C {{
218+
public void LongMethod() {{
219+
private int i = 0;
220+
#region Inside Method
221+
i++;
222+
#endregion
223+
}}
224+
}}
225+
";
226+
await VerifySuccessfulCompilation(givenText);
227+
}
228+
198229
[DataTestMethod]
199230
[DataRow(@"#region Constants", false)]
200231
[DataRow(@"#region Public Properties/Methods", false)]

Philips.CodeAnalysis.Test/Moq/MockObjectsMustCallExistingConstructorsAnalyzerTest.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,6 @@ namespace Philips.CodeAnalysis.Test.Moq
1616
[TestClass]
1717
public class MockObjectsMustCallExistingConstructorsAnalyzerTest : DiagnosticVerifier
1818
{
19-
#region Non-Public Data Members
20-
21-
#endregion
22-
2319
#region Non-Public Properties/Methods
2420
protected override DiagnosticAnalyzer GetDiagnosticAnalyzer()
2521
{

Philips.CodeAnalysis.Test/MsTest/AssertAreEqualLiteralAnalyzerTest.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@ namespace Philips.CodeAnalysis.Test.MsTest
1414
[TestClass]
1515
public class AssertAreEqualLiteralAnalyzerTest : AssertCodeFixVerifier
1616
{
17-
#region Non-Public Data Members
18-
19-
#endregion
20-
2117
#region Non-Public Properties/Methods
2218

2319
protected override DiagnosticResult GetExpectedDiagnostic(int expectedLineNumberErrorOffset = 0, int expectedColumnErrorOffset = 0)

0 commit comments

Comments
 (0)