背景故事(对于理解我的问题不是必需的,但某些上下文可能会有所帮助)
在我的公司,我们使用 IResult<T>
type 以函数式风格处理错误,而不是抛出异常并希望某些客户端捕获它们。 IResult<T>
可以是 DataResult<T>
用T
或 ErrorResult<T>
用Error
但不是两者。 Error
大致相当于 Exception
.所以一个典型的函数会返回 IResult<T>
通过返回值传递任何遇到的错误,而不是使用 throw
备份堆栈.
我们在 IResult<T>
上有扩展方法组成函数链。两个主要的是 Bind
和 Let
.
Bind
是你的标准 monadic bind
来自函数式语言的运算符。基本上如果 IResult
有一个值,它转换这个值,否则它转发错误。它是这样实现的
static IResult<T2> Bind(
this IResult<T1> @this,
Func<T1, IResult<T2>> projection)
{
return @this.HasValue
? projection(@this.Value)
: new ErrorResult<T2>(@this.Error);
}
Let
用于执行副作用,只要在函数链的前面没有遇到错误。它被实现为
static IResult<T> Let(
this IResult<T> @this,
Action<T> action)
{
if (@this.HasValue) {
action(@this.Value);
}
return @this;
}
我的 Roslyn 分析器用例
使用此 IResult<T>
时常犯的错误API,是调用一个返回 IResult<T>
的函数在Action<T>
里面传入Let
.发生这种情况时,如果内部函数返回 Error
,错误将丢失,并且继续执行,就像没有出错一样。当它发生时,这可能是一个很难追踪的错误,并且在过去的一年中发生了好几次。
在这些情况下,Bind
应该改用,以便可以传播错误。
我想识别对返回 IResult<T>
的函数的任何调用作为参数传递给 Let
的 lambda 表达式内部并将它们标记为编译器警告。
我已经创建了一个分析器来执行此操作。您可以在此处查看完整的源代码:https://github.com/JamesFaix/NContext.Analyzers这是解决方案中的主要分析器文件:
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace NContext.Analyzers
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class BindInsteadOfLetAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "NContext_0001";
private const string _Category = "Safety";
private const string _Title = "Unsafe use of IServiceResponse<T> inside Let expression";
private const string _MessageFormat = "Unsafe use of IServiceResponse<T> inside Let expression.";
private const string _Description = "If calling any methods that return IServiceResponse<T>, use Bind instead of Let. " +
"Otherwise, any returned ErrorResponses will be lost, execution will continue as if no error occurred, and no error will be logged.";
private static DiagnosticDescriptor _Rule =
new DiagnosticDescriptor(
DiagnosticId,
_Title,
_MessageFormat,
_Category,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: _Description);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
ImmutableArray.Create(_Rule);
public override void Initialize(AnalysisContext context)
{
context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.InvocationExpression);
}
private static void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
var functionChain = (InvocationExpressionSyntax) context.Node;
//When invoking an extension method, the first child node should be a MemberAccessExpression
var memberAccess = functionChain.ChildNodes().First() as MemberAccessExpressionSyntax;
if (memberAccess == null)
{
return;
}
//When invoking an extension method, the last child node of the member access should be an IdentifierName
var letIdentifier = memberAccess.ChildNodes().Last() as IdentifierNameSyntax;
if (letIdentifier == null)
{
return;
}
//Ignore method invocations that do not have "Let" in the name
if (!letIdentifier.GetText().ToString().Contains("Let"))
{
return;
}
var semanticModel = context.SemanticModel;
var unsafeNestedInvocations = functionChain.ArgumentList
//Get nested method calls
.DescendantNodes().OfType<InvocationExpressionSyntax>()
//Get any identifier names in those calls
.SelectMany(node => node.DescendantNodes().OfType<IdentifierNameSyntax>())
//Get tuples of syntax nodes and the methods they refer to
.Select(node => new
{
Node = node,
Symbol = semanticModel.GetSymbolInfo(node).Symbol as IMethodSymbol
})
//Ignore identifiers that do not refer to methods
.Where(x => x.Symbol != null
//Ignore methods that do not have "IServiceResponse" in the return type
&& x.Symbol.ReturnType.ToDisplayString().Contains("IServiceResponse"));
//Just report the first one to reduce error log clutter
var firstUnsafe = unsafeNestedInvocations.FirstOrDefault();
if (firstUnsafe != null)
{
var diagnostic = Diagnostic.Create(_Rule, firstUnsafe.Node.GetLocation(), firstUnsafe.Node.GetText().ToString());
context.ReportDiagnostic(diagnostic);
}
}
}
}
问题
我的分析器适用于任何 *.cs
当前打开的文件。警告被添加到错误窗口,绿色警告下划线显示在文本编辑器中。但是,如果我关闭包含带有这些警告的调用站点的文件,错误将从“错误”窗口中删除。此外,如果我只是在没有打开文件的情况下编译我的解决方案,则不会记录任何警告。在 Debug模式下运行分析器解决方案时,如果在 Visual Studio 的调试沙箱实例中没有打开源代码文件,则不会命中断点。
如何让我的分析器检查所有文件,甚至是关闭的文件?
最佳答案
我从这个问题中找到了答案:How can I make my code diagnostic syntax node action work on closed files?
显然,如果您的分析器是作为 Visual Studio 扩展安装的,而不是作为项目级包安装的,它默认只分析打开的文件。您可以转到工具 > 选项 > 文本编辑器 > C# > 高级并选中启用完整解决方案分析以使其适用于当前解决方案中的任何文件。
多么奇怪的默认行为。 ¯\_(ツ)_/¯
关于c# - Roslyn 分析器仅针对打开的文件运行,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49592058/