fix(analyzers): resolve Medium code-review findings (Analyzers-001, Analyzers-006)
Analyzers-001: IsInsideWrapperLambda now matches the wrapper method name (ExecuteAsync/ExecuteWriteAsync) in addition to the containing type, so a future non-callSite lambda overload cannot suppress the diagnostic. Analyzers-006: extended StubSources and added coverage for the remaining guarded interfaces, synchronous members, concrete-driver receivers, ExecuteWriteAsync wrapping, and nested lambdas. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,11 +19,14 @@ namespace ZB.MOM.WW.OtOpcUa.Analyzers;
|
||||
/// The analyzer matches by receiver-interface identity using Roslyn's semantic model, not by
|
||||
/// method name, so a driver with an unusually-named method implementing <c>IReadable.ReadAsync</c>
|
||||
/// still trips the rule. Lambda-context detection walks up the syntax tree from the call site
|
||||
/// + checks whether any enclosing <c>InvocationExpressionSyntax</c> targets a member whose
|
||||
/// containing type is <c>CapabilityInvoker</c> or <c>AlarmSurfaceInvoker</c>. The rule is
|
||||
/// intentionally narrow: it does NOT try to enforce the capability argument matches the
|
||||
/// method (e.g. ReadAsync wrapped in <c>ExecuteAsync(DriverCapability.Write, ...)</c> still
|
||||
/// passes) — that'd require flow analysis beyond single-expression scope.
|
||||
/// and checks whether any enclosing <c>InvocationExpressionSyntax</c> targets one of the
|
||||
/// specific wrapper methods listed in <c>WrapperMethods</c> (type + method name pair).
|
||||
/// Matching by method name as well as containing type ensures that a future overload on
|
||||
/// <c>CapabilityInvoker</c> that takes a predicate/selector lambda does not silently widen the
|
||||
/// suppression scope. The rule is intentionally narrow: it does NOT try to enforce that the
|
||||
/// capability argument matches the method (e.g. ReadAsync wrapped in
|
||||
/// <c>ExecuteAsync(DriverCapability.Write, ...)</c> still passes) — that'd require flow
|
||||
/// analysis beyond single-expression scope.
|
||||
/// </remarks>
|
||||
[DiagnosticAnalyzer(Microsoft.CodeAnalysis.LanguageNames.CSharp)]
|
||||
public sealed class UnwrappedCapabilityCallAnalyzer : DiagnosticAnalyzer
|
||||
@@ -42,11 +45,19 @@ public sealed class UnwrappedCapabilityCallAnalyzer : DiagnosticAnalyzer
|
||||
"ZB.MOM.WW.OtOpcUa.Core.Abstractions.IHistoryProvider",
|
||||
];
|
||||
|
||||
/// <summary>Wrapper types whose lambda arguments are the allowed home for guarded calls.</summary>
|
||||
private static readonly string[] WrapperTypes =
|
||||
/// <summary>
|
||||
/// Wrapper types paired with the method names that take a resilience <c>callSite</c> lambda.
|
||||
/// Only a lambda bound to one of these specific methods is treated as a valid wrapper home;
|
||||
/// other lambdas on the same type (e.g. future predicate/selector overloads) do not suppress
|
||||
/// the diagnostic.
|
||||
/// </summary>
|
||||
private static readonly (string TypeFqn, string MethodName)[] WrapperMethods =
|
||||
[
|
||||
"ZB.MOM.WW.OtOpcUa.Core.Resilience.CapabilityInvoker",
|
||||
"ZB.MOM.WW.OtOpcUa.Core.Resilience.AlarmSurfaceInvoker",
|
||||
("ZB.MOM.WW.OtOpcUa.Core.Resilience.CapabilityInvoker", "ExecuteAsync"),
|
||||
("ZB.MOM.WW.OtOpcUa.Core.Resilience.CapabilityInvoker", "ExecuteWriteAsync"),
|
||||
("ZB.MOM.WW.OtOpcUa.Core.Resilience.AlarmSurfaceInvoker", "SubscribeAsync"),
|
||||
("ZB.MOM.WW.OtOpcUa.Core.Resilience.AlarmSurfaceInvoker", "UnsubscribeAsync"),
|
||||
("ZB.MOM.WW.OtOpcUa.Core.Resilience.AlarmSurfaceInvoker", "AcknowledgeAsync"),
|
||||
];
|
||||
|
||||
private static readonly DiagnosticDescriptor Rule = new(
|
||||
@@ -119,7 +130,10 @@ public sealed class UnwrappedCapabilityCallAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
// We only care about an enclosing invocation — the call we're auditing must literally
|
||||
// live inside a lambda (ParenthesizedLambda / SimpleLambda / AnonymousMethod) that is
|
||||
// an argument of a CapabilityInvoker.Execute* / AlarmSurfaceInvoker.* call.
|
||||
// an argument of a specific CapabilityInvoker.Execute* / AlarmSurfaceInvoker.*Async
|
||||
// method. Matching both the containing type AND the method name closes the gap where a
|
||||
// future overload on the same type that takes a predicate/selector lambda would
|
||||
// otherwise incorrectly suppress the diagnostic.
|
||||
if (node is not InvocationExpressionSyntax outer) continue;
|
||||
|
||||
var sym = semanticModel.GetSymbolInfo(outer, ct).Symbol as IMethodSymbol;
|
||||
@@ -127,7 +141,17 @@ public sealed class UnwrappedCapabilityCallAnalyzer : DiagnosticAnalyzer
|
||||
|
||||
var outerTypeFqn = sym.ContainingType.OriginalDefinition.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
|
||||
.Replace("global::", string.Empty);
|
||||
if (!WrapperTypes.Contains(outerTypeFqn)) continue;
|
||||
var methodName = sym.Name;
|
||||
var isWrapperMethod = false;
|
||||
foreach (var (typeFqn, name) in WrapperMethods)
|
||||
{
|
||||
if (typeFqn == outerTypeFqn && name == methodName)
|
||||
{
|
||||
isWrapperMethod = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!isWrapperMethod) continue;
|
||||
|
||||
// The call is wrapped IFF our startNode is transitively inside one of the outer
|
||||
// call's argument lambdas. Walk the outer invocation's argument list + check whether
|
||||
|
||||
Reference in New Issue
Block a user