Skip to content

Commit 4701aa2

Browse files
authored
Merge branch 'main' into copilot/fix-888
2 parents 9eb5794 + 3e662b4 commit 4701aa2

9 files changed

Lines changed: 553 additions & 888 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
name: copilot-setup-steps
2+
on:
3+
workflow_dispatch:
4+
jobs:
5+
copilot-setup-steps:
6+
runs-on: ubuntu-latest
7+
permissions:
8+
contents: read
9+
id-token: write
10+
environment: copilot
11+
steps:
12+
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
13+
- name: Python deps for MCP server
14+
run: |
15+
python3 -m pip install --upgrade pip
16+
python3 -m pip install "fastmcp>=0.4"
17+
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// © 2025 Koninklijke Philips N.V. See License.md in the project root for license information.
2+
3+
using System.Collections.Immutable;
4+
using System.Linq;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CSharp;
7+
using Microsoft.CodeAnalysis.CSharp.Syntax;
8+
using Microsoft.CodeAnalysis.Diagnostics;
9+
using Microsoft.CodeAnalysis.Text;
10+
using Philips.CodeAnalysis.Common;
11+
12+
namespace Philips.CodeAnalysis.MaintainabilityAnalyzers.Readability
13+
{
14+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
15+
public class AvoidEmptyRegionsAnalyzer : DiagnosticAnalyzerBase
16+
{
17+
private const string AvoidEmptyRegionTitle = @"Avoid empty regions";
18+
public const string AvoidEmptyRegionMessageFormat = @"Remove the empty region";
19+
private const string AvoidEmptyRegionDescription = @"Remove the empty region";
20+
private const string AvoidEmptyRegionCategory = Categories.Readability;
21+
22+
private static readonly DiagnosticDescriptor AvoidEmpty = new(DiagnosticId.AvoidEmptyRegions.ToId(), AvoidEmptyRegionTitle,
23+
AvoidEmptyRegionMessageFormat, AvoidEmptyRegionCategory, DiagnosticSeverity.Error, isEnabledByDefault: true, description: AvoidEmptyRegionDescription);
24+
25+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(AvoidEmpty);
26+
27+
protected override void InitializeCompilation(CompilationStartAnalysisContext context)
28+
{
29+
context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.RegionDirectiveTrivia);
30+
}
31+
32+
private static void Analyze(SyntaxNodeAnalysisContext context)
33+
{
34+
var regionDirective = (RegionDirectiveTriviaSyntax)context.Node;
35+
36+
// Find the matching #endregion
37+
EndRegionDirectiveTriviaSyntax endRegionDirective = FindMatchingEndRegion(regionDirective);
38+
if (endRegionDirective == null)
39+
{
40+
return; // No matching end region found
41+
}
42+
43+
// Check if the region is empty (contains only whitespace and comments)
44+
if (IsRegionEmpty(regionDirective, endRegionDirective, context.SemanticModel.SyntaxTree.GetText()))
45+
{
46+
var diagnostic = Diagnostic.Create(AvoidEmpty, regionDirective.GetLocation());
47+
context.ReportDiagnostic(diagnostic);
48+
}
49+
}
50+
51+
private static EndRegionDirectiveTriviaSyntax FindMatchingEndRegion(RegionDirectiveTriviaSyntax regionStart)
52+
{
53+
SyntaxTree syntaxTree = regionStart.SyntaxTree;
54+
SyntaxNode root = syntaxTree.GetRoot();
55+
56+
var regionDepth = 1;
57+
var startPosition = regionStart.SpanStart;
58+
59+
// Find all region and endregion directives after the current region
60+
IOrderedEnumerable<SyntaxTrivia> allDirectives = root.DescendantTrivia(descendIntoTrivia: true)
61+
.Where(trivia => trivia.SpanStart > startPosition &&
62+
(trivia.IsKind(SyntaxKind.RegionDirectiveTrivia) ||
63+
trivia.IsKind(SyntaxKind.EndRegionDirectiveTrivia)))
64+
.OrderBy(trivia => trivia.SpanStart);
65+
66+
foreach (SyntaxTrivia trivia in allDirectives)
67+
{
68+
if (trivia.IsKind(SyntaxKind.RegionDirectiveTrivia))
69+
{
70+
regionDepth++;
71+
}
72+
else if (trivia.IsKind(SyntaxKind.EndRegionDirectiveTrivia))
73+
{
74+
regionDepth--;
75+
if (regionDepth == 0)
76+
{
77+
return (EndRegionDirectiveTriviaSyntax)trivia.GetStructure();
78+
}
79+
}
80+
}
81+
82+
return null;
83+
}
84+
85+
private static bool IsRegionEmpty(RegionDirectiveTriviaSyntax regionStart, EndRegionDirectiveTriviaSyntax regionEnd, SourceText sourceText)
86+
{
87+
var regionStartLine = regionStart.GetLocation().GetLineSpan().StartLinePosition.Line;
88+
var regionEndLine = regionEnd.GetLocation().GetLineSpan().StartLinePosition.Line;
89+
90+
// Check each line between the region start and end
91+
for (var lineNumber = regionStartLine + 1; lineNumber < regionEndLine; lineNumber++)
92+
{
93+
TextLine line = sourceText.Lines[lineNumber];
94+
var lineText = line.ToString().Trim();
95+
96+
// If we find any non-empty, non-comment line, the region is not empty
97+
if (!string.IsNullOrEmpty(lineText) && !lineText.StartsWith("//"))
98+
{
99+
return false;
100+
}
101+
}
102+
103+
return true; // Region contains only whitespace and/or comments
104+
}
105+
}
106+
}

Philips.CodeAnalysis.MaintainabilityAnalyzers/Readability/EnforceRegionsAnalyzer.cs

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,12 @@ public class EnforceRegionsAnalyzer : DiagnosticAnalyzerBase
3333
private const string NonCheckedRegionMemberDescription = @"Member's location relative to region {0} should be verified. Implement the check in enforce region analyer";
3434
private const string NonCheckedRegionMemberCategory = Categories.Readability;
3535

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;
36+
4037

4138
private const string NonPublicDataMembersRegion = "Non-Public Data Members";
4239
private const string NonPublicPropertiesAndMethodsRegion = "Non-Public Properties/Methods";
4340
private const string PublicInterfaceRegion = "Public Interface";
44-
private const string EmptyRegion = "Empty region";
41+
4542

4643
private static readonly Dictionary<string, Func<IReadOnlyList<MemberDeclarationSyntax>, SyntaxNodeAnalysisContext, bool>> RegionChecks = new()
4744
{
@@ -58,11 +55,9 @@ public class EnforceRegionsAnalyzer : DiagnosticAnalyzerBase
5855
private static readonly DiagnosticDescriptor NonCheckedMember = new(DiagnosticId.NonCheckedRegionMember.ToId(),
5956
NonCheckedRegionMemberTitle, NonCheckedRegionMemberMessageFormat, NonCheckedRegionMemberCategory,
6057
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);
6358

6459

65-
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(EnforceMemberLocation, EnforceNonDuplicateRegion, NonCheckedMember, AvoidEmpty);
60+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(EnforceMemberLocation, EnforceNonDuplicateRegion, NonCheckedMember);
6661

6762
protected override void InitializeCompilation(CompilationStartAnalysisContext context)
6863
{
@@ -111,11 +106,6 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
111106

112107
foreach (KeyValuePair<string, LocationRangeModel> pair in regionLocations)
113108
{
114-
if (!MemberPresentInRegion(typeDeclaration, pair.Value))
115-
{
116-
CheckEmptyRegion(pair.Value, members, context);
117-
}
118-
119109
if (RegionChecks.TryGetValue(pair.Key, out Func<IReadOnlyList<MemberDeclarationSyntax>, SyntaxNodeAnalysisContext, bool> functionToCall))
120110
{
121111
IReadOnlyList<MemberDeclarationSyntax> membersOfRegion = GetMembersOfRegion(members, pair.Value);
@@ -125,6 +115,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
125115

126116
}
127117

118+
128119
private static string GetRegionName(DirectiveTriviaSyntax region)
129120
{
130121
var regionName = string.Empty;
@@ -251,19 +242,6 @@ private static bool CheckMembersOfPublicInterfaceRegion(IReadOnlyList<MemberDecl
251242
return true;
252243
}
253244

254-
/// <summary>
255-
/// Checks whether the regions are empty, contain no statements.
256-
/// </summary>
257-
private static void CheckEmptyRegion(LocationRangeModel locationRange, IReadOnlyList<MemberDeclarationSyntax> members, SyntaxNodeAnalysisContext context)
258-
{
259-
var foundMemberInside = members.Any(m => MemberPresentInRegion(m, locationRange));
260-
261-
if (!foundMemberInside)
262-
{
263-
// Empty region
264-
CreateDiagnostic(locationRange.Location, context, EmptyRegion, AvoidEmpty);
265-
}
266-
}
267245

268246
/// <summary>
269247
/// Verify whether the given member belongs to the public interface region

0 commit comments

Comments
 (0)