Skip to content

Commit cc26e84

Browse files
Copilotbcollamore
andauthored
fix: PH2141 false positive for regions containing copyright comments (#961)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bcollamore <57269455+bcollamore@users.noreply.github.com>
1 parent 6edac6e commit cc26e84

3 files changed

Lines changed: 123 additions & 5 deletions

File tree

Documentation/Diagnostics/PH2141.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212

1313
## Introduction
1414

15-
Avoid writing regions with no code inside.
15+
Avoid writing regions with no code inside. Regions that contain only comments are considered empty, unless the comments contain copyright information.
1616

1717
## How to solve
1818

19-
Remove the empty region.
19+
Remove the empty region. If the region contains copyright information, it is not considered empty and no action is needed.
2020

2121
## Example
2222

@@ -26,6 +26,10 @@ class BadExample
2626
{
2727
#region Put some stuff here later
2828
#endregion
29+
30+
#region Just a comment
31+
// This is just a regular comment
32+
#endregion
2933
}
3034

3135
```
@@ -42,10 +46,16 @@ class GoodExample
4246
}
4347

4448
#endregion
49+
50+
#region Copyright Information
51+
// © 2025 Koninklijke Philips N.V. All rights reserved.
52+
#endregion
4553
}
4654

4755
```
4856

57+
Note: Regions containing copyright information (with copyright symbols or "Copyright" keyword and a year) are not considered empty.
58+
4959
## Configuration
5060

5161
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.MaintainabilityAnalyzers/Readability/AvoidEmptyRegionsAnalyzer.cs

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
// © 2025 Koninklijke Philips N.V. See License.md in the project root for license information.
22

3+
using System;
34
using System.Collections.Immutable;
45
using System.Linq;
6+
using System.Text;
7+
using System.Text.RegularExpressions;
58
using Microsoft.CodeAnalysis;
69
using Microsoft.CodeAnalysis.CSharp;
710
using Microsoft.CodeAnalysis.CSharp.Syntax;
@@ -22,6 +25,9 @@ public class AvoidEmptyRegionsAnalyzer : DiagnosticAnalyzerBase
2225
private static readonly DiagnosticDescriptor AvoidEmpty = new(DiagnosticId.AvoidEmptyRegions.ToId(), AvoidEmptyRegionTitle,
2326
AvoidEmptyRegionMessageFormat, AvoidEmptyRegionCategory, DiagnosticSeverity.Error, isEnabledByDefault: true, description: AvoidEmptyRegionDescription);
2427

28+
private static readonly Regex CopyrightRegex = new(@"©|\uFFFD|Copyright", RegexOptions.Singleline | RegexOptions.Compiled | RegexOptions.IgnoreCase, TimeSpan.FromSeconds(1));
29+
private static readonly Regex YearRegex = new(@"\d\d\d\d", RegexOptions.Singleline | RegexOptions.Compiled, TimeSpan.FromSeconds(1));
30+
2531
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(AvoidEmpty);
2632

2733
protected override void InitializeCompilation(CompilationStartAnalysisContext context)
@@ -87,20 +93,66 @@ private static bool IsRegionEmpty(RegionDirectiveTriviaSyntax regionStart, EndRe
8793
var regionStartLine = regionStart.GetLocation().GetLineSpan().StartLinePosition.Line;
8894
var regionEndLine = regionEnd.GetLocation().GetLineSpan().StartLinePosition.Line;
8995

96+
var hasCommentContent = false;
97+
9098
// Check each line between the region start and end
9199
for (var lineNumber = regionStartLine + 1; lineNumber < regionEndLine; lineNumber++)
92100
{
93101
TextLine line = sourceText.Lines[lineNumber];
94102
var lineText = line.ToString().Trim();
95103

96-
// If we find any non-empty, non-comment line, the region is not empty
97-
if (!string.IsNullOrEmpty(lineText) && !lineText.StartsWith("//"))
104+
// Skip empty lines
105+
if (string.IsNullOrEmpty(lineText))
106+
{
107+
continue;
108+
}
109+
110+
// If we find any non-comment line, the region is definitely not empty
111+
if (!lineText.StartsWith("//"))
98112
{
99113
return false;
100114
}
115+
116+
// Track that we have comment content
117+
hasCommentContent = true;
118+
}
119+
120+
// If we have no content at all (only whitespace), the region is empty
121+
if (!hasCommentContent)
122+
{
123+
return true;
101124
}
102125

103-
return true; // Region contains only whitespace and/or comments
126+
// We have only comments - check if they contain copyright information
127+
return !ContainsCopyrightInformation(regionStart, regionEnd, sourceText);
128+
}
129+
130+
private static bool ContainsCopyrightInformation(RegionDirectiveTriviaSyntax regionStart, EndRegionDirectiveTriviaSyntax regionEnd, SourceText sourceText)
131+
{
132+
var regionStartLine = regionStart.GetLocation().GetLineSpan().StartLinePosition.Line;
133+
var regionEndLine = regionEnd.GetLocation().GetLineSpan().StartLinePosition.Line;
134+
135+
var commentTextBuilder = new StringBuilder();
136+
137+
// Collect all comment text in the region
138+
for (var lineNumber = regionStartLine + 1; lineNumber < regionEndLine; lineNumber++)
139+
{
140+
TextLine line = sourceText.Lines[lineNumber];
141+
var lineText = line.ToString().Trim();
142+
143+
if (!string.IsNullOrEmpty(lineText) && lineText.StartsWith("//"))
144+
{
145+
_ = commentTextBuilder.Append(lineText).Append(' ');
146+
}
147+
}
148+
149+
var allCommentText = commentTextBuilder.ToString();
150+
151+
// Check if the comments contain copyright information
152+
var hasCopyright = CopyrightRegex.IsMatch(allCommentText);
153+
var hasYear = YearRegex.IsMatch(allCommentText);
154+
155+
return hasCopyright && hasYear;
104156
}
105157
}
106158
}

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,62 @@ public class ObjectList {{ }}
231231
await VerifySuccessfulCompilation(givenText).ConfigureAwait(false);
232232
}
233233

234+
[TestMethod]
235+
[TestCategory(TestDefinitions.UnitTests)]
236+
public async Task RegionWithCopyrightShouldNotTriggerDiagnostic()
237+
{
238+
const string input = @"public class Foo
239+
{
240+
#region Header
241+
// © 2019 Koninklijke Philips N.V. All rights reserved.
242+
// Reproduction or transmission in whole or in part, in any form or by any means,
243+
// electronic, mechanical or otherwise, is prohibited without the prior written consent of
244+
// the owner.
245+
#endregion
246+
}";
247+
await VerifySuccessfulCompilation(input).ConfigureAwait(false);
248+
}
249+
250+
[TestMethod]
251+
[TestCategory(TestDefinitions.UnitTests)]
252+
public async Task RegionWithOnlyCopyrightCommentShouldNotTriggerDiagnostic()
253+
{
254+
const string input = @"public class Foo
255+
{
256+
#region Copyright
257+
// © 2025 Koninklijke Philips N.V.
258+
#endregion
259+
}";
260+
await VerifySuccessfulCompilation(input).ConfigureAwait(false);
261+
}
262+
263+
[TestMethod]
264+
[TestCategory(TestDefinitions.UnitTests)]
265+
public async Task RegionWithRegularCommentsShouldTriggerDiagnostic()
266+
{
267+
const string input = @"public class Foo
268+
{
269+
#region TODO
270+
// This is just a regular comment
271+
// Nothing special here
272+
#endregion
273+
}";
274+
await VerifyDiagnostic(input, DiagnosticId.AvoidEmptyRegions).ConfigureAwait(false);
275+
}
276+
277+
[TestMethod]
278+
[TestCategory(TestDefinitions.UnitTests)]
279+
public async Task RegionWithCopyrightKeywordShouldNotTriggerDiagnostic()
280+
{
281+
const string input = @"public class Foo
282+
{
283+
#region License
284+
// Copyright 2023 Koninklijke Philips N.V.
285+
#endregion
286+
}";
287+
await VerifySuccessfulCompilation(input).ConfigureAwait(false);
288+
}
289+
234290
protected override CodeFixProvider GetCodeFixProvider()
235291
{
236292
return new EnforceRegionsRemoveEmptyRegionCodeFixProvider();

0 commit comments

Comments
 (0)