【问题标题】:Roslyn analyzer only runs for open filesRoslyn 分析器仅针对打开的文件运行
【发布时间】:2018-03-31 20:22:50
【问题描述】:

背景故事(对于理解我的问题不是必需的,但一些上下文可能会有所帮助)

在我的公司,我们使用IResult<T> 类型以函数式的方式处理错误,而不是抛出异常并希望某些客户端捕获它们。 IResult<T> 可以是带有TDataResult<T> 或带有ErrorErrorResult<T>,但不能同时是两者。 Error 大致相当于Exception。所以一个典型的函数会返回IResult<T>,通过返回值传递任何遇到的错误,而不是用throw备份堆栈。

我们在IResult<T> 上有扩展方法来组成函数链。两个主要的是BindLet

Bind 是函数式语言中的标准一元 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&lt;T&gt; API 时常犯的错误是调用一个函数,该函数在传递给LetAction&lt;T&gt; 内返回IResult&lt;T&gt;。发生这种情况时,如果内部函数返回 Error,则错误会丢失,并且会继续执行,就像没有出错一样。当它发生时,这可能是一个很难追踪的错误,并且在过去一年中已经发生了好几次。 在这些情况下,应改用Bind,以便可以传播错误。

我想识别对在作为参数传递给Let 的 lambda 表达式中返回 IResult&lt;T&gt; 的函数的任何调用,并将它们标记为编译器警告。

我已经创建了一个分析器来执行此操作。你可以在这里查看完整的源代码: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 文件。警告被添加到错误窗口,绿色警告下划线显示在文本编辑器中。但是,如果我关闭包含带有这些警告的调用站点的文件,则错误将从错误窗口中删除。此外,如果我只是在没有打开文件的情况下编译我的解决方案,则不会记录任何警告。在调试模式下运行分析器解决方案时,如果在 Visual Studio 的调试沙箱实例中没有打开源代码文件,则不会命中断点。

如何让我的分析器检查所有文件,甚至是关闭的文件?

【问题讨论】:

标签: c# visual-studio roslyn


【解决方案1】:

我从这个问题中找到了答案:How can I make my code diagnostic syntax node action work on closed files?

显然,如果您的分析器是作为 Visual Studio 扩展安装的,而不是作为项目级包安装的,则它默认只分析打开的文件。您可以转到工具 > 选项 > 文本编辑器 > C# > 高级 并选中 启用完整解决方案分析 以使其适用于当前解决方案中的任何文件。

多么奇怪的默认行为。 ¯\_(ツ)_/¯

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-07-24
    • 2019-03-03
    • 1970-01-01
    • 2016-06-25
    • 1970-01-01
    • 2021-01-29
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多