Skip to content

Commit 97c96aa

Browse files
authored
Merge pull request #1 from PandaTechAM/development
Bug fix on external interfaces
2 parents 95f4301 + 34438ac commit 97c96aa

File tree

7 files changed

+389
-20
lines changed

7 files changed

+389
-20
lines changed

src/Analyzers/Analyzers.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@
1818
<PackageId>Pandatech.Analyzers</PackageId>
1919
<PackageIcon>pandatech.png</PackageIcon>
2020
<PackageReadmeFile>Readme.md</PackageReadmeFile>
21-
<Version>1.0.0</Version>
21+
<Version>1.1.0</Version>
2222
<Authors>Pandatech</Authors>
2323
<Description>Pandatech Roslyn analyzers enforcing company coding rules.</Description>
2424
<PackageLicenseExpression>MIT</PackageLicenseExpression>
2525
<PackageTags>Pandatech, analyzers, roslyn, async, cancellation, coding-rules</PackageTags>
2626
<RepositoryUrl>https://github.com/PandaTechAM/be-lib-analyzers</RepositoryUrl>
27-
<PackageReleaseNotes>Init</PackageReleaseNotes>
27+
<PackageReleaseNotes>Bug fix on external interfaces</PackageReleaseNotes>
2828

2929

3030
</PropertyGroup>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using System.Linq;
2+
using Microsoft.CodeAnalysis;
3+
4+
namespace Pandatech.Analyzers.Async;
5+
6+
internal static class AsyncHelpers
7+
{
8+
internal static bool IsContractImplementation(this IMethodSymbol method)
9+
{
10+
if (method.IsOverride)
11+
{
12+
return true;
13+
}
14+
15+
if (!method.ExplicitInterfaceImplementations.IsEmpty)
16+
{
17+
return true;
18+
}
19+
20+
var containingType = method.ContainingType;
21+
if (containingType is null)
22+
{
23+
return false;
24+
}
25+
26+
foreach (var member in containingType.AllInterfaces.SelectMany(iface => iface.GetMembers(method.Name)))
27+
{
28+
if (member is not IMethodSymbol ifaceMethod)
29+
{
30+
continue;
31+
}
32+
33+
var implementation = containingType.FindImplementationForInterfaceMember(ifaceMethod);
34+
if (SymbolEqualityComparer.Default.Equals(implementation, method))
35+
{
36+
return true;
37+
}
38+
}
39+
40+
return false;
41+
}
42+
}

src/Analyzers/Async/AsyncMethodConventionsAnalyzer.cs

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,24 @@ private static void AnalyzeMethod(SymbolAnalysisContext context)
7777
return;
7878
}
7979

80+
// Only analyze methods declared in source (skip metadata / external assemblies).
81+
var hasSourceLocation = false;
82+
foreach (var location in method.Locations)
83+
{
84+
if (!location.IsInSource)
85+
{
86+
continue;
87+
}
88+
89+
hasSourceLocation = true;
90+
break;
91+
}
92+
93+
if (!hasSourceLocation)
94+
{
95+
return;
96+
}
97+
8098
if (method.ReturnType is not INamedTypeSymbol returnType)
8199
{
82100
return;
@@ -123,27 +141,42 @@ private static void AnalyzeAsyncMember(IMethodSymbol method,
123141
string displayName,
124142
Action<Diagnostic> report)
125143
{
126-
// PT0001 – name must end with Async (for named methods).
144+
var isContract = method.IsContractImplementation();
145+
146+
// PT0001 – name must end with Async (for named methods),
147+
// but we DO NOT enforce it on contract implementations (MediatR Handle, overrides, etc.).
127148
if (!displayName.Equals("anonymous function", StringComparison.Ordinal) &&
128-
!displayName.EndsWith("Async", StringComparison.Ordinal))
149+
!displayName.EndsWith("Async", StringComparison.Ordinal) &&
150+
!isContract)
129151
{
130152
report(Diagnostic.Create(AsyncSuffixRule, location, displayName));
131153
}
132154

133155
var ctInfo = GetCancellationTokenInfo(method);
134156

157+
// PT0002 – missing CancellationToken
158+
// Only enforced on non-contract methods (e.g. interface itself, normal class methods).
135159
if (!ctInfo.HasCt)
136160
{
137-
report(Diagnostic.Create(CancellationTokenMissingRule, location, displayName));
161+
if (!isContract)
162+
{
163+
report(Diagnostic.Create(CancellationTokenMissingRule, location, displayName));
164+
}
165+
138166
return;
139167
}
140168

169+
// PT0003 – name must be ct
170+
// Always enforced when CT exists (both interface + implementation),
171+
// so implementations still get "rename to ct".
141172
if (!ctInfo.IsNamedCt)
142173
{
143174
report(Diagnostic.Create(CancellationTokenNameRule, location, displayName, ctInfo.Name));
144175
}
145176

146-
if (!ctInfo.IsLast)
177+
// PT0004 – CT must be last
178+
// Only enforced on non-contract methods (interface / own class methods).
179+
if (!ctInfo.IsLast && !isContract)
147180
{
148181
report(Diagnostic.Create(CancellationTokenPositionRule, location, displayName, ctInfo.Name));
149182
}

0 commit comments

Comments
 (0)