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

如何借助CSharpSyntaxRewriter识别#region与#if DEBUG内的语法节点

可靠判定的核心原理

Roslyn语法树为每一个元素分配了和源码行位置严格对齐的文本跨度(TextSpan),即使#region、#if这类预处理指令是作为语法节点的附属trivia存在,它们的起始、结束位置也完全匹配源码书写顺序。
判断某节点是否被某个预处理块覆盖,本质是文本跨度的包含关系判断,和遍历顺序、指令书写先后没有任何关系。之前遇到的所有判定失效问题,根源都是试图用遍历过程中的时序状态打标代替绝对位置的包含判断——CSharpSyntaxRewriter默认采用“节点优先遍历、trivia后置处理”的机制,当你走到目标类节点的时候,它前置的预处理指令作为trivia还没被遍历到,状态标记自然为空,根本不可能准确判定。

必须使用CSharpSyntaxRewriter的适配实现

这个方案完全不破坏现有Rewriter的工作流,所有依赖Rewriter输出的上层模块不需要做任何修改,整体分两步执行:

第一步:Rewriter启动前的预处理扫描

在拿到语法树根节点、初始化Rewriter之前,全量遍历语法树的所有trivia,提前收集两类指令的文本跨度列表,存在Rewriter可访问的只读集合里:

  • 收集所有#region/#endregion配对块的完整跨度:找到每个RegionDirectiveTrivia的配对结束指令,把从#region起始位置到#endregion结束位置的完整范围存下来
  • 收集所有条件为DEBUG的#if/#endif配对块的完整跨度:筛选出IfDirectiveTrivia中条件表达式为DEBUG的起始指令,找到配对的EndIfDirective,把对应范围存下来

这一步只是提前准备判定需要的元数据,不会替代Rewriter的核心遍历逻辑。
核心扫描代码片段:

var root = tree.GetRoot();
var regionSpans = new List<TextSpan>();
var debugIfSpans = new List<TextSpan>();

foreach (var trivia in root.DescendantTrivia())
{
    // 匹配region块
    if (trivia.GetStructure() is RegionDirectiveTrivia regionStart)
    {
        var regionEnd = regionStart.GetMatchingEndRegionDirective();
        regionSpans.Add(TextSpan.FromBounds(regionStart.Span.Start, regionEnd.Span.End));
    }
    // 匹配DEBUG条件的#if块
    if (trivia.GetStructure() is IfDirectiveTrivia ifStart &&
        ifStart.Condition.ToString().Trim() == "DEBUG")
    {
        var ifEnd = ifStart.GetMatchingEndIfDirective();
        debugIfSpans.Add(TextSpan.FromBounds(ifStart.Span.Start, ifEnd.Span.End));
    }
}

第二步:在Rewriter的节点回调中做包含判断

重写你需要处理的节点访问方法(比如VisitClassDeclaration),拿到当前访问的语法节点后,用节点自身的Span和之前存的两个跨度列表做匹配:只要当前节点的Span完全落在至少一个region跨度内,同时完全落在至少一个DEBUG条件的#if跨度内,就判定为需要处理的目标节点。
核心判定代码片段:

public override ClassDeclarationSyntax VisitClassDeclaration(ClassDeclarationSyntax node)
{
    var currentNodeSpan = node.Span;
    // 判断是否处于任意#region块覆盖范围
    bool isInRegion = regionSpans.Any(span => span.Contains(currentNodeSpan));
    // 判断是否处于任意#if DEBUG块覆盖范围
    bool isInDebugBlock = debugIfSpans.Any(span => span.Contains(currentNodeSpan));

    if (isInRegion && isInDebugBlock)
    {
        // 执行对应处理:删除、提取、保留逻辑
    }

    return base.VisitClassDeclaration(node);
}
常见边界问题说明
  • 不管两个指令的书写顺序怎么颠倒(比如先写#if DEBUG再写#region Test),只要两个块的跨度都覆盖当前节点,就会被准确识别,完全不会出现漏判
  • 不需要在访问trivia的回调里维护嵌套状态栈:CSharpSyntaxRewriter默认不会按源码行顺序遍历trivia,维护栈的方式天然存在时序差,远不如跨度判断可靠
  • 不需要根据节点名称(比如类名是不是带Test后缀)做粗粒度匹配,只要在目标区间内的所有节点(包括Utility、StaffHelper这类不带Test标识的辅助类)都会被准确捕获
  • 嵌套的其他预处理块(比如#if RELEASE)不会造成误判,因为我们只收集条件为DEBUG的#if块跨度
  • 这套判定逻辑对不规范的遗留代码兼容性极强:即使代码存在语法瑕疵,Roslyn生成的语法树依然会保留准确的指令配对关系和位置跨度,不会因为结构问题出现漏判、误判

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.22 23:06:04