如何编写自定义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;
实现后续使用的空检查检测
因为不需要覆盖复杂场景,直接遍历当前声明所在代码块内、声明语句之后的所有语法节点即可:
- 标记是否已经遇到对该变量的null检查
- 如果在第一次非空传播的成员访问前没有null检查,直接在成员访问位置上报诊断
- 遇到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
相关产品推荐
相关产品推荐

