它没报错,也没干活
上一篇埋了一个发现没展开:Tier 的家族规则,带着九处”早就豁免过”的违规,安安静静几个月没有执法——配置键写在 .editorconfig 的节头之前,Roslyn 把它静默忽略了。
这不是一次意外,而是一族事故的基本形态。做编译期这条线(生成器、分析器、构建工具)以来,踩到的失败几乎全是静默的,数一数,有四种:
- 不加载:生成器明明在包里,编译器装作没看见;
- 不读取:配置语义写对了,位置写错了;
- 不匹配:大小写、通配符、段位,差一个字符,规则整体失活;
- 不登记:人工维护的清单漂移了,没人喊。
四种形态有一个共同点:构建依然是绿的。 这篇逐个摊开。
不加载:76 个”类型不存在”
最贵的一次在 Tier:生成器包从 4.8.0 升到 4.9.2,SDK 还是 8.0.130(编译器自带 Roslyn 4.8.0)——版本错配的结果不是报错,是静默拒载:生成器根本没被加载,[BinaryLayout] 编解码器零产出,下游炸出一片”类型不存在”——TierVolume 76 个 CS0103,摊在眼前的是症状海,根因在哪完全看不出来。
这类失败的形态值得记下来:“标注没有生成输出”,不是什么显眼的编译错误;唯一可见的线索是一条 CS9057 警告,淹没在 76 条错误里。销案是降级对齐”文件版本”4.800.25.56925,从此立下纪律:Roslyn 统一钉 4.8.0,“升 SDK 前先核 Roslyn 文件版本再动此处”。
还有一个更隐蔽的邻支:加载契约的宿主面。Roslyn 对生成器的宿主加载面是 netstandard2.0——net8.0 产物在 dotnet build 里能加载,到了 VS IDE / 旧 Roslyn 宿主静默不载,失败形态还是那句”没有生成输出”。这就是 analyzer 包必须 netstandard2.0 的全部理由:不是兼容性洁癖,是卸载风险的形状。
两次事故共同给出了一个哨兵:生成器静默不载的天然信号 = 生成依赖爆炸(CS0103 型编译错误批量出现)——“哨兵在即回归在”。而验收要落到行为上:generated/ 目录里有 .g.cs 才算生效。
不读取:写在节外面的配置
第二形态:配置被写了,但没被读到。
实测现场(隔离项目):键写在 .editorconfig 任何节头之前——无节键 0 命中;同样的键移进 [*] 节——2 命中。Roslyn 的读取语义是:.globalconfig(is_global = true)进全局选项;.editorconfig 的键只进 per-tree 选项,而节头之前的键哪都不进。家族纪律现在写死了:键必须落在 .globalconfig 或 .editorconfig 的节内。
同族还有一次更早的”读半边”:规则只读 GlobalOptions,而 .editorconfig 的键进的是 per-tree options——CLI dotnet build 下治理键零产出、规则静默失效,Tier 仓自己吃自己的狗粮,同样吃的这个盲区。根修是把 per-tree options 做并集合并,外加一条漂亮的冲突纪律:跨源同键异值 = 配置冲突,fail-fast,绝静默取一。
这也是为什么 Loom 的诊断里,配置类错误一律 Error、文案收尾统一六个字:“配置错误绝静默。”
不匹配:大小写和通配符
第三种静默:两串文本”看起来一样”。
Tier 的规则左值按程序集名匹配,实现最初用了大小写敏感的 Ordinal——配置里写 KernLab.Tier.Runtime、实际 AssemblyName 大小写稍有出入,规则整体静默不激活。最毒的是观测形态:配置校验规则(139)照常开火,说明”配置本身能用”,而 131/132 零报告——零警告、零报错,一切”正常”。修复是大小写不敏感匹配,回归测试名字直接叫判例:“配置左值与实际 AssemblyName 大小写不一致时规则静默失活是 #491 级缺陷”。(命名空间的比较则有意保持大小写敏感——哪个是缺陷、哪个是设计,要分得开。)
同形态的还有通配符。CI 的 global.json 写过 8.0.4xx + latestFeature,看起来是”钉在 8.0.4xx 特性带”——实际被静默无视:runner 上装了 SDK 10.0.401,dotnet 直接解析到 10.0.401(dotnet --list-sdks 取证:8.0.425 就在旁边,“钉版”根本没生效)。SDK 10 下的 pack 给出当天最著名的一句日志:
error MSB4181: The "RestoreTask" task returned false but did not log an error.
失败了,但什么都没说。 最后是精确钉 8.0.422(本地验证环境同版)才收口——“通配会被 latestMajor 滚上未知大版本”从此写进所有 CI 桩的参数说明里。
不登记:人工清单必然漂移
第四种:规则本身是活的,但”登记册”失焦。
每个 analyzer 包都要维护 AnalyzerReleases.Shipped/Unshipped.md(release 跟踪)。Loom 早期排查一个 RS2007 非确定性误报时,四个包的登记文件被”修回”了 Tier 时代的内容——ID 还是老前缀。另一处是格式化笔误产出的 KLSG0554(应为 KLSG05x 段)。最伤的是第三处:EF 面的诊断段(120-143)从未登记进家族注册表,于是它与 Analyzers 的 130-139、Validation 的 140-144 三方撞号——同一段 ID 两个面各自使用,谁都不报错。
修法是两条:从源码真值重建登记(以各包源码里的 DiagnosticDescriptor 字面量为唯一事实源重新誊写),以及立一道护栏测试:
- 源码 ID ↔ 登记表一一对应(缺一个、多一个都红);
- 跨面不撞号;
- ID 必为
KLSG+ 三位数字(笔误的直接产物)。
测试类头注释就是判词:“人工维护必然漂移,故立此门。” EF 那段随后整段迁到 KLSG200-259(+80 映射、预留 60 位),组织注册表同步补登。顺带留了个诚实的小尾巴:机械改名在 README 里留下了几处 KLSG210-133 这类文本腐蚀——文档也是人工维护的,同病。
还有一族的错觉:生成物”能编译”
以上四种是”东西没生效”。反过来还有一类同样危险:东西生效了,但生成的东西从没被真正编译过。
黄金快照只比文本不编译——于是两处生成缺陷是”语料从未编译过该形态”才漏出去的:[WireMessage] 有个属性叫 Namespace,派生出的局部变量撞 C# 关键字(namespace),消费方被迫给协议改名;端点的 GET 查询串在”非可空基元”这条缺省分支上生成了 ((global::int)x)——C# 关键字不能被 global:: 限定。修复之外,回归口径升级为:语料补齐各形态 + 生成产物必须编译干净(真桩上 FrameworkReference 真编译)+ 真宿主 E2E。判词一句话:golden 是文本仪器,编译是行为仪器,两个都要有。
平台差异同理:同一个发射器在 Windows 用 AppendLine(\r\n)、Linux 是 \n,TrimEnd('\n') 只裁一半——同一份生成器,两个平台换行差一个。金片测试此前从没上过 Linux CI,PR 首跑即暴露;修法是 144 处 AppendLine 全改 Append('\n'),确定性发射,双平台字节级一致。
为什么这一族这么难
因为症状与根因隔着四层:版本(Roslyn/SDK)、位置(配置键落在哪)、命名(大小写、通配、段位)、宿主(CLI/IDE、Windows/Linux)。四层里任何一层静默,观感都是同一句话:“它没报错,也没干活。”
我们的对策也归成四条:
- 契约面改名必须行为断言验收——“故意写违规代码,断言必须报错”,改名后要同步键名、键值里的程序集前缀、
dotnet.diagnostic.<ID>.severity、#pragma里的 ID 四件套; - 配置错误 fail-fast——跨源同键异值绝不静默取一,未知键、坏语法一律 Error;
- 哨兵与护栏——生成依赖爆炸是拒载的哨兵,登记对账是漂移的护栏,都进 CI;
- 矩阵测试——最低/最高 SDK 各跑一轮、双平台金片、双通道(全局/per-tree)配置用例。
收尾:一条可迁移的判据
如果只能留一句给未来的自己:
编译期工具最贵的 bug 不是崩溃,是沉默:它不加载、不读取、不匹配、不登记——而你的构建依然是绿的。对这类组件,唯一的验收是行为断言——“故意写违规代码,断言必须报错”;“没报错”,从来不是证据。
下一篇换个口味:讲这台织机第一次把产品发出去时的乌龙——一次”空跑”,真的把八个包发上了 nuget。
本文事实来自 KernLab.Loom 的 Roslyn 统一钉版纪律与 Tier 侧 76-CS0103 销案记录、无节键隔离实证与配置通道并集修复、#491 大小写失活判例、global.json 钉版取证链、AnalyzerReleases 登记重建与对账护栏测试、生成物编译缺口的回归升级,与双平台金片判例。
评论