You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何编写自定义Roslyn分析器检测特定类型null引用风险

.NET Framework 4.8 自定义Roslyn分析器实现配置返回值空检查检测

你当前的实现思路存在核心偏差,不需要调用全量数据流分析接口就能覆盖你提到的限定场景(无数组/集合存储IConfiguration实例的写法),具体实现步骤如下:

修正现有代码的基础判断逻辑

你当前的类型判断逻辑存在bug:当开发人员用var声明变量时,localDeclaration.Declaration.Type对应的是var关键字,无法直接获取到IConfiguration类型符号,必须先通过语义模型拿到局部变量本身的符号再判断类型,同时要过滤掉非GetConfiguration方法返回的IConfiguration实例避免误报。

// 提前定义诊断规则,放在分析器类的静态成员中
private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
    id: "CUSTOM001",
    title: "IConfiguration实例访问前必须做null检查",
    messageFormat: "IConfiguration变量 '{0}' 来自GetConfiguration返回值,访问成员前未做null检查",
    category: "Reliability",
    defaultSeverity: DiagnosticSeverity.Warning,
    isEnabledByDefault: true);

public override void Initialize(AnalysisContext context)
{
    context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
    context.EnableConcurrentExecution();
    context.RegisterSyntaxNodeAction(AnalyzeDeclaration, SyntaxKind.LocalDeclarationStatement);
}

private static void AnalyzeDeclaration(SyntaxNodeAnalysisContext context)
{
    var localDeclaration = (LocalDeclarationStatementSyntax)context.Node;
    // 仅处理单变量声明,适配你方代码库场景
    if (localDeclaration.Declaration.Variables.Count != 1) return;
    var variable = localDeclaration.Declaration.Variables[0];
    // 跳过无初始化值、初始化值不是await表达式的声明
    if (variable.Initializer?.Value is not AwaitExpressionSyntax awaitExpr) return;

    var compilation = context.SemanticModel.Compilation;
    // 替换为你项目中两个类型的实际全限定名
    var configType = compilation.GetTypeByMetadataName("YourProjectNamespace.IConfiguration");
    var configServiceType = compilation.GetTypeByMetadataName("YourProjectNamespace.IConfigurationService");
    if (configType == null || configServiceType == null) return;

    // 精确判断变量类型是否为IConfiguration
    var variableSymbol = context.SemanticModel.GetDeclaredSymbol(variable, context.CancellationToken) as ILocalSymbol;
    if (!SymbolEqualityComparer.Default.Equals(variableSymbol?.Type, configType)) return;

    // 精确判断await的调用是否为IConfigurationService.GetConfiguration方法
    if (awaitExpr.Expression is not InvocationExpressionSyntax invocation) return;
    if (context.SemanticModel.GetSymbolInfo(invocation, context.CancellationToken).Symbol is not IMethodSymbol calledMethod) return;
    if (!SymbolEqualityComparer.Default.Equals(calledMethod.ContainingType, configServiceType) 
        || calledMethod.Name != "GetConfiguration") return;

实现后续使用的空检查检测

因为不需要覆盖复杂场景,直接遍历当前声明所在代码块内、声明语句之后的所有语法节点即可:

  1. 标记是否已经遇到对该变量的null检查
  2. 如果在第一次非空传播的成员访问前没有null检查,直接在成员访问位置上报诊断
  3. 遇到null检查后更新标记,后续的成员访问不再报错
// 找到声明所在的最内层代码块
    var parentBlock = localDeclaration.FirstAncestorOrSelf<BlockSyntax>();
    if (parentBlock == null) return;
    var declIndex = parentBlock.Statements.IndexOf(localDeclaration);
    if (declIndex < 0) return;

    var nullCheckPassed = false;
    // 遍历声明之后的所有语句
    for (var i = declIndex + 1; i < parentBlock.Statements.Count; i++)
    {
        var statement = parentBlock.Statements[i];
        foreach (var node in statement.DescendantNodesAndSelf())
        {
            // 跳过非目标变量的引用
            if (node is not IdentifierNameSyntax idNode 
                || !idNode.Identifier.Text.Equals(variableSymbol.Name, StringComparison.Ordinal))
                continue;
            var refSymbol = context.SemanticModel.GetSymbolInfo(idNode, context.CancellationToken).Symbol;
            if (!SymbolEqualityComparer.Default.Equals(refSymbol, variableSymbol)) continue;

            // 识别null检查场景,更新标记
            if (IsInNullCheckContext(idNode))
            {
                nullCheckPassed = true;
                continue;
            }

            // 识别非空传播的成员访问
            if (node.Parent is MemberAccessExpressionSyntax memberAccess 
                && memberAccess.Expression == node
                && !memberAccess.OperatorToken.IsKind(SyntaxKind.QuestionToken))
            {
                if (!nullCheckPassed)
                {
                    context.ReportDiagnostic(Diagnostic.Create(Rule, memberAccess.GetLocation(), variableSymbol.Name));
                    // 上报第一个违规点后直接返回,减少不必要遍历
                    return;
                }
            }
        }
    }
}

辅助方法:识别null检查上下文

补充IsInNullCheckContext的判断逻辑,覆盖常见的null检查写法:

private static bool IsInNullCheckContext(IdentifierNameSyntax variableNode)
{
    var parent = variableNode.Parent;
    // 1. 二元表达式判断:== null / != null
    if (parent is BinaryExpressionSyntax binary 
        && (binary.IsKind(SyntaxKind.EqualsExpression) || binary.IsKind(SyntaxKind.NotEqualsExpression)))
    {
        var otherSide = binary.Left == variableNode ? binary.Right : binary.Left;
        if (otherSide.IsKind(SyntaxKind.NullLiteralExpression)) return true;
    }
    // 2. is模式判断:is null / is not null
    if (parent is IsPatternExpressionSyntax isPattern)
    {
        if (isPattern.Pattern is ConstantPatternSyntax constPat && constPat.Expression.IsKind(SyntaxKind.NullLiteralExpression))
            return true;
        if (isPattern.Pattern is UnaryPatternSyntax unaryPat && unaryPat.IsKind(SyntaxKind.NotPattern) && unaryPat.Pattern is ConstantPatternSyntax cPat && cPat.Expression.IsKind(SyntaxKind.NullLiteralExpression))
            return true;
    }
    // 3. 空传播访问:variable?.XXX
    if (parent is ConditionalAccessExpressionSyntax conditionalAccess && conditionalAccess.Expression == variableNode)
        return true;
    // 4. 可按需扩展你方代码库常用的null守卫方法,比如ArgumentNullException.ThrowIfNull、Guard.NotNull等
    return false;
}

实现说明

  • 所有类型判断都使用语义模型拿到的ITypeSymbol做比对,禁止用字符串比较类型名,避免不同命名空间下的同名类型导致误判
  • AnalyzeDataFlow接口适合做跨分支、跨赋值的复杂数据流判断,对于你限定的场景来说过重,且需要准确传入分析的起止节点范围;直接遍历语法节点的实现性能更高、逻辑更可控,完全满足需求
  • 该实现完全兼容.NET Framework 4.8项目,不需要额外依赖

内容的提问来源于stack exchange,提问作者Johnny5

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 12:42:14