Skip to content

Commit 49f6a53

Browse files
committed
feat: Enhance MissingExpressiveAnalyzer with enum handling and add related tests
1 parent 904a4ee commit 49f6a53

2 files changed

Lines changed: 122 additions & 1 deletion

File tree

src/ExpressiveSharp.CodeFixers/MissingExpressiveAnalyzer.cs

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,14 @@ private static void AnalyzeDescendants(SyntaxNodeAnalysisContext context, Syntax
126126
if (node is InvocationExpressionSyntax invocation)
127127
{
128128
var info = context.SemanticModel.GetSymbolInfo(invocation, context.CancellationToken);
129-
referencedSymbol = info.Symbol;
129+
if (info.Symbol is not IMethodSymbol invokedMethod)
130+
continue;
131+
132+
// Skip enum method calls — the generator expands these via TryEmitEnumMethodExpansion
133+
if (HasEnumReceiver(invokedMethod, invocation, context.SemanticModel, context.CancellationToken))
134+
continue;
135+
136+
referencedSymbol = invokedMethod;
130137
location = invocation.Expression is MemberAccessExpressionSyntax memberAccess
131138
? memberAccess.Name.GetLocation()
132139
: invocation.Expression.GetLocation();
@@ -162,6 +169,47 @@ identifier.Parent is not MemberAccessExpressionSyntax &&
162169
}
163170
}
164171

172+
/// <summary>
173+
/// Returns true when the invocation's receiver (or first extension-method arg) is an enum
174+
/// or Nullable&lt;Enum&gt;. The generator already expands these via TryEmitEnumMethodExpansion,
175+
/// so EXP0013 would be a false positive.
176+
/// </summary>
177+
private static bool HasEnumReceiver(
178+
IMethodSymbol method,
179+
InvocationExpressionSyntax invocation,
180+
SemanticModel semanticModel,
181+
System.Threading.CancellationToken ct)
182+
{
183+
ITypeSymbol? receiverType = null;
184+
185+
if (!method.IsStatic && method.ReceiverType is not null)
186+
{
187+
receiverType = method.ReceiverType;
188+
}
189+
else if (method.IsExtensionMethod)
190+
{
191+
var original = method.ReducedFrom ?? method;
192+
if (original.Parameters.Length > 0)
193+
receiverType = original.Parameters[0].Type;
194+
}
195+
196+
return receiverType is not null && IsEnumOrNullableEnum(receiverType);
197+
}
198+
199+
private static bool IsEnumOrNullableEnum(ITypeSymbol type)
200+
{
201+
if (type.TypeKind == TypeKind.Enum)
202+
return true;
203+
204+
if (type is INamedTypeSymbol { IsGenericType: true } namedType &&
205+
namedType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T &&
206+
namedType.TypeArguments.Length == 1 &&
207+
namedType.TypeArguments[0].TypeKind == TypeKind.Enum)
208+
return true;
209+
210+
return false;
211+
}
212+
165213
// ── Core logic (matches ExpressionTreeEmitter.WarnIfMissingExpressive) ──
166214

167215
private static void WarnIfMissingExpressive(

tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/MissingExpressiveDiagnosticTests.cs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,50 @@ class C {
9595
"Expected EXP0013 for method call to block-body method without [Expressive]");
9696
}
9797

98+
[TestMethod]
99+
public async Task ExtensionMethod_OnNonEnumReceiver_WarnsEXP0013()
100+
{
101+
var diagnostics = await RunAnalyzerAsync(
102+
"""
103+
namespace Foo {
104+
public static class StringExtensions {
105+
public static string Shout(this string value) => value.ToUpper() + "!";
106+
}
107+
108+
class C {
109+
public string Name { get; set; } = "";
110+
111+
[Expressive]
112+
public string Loud => Name.Shout();
113+
}
114+
}
115+
""");
116+
117+
Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0013"),
118+
"Expected EXP0013 for non-enum extension method without [Expressive]");
119+
}
120+
121+
[TestMethod]
122+
public async Task PropertyWithoutExpressive_ReferencedInExpressive_WarnsEXP0013()
123+
{
124+
var diagnostics = await RunAnalyzerAsync(
125+
"""
126+
namespace Foo {
127+
class Order {
128+
public double Price { get; set; }
129+
public int Quantity { get; set; }
130+
public double Total => Price * Quantity;
131+
132+
[Expressive]
133+
public string? Label => Total >= 0 ? "Positive" : "Negative";
134+
}
135+
}
136+
""");
137+
138+
Assert.IsTrue(diagnostics.Any(d => d.Id == "EXP0013"),
139+
"Expected EXP0013 for property without [Expressive] referenced in [Expressive] member");
140+
}
141+
98142
[TestMethod]
99143
public async Task EXP0013_HasAdditionalLocation_PointingToDeclaration()
100144
{
@@ -197,6 +241,35 @@ class C {
197241
"Should not warn when referenced property already has [Expressive]");
198242
}
199243

244+
[TestMethod]
245+
public async Task ExtensionMethod_OnEnumReceiver_NoWarning()
246+
{
247+
var diagnostics = await RunAnalyzerAsync(
248+
"""
249+
namespace Foo {
250+
public enum OrderStatus { Pending, Approved, Rejected }
251+
252+
public static class OrderStatusExtensions {
253+
public static string GetDescription(this OrderStatus value) => value switch {
254+
OrderStatus.Pending => "Awaiting processing",
255+
OrderStatus.Approved => "Order approved",
256+
_ => value.ToString(),
257+
};
258+
}
259+
260+
class Order {
261+
public OrderStatus Status { get; set; }
262+
263+
[Expressive]
264+
public string StatusDescription => Status.GetDescription();
265+
}
266+
}
267+
""");
268+
269+
Assert.IsFalse(diagnostics.Any(d => d.Id == "EXP0013"),
270+
"Should not warn for enum extension method — generator expands these via TryEmitEnumMethodExpansion");
271+
}
272+
200273
// ── Helper ──────────────────────────────────────────────────────────────
201274

202275
private async Task<ImmutableArray<Diagnostic>> RunAnalyzerAsync(string source)

0 commit comments

Comments
 (0)