Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions Documentation/Diagnostics/PH2141.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@

## Introduction

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

## How to solve

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

## Example

Expand All @@ -26,6 +26,10 @@ class BadExample
{
#region Put some stuff here later
#endregion

#region Just a comment
// This is just a regular comment
#endregion
}

```
Expand All @@ -42,10 +46,16 @@ class GoodExample
}

#endregion

#region Copyright Information
// © 2025 Koninklijke Philips N.V. All rights reserved.
#endregion
}

```

Note: Regions containing copyright information (with copyright symbols or "Copyright" keyword and a year) are not considered empty.

## Configuration

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.
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
// © 2025 Koninklijke Philips N.V. See License.md in the project root for license information.

using System;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
Expand All @@ -22,6 +25,9 @@ public class AvoidEmptyRegionsAnalyzer : DiagnosticAnalyzerBase
private static readonly DiagnosticDescriptor AvoidEmpty = new(DiagnosticId.AvoidEmptyRegions.ToId(), AvoidEmptyRegionTitle,
AvoidEmptyRegionMessageFormat, AvoidEmptyRegionCategory, DiagnosticSeverity.Error, isEnabledByDefault: true, description: AvoidEmptyRegionDescription);

private static readonly Regex CopyrightRegex = new(@"©|\uFFFD|Copyright", RegexOptions.Singleline | RegexOptions.Compiled | RegexOptions.IgnoreCase, TimeSpan.FromSeconds(1));
private static readonly Regex YearRegex = new(@"\d\d\d\d", RegexOptions.Singleline | RegexOptions.Compiled, TimeSpan.FromSeconds(1));

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(AvoidEmpty);

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

var hasCommentContent = false;

// Check each line between the region start and end
for (var lineNumber = regionStartLine + 1; lineNumber < regionEndLine; lineNumber++)
{
TextLine line = sourceText.Lines[lineNumber];
var lineText = line.ToString().Trim();

// If we find any non-empty, non-comment line, the region is not empty
if (!string.IsNullOrEmpty(lineText) && !lineText.StartsWith("//"))
// Skip empty lines
if (string.IsNullOrEmpty(lineText))
{
continue;
}

// If we find any non-comment line, the region is definitely not empty
if (!lineText.StartsWith("//"))
Comment thread
pachecoke marked this conversation as resolved.
{
return false;
}

// Track that we have comment content
hasCommentContent = true;
}

// If we have no content at all (only whitespace), the region is empty
if (!hasCommentContent)
{
return true;
}

return true; // Region contains only whitespace and/or comments
// We have only comments - check if they contain copyright information
return !ContainsCopyrightInformation(regionStart, regionEnd, sourceText);
}

private static bool ContainsCopyrightInformation(RegionDirectiveTriviaSyntax regionStart, EndRegionDirectiveTriviaSyntax regionEnd, SourceText sourceText)
{
var regionStartLine = regionStart.GetLocation().GetLineSpan().StartLinePosition.Line;
var regionEndLine = regionEnd.GetLocation().GetLineSpan().StartLinePosition.Line;

var commentTextBuilder = new StringBuilder();

// Collect all comment text in the region
for (var lineNumber = regionStartLine + 1; lineNumber < regionEndLine; lineNumber++)
{
TextLine line = sourceText.Lines[lineNumber];
var lineText = line.ToString().Trim();

if (!string.IsNullOrEmpty(lineText) && lineText.StartsWith("//"))
{
_ = commentTextBuilder.Append(lineText).Append(' ');
}
}

var allCommentText = commentTextBuilder.ToString();

// Check if the comments contain copyright information
var hasCopyright = CopyrightRegex.IsMatch(allCommentText);
var hasYear = YearRegex.IsMatch(allCommentText);

return hasCopyright && hasYear;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,62 @@ public class ObjectList {{ }}
await VerifySuccessfulCompilation(givenText).ConfigureAwait(false);
}

[TestMethod]
[TestCategory(TestDefinitions.UnitTests)]
public async Task RegionWithCopyrightShouldNotTriggerDiagnostic()
{
const string input = @"public class Foo
{
#region Header
// © 2019 Koninklijke Philips N.V. All rights reserved.
// Reproduction or transmission in whole or in part, in any form or by any means,
// electronic, mechanical or otherwise, is prohibited without the prior written consent of
// the owner.
#endregion
}";
await VerifySuccessfulCompilation(input).ConfigureAwait(false);
}

[TestMethod]
[TestCategory(TestDefinitions.UnitTests)]
public async Task RegionWithOnlyCopyrightCommentShouldNotTriggerDiagnostic()
{
const string input = @"public class Foo
{
#region Copyright
// © 2025 Koninklijke Philips N.V.
#endregion
}";
await VerifySuccessfulCompilation(input).ConfigureAwait(false);
}

[TestMethod]
[TestCategory(TestDefinitions.UnitTests)]
public async Task RegionWithRegularCommentsShouldTriggerDiagnostic()
{
const string input = @"public class Foo
{
#region TODO
// This is just a regular comment
// Nothing special here
#endregion
}";
await VerifyDiagnostic(input, DiagnosticId.AvoidEmptyRegions).ConfigureAwait(false);
}

[TestMethod]
[TestCategory(TestDefinitions.UnitTests)]
public async Task RegionWithCopyrightKeywordShouldNotTriggerDiagnostic()
{
const string input = @"public class Foo
{
#region License
// Copyright 2023 Koninklijke Philips N.V.
#endregion
}";
await VerifySuccessfulCompilation(input).ConfigureAwait(false);
}

protected override CodeFixProvider GetCodeFixProvider()
{
return new EnforceRegionsRemoveEmptyRegionCodeFixProvider();
Expand Down
Loading