Skip to content

Commit 4a0df3f

Browse files
authored
Registration in lambda (#37)
Find event registrations in lambdas
1 parent 60c2234 commit 4a0df3f

32 files changed

+480
-310
lines changed

CSharpCodeAnalyst/Analyzers/EventRegistration/Analyzer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public bool IsDirty()
5757

5858
public event EventHandler? DataChanged;
5959

60-
private static List<Result> FindImbalances(CodeGraph originalGraph)
60+
public static List<Result> FindImbalances(CodeGraph originalGraph)
6161
{
6262
var relationships = originalGraph.GetAllRelationships().Where(r => r.Type == RelationshipType.Handles).ToHashSet();
6363

CSharpCodeAnalyst/Analyzers/EventRegistration/Result.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace CSharpCodeAnalyst.Analyzers.EventRegistration;
44

5-
internal class Result
5+
public class Result
66
{
77
public Result(CodeElement handler, CodeElement evt, List<SourceLocation> locations)
88
{

CSharpCodeAnalyst/Areas/TreeArea/TreeViewModel.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,15 +89,15 @@ public string SearchText
8989

9090
private void OnSelectedItemChanged(TreeItemViewModel item)
9191
{
92-
if (item?.CodeElement != null)
92+
if (item.CodeElement != null)
9393
{
9494
_lastSelectedCodeElement = item.CodeElement.Id;
9595
}
9696
}
9797

9898
private bool RefactoringCanMoveCodeElement(TreeItemViewModel tvm)
9999
{
100-
var id = tvm?.CodeElement?.Id;
100+
var id = tvm.CodeElement?.Id;
101101
return id != null && _refactoringService.CanMoveCodeElements([id]);
102102
}
103103

@@ -114,16 +114,14 @@ private void RefactoringMoveCodeElement(TreeItemViewModel? tvm)
114114

115115
private bool RefactoringCanSetMovementTarget(TreeItemViewModel tvm)
116116
{
117-
return _refactoringService.CanSetMovementTarget(tvm?.CodeElement?.Id);
117+
return _refactoringService.CanSetMovementTarget(tvm.CodeElement?.Id);
118118
}
119119

120120
private void RefactoringSetMovementTarget(TreeItemViewModel tvm)
121121
{
122-
_refactoringService.SetMovementTarget(tvm?.CodeElement?.Id);
122+
_refactoringService.SetMovementTarget(tvm.CodeElement?.Id);
123123
}
124124

125-
126-
127125
private static void OnCopyToClipboard(TreeItemViewModel vm)
128126
{
129127
var text = vm.CodeElement?.FullName;

CodeParser/Parser/ISyntaxNodeHandler.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,32 @@ public interface ISyntaxNodeHandler
1212
void AnalyzeInvocation(CodeElement sourceElement, InvocationExpressionSyntax invocationSyntax,
1313
SemanticModel semanticModel);
1414

15-
void AnalyzeAssignment(CodeElement sourceElement, AssignmentExpressionSyntax assignmentExpression,
15+
/// <summary>
16+
/// Analyzes assignment expressions for event registration/unregistration.
17+
/// Property/field access on left and right sides is handled by the walker's normal traversal.
18+
/// </summary>
19+
void AnalyzeEventRegistrationAssignment(CodeElement sourceElement, AssignmentExpressionSyntax assignmentExpression,
1620
SemanticModel semanticModel);
1721

1822
/// <summary>
1923
/// Analyzes standalone identifier references (fields, properties, etc.).
2024
/// Ownership: Handles ONLY standalone identifiers. Identifiers that are part of
2125
/// MemberAccessExpressions are NOT visited here - they're handled by AnalyzeMemberAccess.
26+
/// The propertyAccessType parameter controls whether property access creates "Calls" or "Uses" relationships.
27+
/// Default is "Calls" for method bodies; lambda bodies should pass "Uses" because we don't know when/if the lambda executes.
2228
/// </summary>
2329
void AnalyzeIdentifier(CodeElement sourceElement, IdentifierNameSyntax identifierSyntax,
24-
SemanticModel semanticModel);
30+
SemanticModel semanticModel, RelationshipType propertyAccessType = RelationshipType.Calls);
2531

2632
/// <summary>
2733
/// Analyzes member access expressions (obj.Property, obj.Field, obj.Event).
2834
/// Ownership: Handles the member being accessed (the .Name part on the right side).
2935
/// The Expression (left side) is handled by the walker, which will visit it independently.
36+
/// The propertyAccessType parameter controls whether property access creates "Calls" or "Uses" relationships.
37+
/// Default is "Calls" for method bodies; lambda bodies should pass "Uses" because we don't know when/if the lambda executes.
3038
/// </summary>
3139
void AnalyzeMemberAccess(CodeElement sourceElement, MemberAccessExpressionSyntax memberAccessSyntax,
32-
SemanticModel semanticModel);
40+
SemanticModel semanticModel, RelationshipType propertyAccessType = RelationshipType.Calls);
3341

3442
void AnalyzeArgument(CodeElement sourceElement, ArgumentSyntax argumentSyntax, SemanticModel semanticModel);
3543

CodeParser/Parser/LambdaBodyWalker.cs

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -85,37 +85,34 @@ public override void VisitInvocationExpression(InvocationExpressionSyntax node)
8585
base.VisitInvocationExpression(node);
8686
}
8787

88-
// ReSharper disable once RedundantOverriddenMember
8988
public override void VisitAssignmentExpression(AssignmentExpressionSyntax node)
9089
{
91-
// We need to walk further to capture following expressions:
92-
// Traversal.Dfs(newParent, n => n.FullName = n.GetFullPath());
90+
// Track event registration/unregistration (handled by AnalyzeAssignment)
91+
// Property/field access on both sides is handled by the walker's normal traversal,
92+
// which correctly uses "Uses" relationships for lambdas (see VisitIdentifierName and VisitMemberAccessExpression)
93+
Analyzer.AnalyzeEventRegistrationAssignment(SourceElement, node, SemanticModel);
9394
base.VisitAssignmentExpression(node);
9495
}
9596

96-
public override void VisitMemberAccessExpression(MemberAccessExpressionSyntax node)
97+
/// <summary>
98+
/// Visit standalone identifiers (properties, fields, etc.).
99+
/// Uses "Uses" relationship for lambda bodies (we don't know when/if lambda executes).
100+
/// Examples: x => MyProperty (standalone), not obj.MyProperty (that's MemberAccess)
101+
/// </summary>
102+
public override void VisitIdentifierName(IdentifierNameSyntax node)
97103
{
98-
// Track member references (properties, fields, events) with "Uses" relationship
99-
var symbolInfo = SemanticModel.GetSymbolInfo(node);
100-
var symbol = symbolInfo.Symbol;
101-
var location = node.GetSyntaxLocation();
104+
Analyzer.AnalyzeIdentifier(SourceElement, node, SemanticModel, RelationshipType.Uses);
105+
base.VisitIdentifierName(node);
106+
}
102107

103-
if (symbol is IPropertySymbol propertySymbol)
104-
{
105-
Analyzer.AddSymbolRelationshipPublic(
106-
SourceElement, propertySymbol, RelationshipType.Uses, [location], RelationshipAttribute.None);
107-
}
108-
else if (symbol is IFieldSymbol fieldSymbol)
109-
{
110-
Analyzer.AddSymbolRelationshipPublic(
111-
SourceElement, fieldSymbol, RelationshipType.Uses, [location], RelationshipAttribute.None);
112-
}
113-
else if (symbol is IEventSymbol eventSymbol)
114-
{
115-
Analyzer.AddSymbolRelationshipPublic(
116-
SourceElement, eventSymbol, RelationshipType.Uses, [location], RelationshipAttribute.None);
117-
}
108+
public override void VisitMemberAccessExpression(MemberAccessExpressionSyntax node)
109+
{
110+
// Delegate to AnalyzeMemberAccess with "Uses" relationship type for lambdas
111+
// Same rationale as VisitAssignmentExpression - we don't know when/if the lambda executes
112+
Analyzer.AnalyzeMemberAccess(SourceElement, node, SemanticModel, RelationshipType.Uses);
118113

114+
// Now safe to call base traversal since VisitIdentifierName is overridden in this class
115+
// and will use RelationshipType.Uses (not the default Calls)
119116
base.VisitMemberAccessExpression(node);
120117
}
121118

CodeParser/Parser/MethodBodyWalker.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,17 @@ public MethodBodyWalker(ISyntaxNodeHandler analyzer, CodeElement sourceElement,
1616
{
1717
}
1818

19+
/// <summary>
20+
/// Visit standalone identifiers (properties, fields, etc.).
21+
/// Uses "Calls" relationship for method bodies.
22+
/// Examples: MyProperty (standalone), not obj.MyProperty (that's MemberAccess)
23+
/// </summary>
24+
public override void VisitIdentifierName(IdentifierNameSyntax node)
25+
{
26+
Analyzer.AnalyzeIdentifier(SourceElement, node, SemanticModel, RelationshipType.Calls);
27+
base.VisitIdentifierName(node);
28+
}
29+
1930
public override void VisitInvocationExpression(InvocationExpressionSyntax node)
2031
{
2132
Analyzer.AnalyzeInvocation(SourceElement, node, SemanticModel);
@@ -25,7 +36,7 @@ public override void VisitInvocationExpression(InvocationExpressionSyntax node)
2536

2637
public override void VisitAssignmentExpression(AssignmentExpressionSyntax node)
2738
{
28-
Analyzer.AnalyzeAssignment(SourceElement, node, SemanticModel);
39+
Analyzer.AnalyzeEventRegistrationAssignment(SourceElement, node, SemanticModel);
2940
base.VisitAssignmentExpression(node);
3041
}
3142

CodeParser/Parser/RelationshipAnalyzer.cs

Lines changed: 8 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,11 @@ public void AnalyzeInvocation(CodeElement sourceElement, InvocationExpressionSyn
102102
}
103103
}
104104

105-
public void AnalyzeAssignment(CodeElement sourceElement, AssignmentExpressionSyntax assignmentExpression,
105+
public void AnalyzeEventRegistrationAssignment(CodeElement sourceElement, AssignmentExpressionSyntax assignmentExpression,
106106
SemanticModel semanticModel)
107107
{
108-
// Analyze the left side of the assignment (target)
109-
AnalyzeExpressionForPropertyAccess(sourceElement, assignmentExpression.Left, semanticModel);
110-
111-
// Analyze the right side of the assignment (value)
112-
AnalyzeExpressionForPropertyAccess(sourceElement, assignmentExpression.Right, semanticModel);
108+
// Note: Property/field access on left and right sides is handled by the walker's normal traversal
109+
// (VisitIdentifierName and VisitMemberAccessExpression). We only need to handle event registration/unregistration here.
113110

114111
var isRegistration = assignmentExpression.IsKind(SyntaxKind.AddAssignmentExpression);
115112
var isUnregistration = assignmentExpression.IsKind(SyntaxKind.SubtractAssignmentExpression);
@@ -179,7 +176,7 @@ public void AnalyzeTypeSyntax(CodeElement sourceElement, SemanticModel semanticM
179176
/// <inheritdoc cref="ISyntaxNodeHandler.AnalyzeIdentifier" />
180177
/// </summary>
181178
public void AnalyzeIdentifier(CodeElement sourceElement, IdentifierNameSyntax identifierSyntax,
182-
SemanticModel semanticModel)
179+
SemanticModel semanticModel, RelationshipType propertyAccessType = RelationshipType.Calls)
183180
{
184181
var symbolInfo = semanticModel.GetSymbolInfo(identifierSyntax);
185182
var symbol = symbolInfo.Symbol;
@@ -190,14 +187,14 @@ public void AnalyzeIdentifier(CodeElement sourceElement, IdentifierNameSyntax id
190187
if (symbol is IPropertySymbol propertySymbol)
191188
{
192189
var location = identifierSyntax.GetSyntaxLocation();
193-
AddPropertyCallRelationship(sourceElement, propertySymbol, [location], RelationshipAttribute.None);
190+
AddRelationshipWithFallbackToContainingType(sourceElement, propertySymbol, propertyAccessType, [location], RelationshipAttribute.None);
194191
}
195192
else if (symbol is IFieldSymbol fieldSymbol)
196193
{
197194
var location = identifierSyntax.GetSyntaxLocation();
198195
AddRelationshipWithFallbackToContainingType(sourceElement, fieldSymbol, RelationshipType.Uses, [location], RelationshipAttribute.None);
199196
}
200-
else if (symbol is IEventSymbol eventSymbol)
197+
else if (symbol is IEventSymbol eventSymbol)
201198
{
202199
var location = identifierSyntax.GetSyntaxLocation();
203200
AddEventUsageRelationship(sourceElement, eventSymbol, location);
@@ -208,7 +205,7 @@ public void AnalyzeIdentifier(CodeElement sourceElement, IdentifierNameSyntax id
208205
/// <inheritdoc cref="ISyntaxNodeHandler.AnalyzeMemberAccess" />
209206
/// </summary>
210207
public void AnalyzeMemberAccess(CodeElement sourceElement, MemberAccessExpressionSyntax memberAccessSyntax,
211-
SemanticModel semanticModel)
208+
SemanticModel semanticModel, RelationshipType propertyAccessType = RelationshipType.Calls)
212209
{
213210
// Analyze the member being accessed (the right side of the dot)
214211
var symbolInfo = semanticModel.GetSymbolInfo(memberAccessSyntax);
@@ -217,7 +214,7 @@ public void AnalyzeMemberAccess(CodeElement sourceElement, MemberAccessExpressio
217214
if (symbol is IPropertySymbol propertySymbol)
218215
{
219216
var location = memberAccessSyntax.GetSyntaxLocation();
220-
AddPropertyCallRelationship(sourceElement, propertySymbol, [location], RelationshipAttribute.None);
217+
AddRelationshipWithFallbackToContainingType(sourceElement, propertySymbol, propertyAccessType, [location], RelationshipAttribute.None);
221218
}
222219
else if (symbol is IFieldSymbol fieldSymbol)
223220
{
@@ -814,21 +811,6 @@ private void AddEventHandlerRelationship(IMethodSymbol handlerMethod, IEventSymb
814811
// $"Unable to add 'Handles' relationship: Handler {handlerMethod.Name} or Event {eventSymbol.Name} not found in codebase.");
815812
}
816813

817-
private void AnalyzeExpressionForPropertyAccess(CodeElement sourceElement, ExpressionSyntax expression,
818-
SemanticModel semanticModel)
819-
{
820-
switch (expression)
821-
{
822-
case IdentifierNameSyntax identifierSyntax:
823-
AnalyzeIdentifier(sourceElement, identifierSyntax, semanticModel);
824-
break;
825-
case MemberAccessExpressionSyntax memberAccessSyntax:
826-
AnalyzeMemberAccess(sourceElement, memberAccessSyntax, semanticModel);
827-
break;
828-
// Add more cases if needed for other types of expressions
829-
}
830-
}
831-
832814
private void AddCallsRelationship(CodeElement sourceElement, IMethodSymbol methodSymbol, SourceLocation location, RelationshipAttribute attributes)
833815
{
834816
//Debug.Assert(FindCodeElement(methodSymbol)!= null);
@@ -1006,15 +988,6 @@ private void AddNamedTypeRelationship(CodeElement sourceElement, INamedTypeSymbo
1006988
return _externalCodeElementCache.TryGetOrCreateExternalCodeElement(symbol);
1007989
}
1008990

1009-
/// <summary>
1010-
/// Calling a property is treated like calling a method.
1011-
/// </summary>
1012-
private void AddPropertyCallRelationship(CodeElement sourceElement, IPropertySymbol propertySymbol,
1013-
List<SourceLocation> locations, RelationshipAttribute attributes)
1014-
{
1015-
AddRelationshipWithFallbackToContainingType(sourceElement, propertySymbol, RelationshipType.Calls, locations, attributes);
1016-
}
1017-
1018991
/// <summary>
1019992
/// Adds a relationship to a symbol, with configurable fallback behavior for external symbols.
1020993
/// Tries in order: direct symbol → normalized symbol → containing type → external element

CodeParser/Parser/SyntaxWalkerBase.cs

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,10 @@ public override void VisitArgument(ArgumentSyntax node)
3232
base.VisitArgument(node);
3333
}
3434

35-
/// <summary>
36-
/// The walker ensures we only visit standalone identifiers
37-
/// like MyProperty, not: obj.MyProperty
38-
/// </summary>
39-
public override void VisitIdentifierName(IdentifierNameSyntax node)
40-
{
41-
Analyzer.AnalyzeIdentifier(SourceElement, node, SemanticModel);
42-
base.VisitIdentifierName(node);
43-
}
44-
35+
// Note: VisitIdentifierName is NOT overridden here because concrete walkers need to specify
36+
// their relationship type (Calls for MethodBodyWalker, Uses for LambdaBodyWalker).
37+
// Each walker overrides VisitIdentifierName with the appropriate RelationshipType parameter.
38+
4539
public override void VisitLocalDeclarationStatement(LocalDeclarationStatementSyntax node)
4640
{
4741
Analyzer.AnalyzeLocalDeclaration(SourceElement, node, SemanticModel);

TestSuite/Core.BasicLanguageFeatures/Lambdas.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ private void Start()
1919
var y = () =>
2020
{
2121
BaseClass? baseClass;
22-
2322
};
2423

2524

TestSuite/Regression.SpecificBugs/AssignmentDuplicateTest.cs renamed to TestSuite/Regression.SpecificBugs/AssignmentDuplicate/AssignmentDuplicate.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
namespace Regression.SpecificBugs;
1+
namespace Regression.SpecificBugs.AssignmentDuplicate;
22

3-
public class AssignmentDuplicateTest
3+
public class AssignmentDuplicate
44
{
55
public string TestField;
66
public string TestProperty { get; set; }

0 commit comments

Comments
 (0)