HandledEventArgsAnalyzer.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. using System.Collections.Immutable;
  2. using Microsoft.CodeAnalysis;
  3. using Microsoft.CodeAnalysis.CSharp;
  4. using Microsoft.CodeAnalysis.CSharp.Syntax;
  5. using Microsoft.CodeAnalysis.Diagnostics;
  6. namespace Terminal.Gui.Analyzers;
  7. [DiagnosticAnalyzer (LanguageNames.CSharp)]
  8. public class HandledEventArgsAnalyzer : DiagnosticAnalyzer
  9. {
  10. public const string DiagnosticId = "TGUI001";
  11. private static readonly LocalizableString Title = "Accepting event handler should set Handled = true";
  12. private static readonly LocalizableString MessageFormat = "Accepting event handler does not set Handled = true";
  13. private static readonly LocalizableString Description = "Handlers for Accepting should mark the CommandEventArgs as handled by setting Handled = true otherwise subsequent Accepting event handlers may also fire (e.g. default buttons).";
  14. private static readonly string Url = "https://github.com/tznind/gui.cs/blob/analyzer-no-handled/Terminal.Gui.Analyzers/TGUI001.md";
  15. private const string Category = nameof(DiagnosticCategory.Reliability);
  16. private static readonly DiagnosticDescriptor _rule = new (
  17. DiagnosticId,
  18. Title,
  19. MessageFormat,
  20. Category,
  21. DiagnosticSeverity.Warning,
  22. true,
  23. Description,
  24. helpLinkUri: Url);
  25. public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [_rule];
  26. public override void Initialize (AnalysisContext context)
  27. {
  28. context.EnableConcurrentExecution ();
  29. // Only analyze non-generated code
  30. context.ConfigureGeneratedCodeAnalysis (GeneratedCodeAnalysisFlags.None);
  31. // Register for b.Accepting += (s,e)=>{...};
  32. context.RegisterSyntaxNodeAction (
  33. AnalyzeLambdaOrAnonymous,
  34. SyntaxKind.ParenthesizedLambdaExpression,
  35. SyntaxKind.SimpleLambdaExpression,
  36. SyntaxKind.AnonymousMethodExpression);
  37. // Register for b.Accepting += MyMethod;
  38. context.RegisterSyntaxNodeAction (
  39. AnalyzeEventSubscriptionWithMethodGroup,
  40. SyntaxKind.AddAssignmentExpression);
  41. }
  42. private static void AnalyzeLambdaOrAnonymous (SyntaxNodeAnalysisContext context)
  43. {
  44. var lambda = (AnonymousFunctionExpressionSyntax)context.Node;
  45. // Check if this lambda is assigned to the Accepting event
  46. if (!IsAssignedToAcceptingEvent (lambda.Parent, context))
  47. {
  48. return;
  49. }
  50. // Look for any parameter of type CommandEventArgs (regardless of name)
  51. IParameterSymbol? eParam = GetCommandEventArgsParameter (lambda, context.SemanticModel);
  52. if (eParam == null)
  53. {
  54. return;
  55. }
  56. // Analyze lambda body for e.Handled = true assignment
  57. if (lambda.Body is BlockSyntax block)
  58. {
  59. bool setsHandled = block.Statements
  60. .SelectMany (s => s.DescendantNodes ().OfType<AssignmentExpressionSyntax> ())
  61. .Any (a => IsHandledAssignment (a, eParam, context));
  62. if (!setsHandled)
  63. {
  64. var diag = Diagnostic.Create (_rule, lambda.GetLocation ());
  65. context.ReportDiagnostic (diag);
  66. }
  67. }
  68. else if (lambda.Body is ExpressionSyntax)
  69. {
  70. // Expression-bodied lambdas unlikely for event handlers — skip
  71. }
  72. }
  73. /// <summary>
  74. /// Finds the first parameter of type CommandEventArgs in any parameter list (method or lambda).
  75. /// </summary>
  76. /// <param name="paramOwner"></param>
  77. /// <param name="semanticModel"></param>
  78. /// <returns></returns>
  79. private static IParameterSymbol? GetCommandEventArgsParameter (SyntaxNode paramOwner, SemanticModel semanticModel)
  80. {
  81. SeparatedSyntaxList<ParameterSyntax>? parameters = paramOwner switch
  82. {
  83. AnonymousFunctionExpressionSyntax lambda => GetParameters (lambda),
  84. MethodDeclarationSyntax method => method.ParameterList.Parameters,
  85. _ => null
  86. };
  87. if (parameters == null || parameters.Value.Count == 0)
  88. {
  89. return null;
  90. }
  91. foreach (ParameterSyntax param in parameters.Value)
  92. {
  93. IParameterSymbol? symbol = semanticModel.GetDeclaredSymbol (param);
  94. if (symbol != null && IsCommandEventArgsType (symbol.Type))
  95. {
  96. return symbol;
  97. }
  98. }
  99. return null;
  100. }
  101. private static bool IsAssignedToAcceptingEvent (SyntaxNode? node, SyntaxNodeAnalysisContext context)
  102. {
  103. if (node is AssignmentExpressionSyntax assignment && IsAcceptingEvent (assignment.Left, context))
  104. {
  105. return true;
  106. }
  107. if (node?.Parent is AssignmentExpressionSyntax parentAssignment && IsAcceptingEvent (parentAssignment.Left, context))
  108. {
  109. return true;
  110. }
  111. return false;
  112. }
  113. private static bool IsCommandEventArgsType (ITypeSymbol? type) { return type != null && type.Name == "CommandEventArgs"; }
  114. private static void AnalyzeEventSubscriptionWithMethodGroup (SyntaxNodeAnalysisContext context)
  115. {
  116. var assignment = (AssignmentExpressionSyntax)context.Node;
  117. // Check event name: b.Accepting += ...
  118. if (!IsAcceptingEvent (assignment.Left, context))
  119. {
  120. return;
  121. }
  122. // Right side: should be method group (IdentifierNameSyntax)
  123. if (assignment.Right is IdentifierNameSyntax methodGroup)
  124. {
  125. // Resolve symbol of method group
  126. SymbolInfo symbolInfo = context.SemanticModel.GetSymbolInfo (methodGroup);
  127. if (symbolInfo.Symbol is IMethodSymbol methodSymbol)
  128. {
  129. // Find method declaration in syntax tree
  130. ImmutableArray<SyntaxReference> declRefs = methodSymbol.DeclaringSyntaxReferences;
  131. foreach (SyntaxReference declRef in declRefs)
  132. {
  133. var methodDecl = declRef.GetSyntax () as MethodDeclarationSyntax;
  134. if (methodDecl != null)
  135. {
  136. AnalyzeHandlerMethodBody (context, methodDecl, methodSymbol);
  137. }
  138. }
  139. }
  140. }
  141. }
  142. private static void AnalyzeHandlerMethodBody (SyntaxNodeAnalysisContext context, MethodDeclarationSyntax methodDecl, IMethodSymbol methodSymbol)
  143. {
  144. // Look for any parameter of type CommandEventArgs
  145. IParameterSymbol? eParam = GetCommandEventArgsParameter (methodDecl, context.SemanticModel);
  146. if (eParam == null)
  147. {
  148. return;
  149. }
  150. // Analyze method body
  151. if (methodDecl.Body != null)
  152. {
  153. bool setsHandled = methodDecl.Body.Statements
  154. .SelectMany (s => s.DescendantNodes ().OfType<AssignmentExpressionSyntax> ())
  155. .Any (a => IsHandledAssignment (a, eParam, context));
  156. if (!setsHandled)
  157. {
  158. var diag = Diagnostic.Create (_rule, methodDecl.Identifier.GetLocation ());
  159. context.ReportDiagnostic (diag);
  160. }
  161. }
  162. }
  163. private static SeparatedSyntaxList<ParameterSyntax> GetParameters (AnonymousFunctionExpressionSyntax lambda)
  164. {
  165. switch (lambda)
  166. {
  167. case ParenthesizedLambdaExpressionSyntax p:
  168. return p.ParameterList.Parameters;
  169. case SimpleLambdaExpressionSyntax s:
  170. // Simple lambda has a single parameter, wrap it in a list
  171. return SyntaxFactory.SeparatedList (new [] { s.Parameter });
  172. case AnonymousMethodExpressionSyntax a:
  173. return a.ParameterList?.Parameters ?? default (SeparatedSyntaxList<ParameterSyntax>);
  174. default:
  175. return default (SeparatedSyntaxList<ParameterSyntax>);
  176. }
  177. }
  178. private static bool IsAcceptingEvent (ExpressionSyntax expr, SyntaxNodeAnalysisContext context)
  179. {
  180. // Check if expr is b.Accepting or similar
  181. // Get symbol info
  182. SymbolInfo symbolInfo = context.SemanticModel.GetSymbolInfo (expr);
  183. ISymbol? symbol = symbolInfo.Symbol;
  184. if (symbol == null)
  185. {
  186. return false;
  187. }
  188. // Accepting event symbol should be an event named "Accepting"
  189. if (symbol.Kind == SymbolKind.Event && symbol.Name == "Accepting")
  190. {
  191. return true;
  192. }
  193. return false;
  194. }
  195. private static bool IsHandledAssignment (AssignmentExpressionSyntax assignment, IParameterSymbol eParamSymbol, SyntaxNodeAnalysisContext context)
  196. {
  197. // Check if left side is "e.Handled" and right side is "true"
  198. // Left side should be MemberAccessExpression: e.Handled
  199. if (assignment.Left is MemberAccessExpressionSyntax memberAccess)
  200. {
  201. // Check that member access expression is "e.Handled"
  202. ISymbol? exprSymbol = context.SemanticModel.GetSymbolInfo (memberAccess.Expression).Symbol;
  203. if (exprSymbol == null)
  204. {
  205. return false;
  206. }
  207. if (!SymbolEqualityComparer.Default.Equals (exprSymbol, eParamSymbol))
  208. {
  209. return false;
  210. }
  211. if (memberAccess.Name.Identifier.Text != "Handled")
  212. {
  213. return false;
  214. }
  215. // Check right side is true literal
  216. if (assignment.Right is LiteralExpressionSyntax literal && literal.IsKind (SyntaxKind.TrueLiteralExpression))
  217. {
  218. return true;
  219. }
  220. }
  221. return false;
  222. }
  223. }