Skip to content

Commit 9945bd5

Browse files
amis92Copilot
andcommitted
Add WHAM002 analyzer: warn on non-static GetBoundField lambda
Non-static lambdas in GetBoundField cause delegate allocation on every property access. The static modifier prevents accidental closure captures and keeps the hot path allocation-free. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 28d97d1 commit 9945bd5

2 files changed

Lines changed: 51 additions & 1 deletion

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ Extensions, Concrete.Extensions, Concrete.Extensions.Generators, EditorServices,
177177
- `[GenerateSymbol(SymbolKind.X)]` — generates `Kind` property + 3 `Accept` overloads
178178
- `[Bound]` on properties — generates `CheckReferencesCore` accessing all bound properties
179179
- `WHAM001` analyzer — warns when `GetBoundField` is called without `[Bound]`
180+
- `WHAM002` analyzer — warns when `GetBoundField` lambda is not `static`
180181
- Symbol classes using these must be `partial`
181182
- **BattleScribe quirks**: some spec default expectations match BattleScribe bugs rather than
182183
"correct" behavior; wham uses engine-specific overrides for these (documented in

src/WarHub.ArmouryModel.Concrete.Extensions.Generators/BoundAnalyzer.cs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System.Collections.Immutable;
22
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.CSharp;
4+
using Microsoft.CodeAnalysis.CSharp.Syntax;
35
using Microsoft.CodeAnalysis.Diagnostics;
46
using Microsoft.CodeAnalysis.Operations;
57

@@ -16,8 +18,16 @@ public sealed class BoundAnalyzer : DiagnosticAnalyzer
1618
defaultSeverity: DiagnosticSeverity.Warning,
1719
isEnabledByDefault: true);
1820

21+
public static readonly DiagnosticDescriptor GetBoundFieldNonStaticLambda = new(
22+
id: "WHAM002",
23+
title: "GetBoundField called with non-static lambda",
24+
messageFormat: "GetBoundField lambda should be static to avoid delegate allocation on every access. Add the 'static' modifier.",
25+
category: "WarHub.ArmouryModel",
26+
defaultSeverity: DiagnosticSeverity.Warning,
27+
isEnabledByDefault: true);
28+
1929
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
20-
ImmutableArray.Create(GetBoundFieldWithoutBoundAttribute);
30+
ImmutableArray.Create(GetBoundFieldWithoutBoundAttribute, GetBoundFieldNonStaticLambda);
2131

2232
public override void Initialize(AnalysisContext context)
2333
{
@@ -33,6 +43,12 @@ private static void AnalyzeInvocation(OperationAnalysisContext context)
3343
if (invocation.TargetMethod.Name != "GetBoundField")
3444
return;
3545

46+
CheckBoundAttribute(context, invocation);
47+
CheckStaticLambda(context, invocation);
48+
}
49+
50+
private static void CheckBoundAttribute(OperationAnalysisContext context, IInvocationOperation invocation)
51+
{
3652
// Walk up to find the containing property
3753
var containingSymbol = context.ContainingSymbol;
3854
if (containingSymbol is not IMethodSymbol { AssociatedSymbol: IPropertySymbol property })
@@ -59,4 +75,37 @@ private static void AnalyzeInvocation(OperationAnalysisContext context)
5975
context.ReportDiagnostic(diagnostic);
6076
}
6177
}
78+
79+
private static void CheckStaticLambda(OperationAnalysisContext context, IInvocationOperation invocation)
80+
{
81+
// The last argument to GetBoundField is the binding lambda
82+
var args = invocation.Arguments;
83+
if (args.Length < 3)
84+
return;
85+
86+
var lambdaArg = args[args.Length - 1];
87+
if (lambdaArg.Value is not IDelegateCreationOperation delegateCreation)
88+
return;
89+
90+
if (delegateCreation.Target is not IAnonymousFunctionOperation anonymousFunc)
91+
return;
92+
93+
// Check the syntax node for the 'static' modifier
94+
var syntax = anonymousFunc.Syntax;
95+
bool isStatic = false;
96+
if (syntax is LambdaExpressionSyntax lambda)
97+
{
98+
isStatic = lambda.Modifiers.Any(SyntaxKind.StaticKeyword);
99+
}
100+
else if (syntax is AnonymousMethodExpressionSyntax anonymousMethod)
101+
{
102+
isStatic = anonymousMethod.Modifiers.Any(SyntaxKind.StaticKeyword);
103+
}
104+
105+
if (!isStatic)
106+
{
107+
context.ReportDiagnostic(
108+
Diagnostic.Create(GetBoundFieldNonStaticLambda, syntax.GetLocation()));
109+
}
110+
}
62111
}

0 commit comments

Comments
 (0)