- 开发工具
- 静态分析
- IDE
- 代码质量
【免费下载链接】pyrefly
A fast type checker and language server for Python
Pyrefly 作为面向 Python 的类型检查器与语言服务器(LSP),在 IDE 中提供了边输入边反馈的诊断体验。本文以仓库中streaming测试夹具及其依赖结构为切入点,结合 server.rs 的流式诊断实现、streamDiagnostics配置项与配套 LSP 交互测试,完整剖析 Pyrefly 如何在后台增量重检(recheck)过程中将类型错误沿模块依赖链"流式"推送给编辑器,并解释其收敛与防冲突策略。读完本文,你将理解该机制的触发条件、实现原理与可配置项,并能复现对应的 LSP 测试场景。
一、什么是流式诊断:先看测试夹具的依赖结构
流式诊断(streaming diagnostics)指的是:当后台的重检事务仍在进行时,Pyrefly 不等待全部文件验证完成,而是每验证完一个打开的文件就立即通过 LSP 的publishDiagnostics通知推送该文件的诊断结果,从而让编辑器在类型检查完成前就能展示最新错误。
要理解这一机制,先看它对应的最小复现夹具。仓库中 streaming 测试夹具 目录下的 README.md 用一张依赖图描述了文件的组织方式:
a \ e -> d -> c -> b该目录下包含a.py、b.py、c.py、d.py、e.py五个模块与一个空的 pyrefly.toml。结合各文件的 import 语句,依赖关系可以展开为:
- b.py 定义基础符号:
X = 1(依赖链的根); - c.py
import b并重导出X = b.X; - d.py
import c,声明X: int = c.X与X2 = c.X; - e.py
import d并取用X = d.X2; - a.py 直接
import b,即图中位于顶端的a通过斜线指向b。
这张图的本质是一条单链依赖:e -> d -> c -> b,外加a -> b的旁路。它专门用于测试"修改依赖链根部的一个文件,错误如何沿链传播到上游打开的文件",以及流式诊断在传播过程中如何被逐级发布。
二、错误沿依赖链传播:为什么下游文件会"连带报错"
在类型检查中,上游模块的类型错误会沿 import 关系传导到下游。以夹具为例,如果修改b.py把X = 1改成X = ''(由int变为str):
d.py中X: int = c.X的注解声明与b.X的str类型冲突,d.py产生 1 个类型错误;- 而
a.py、c.py、e.py没有显式类型注解,因此不会新增错误。
这正是 diagnostic.rs 中test_stream_diagnostics_after_save测试所验证的场景:仅打开d.py与b.py两个文件,修改并保存b.py后,期望d.py先收到一条"流式"错误(Streaming来源),随后在重检事务提交完成后再次收到同样数量的错误(CommittingTransaction来源),用于确认最终收敛结果。测试代码见 diagnostic.rs。
从这里可以看到流式诊断的核心价值:错误不是等整个项目重检完才一次性出现,而是沿着依赖链逐个文件地"冒泡"出来,让用户在最关心的打开文件上第一时间看到影响。
三、实现原理:invalidate_queue与重检订阅者
流式诊断的底层实现在 server.rs 的invalidate_queue方法中。整个流程可以拆解为四步:
3.1 筛选可流式传输的打开文件
重检事务启动前,服务器先收集当前所有打开文件的句柄(handle),并调用should_stream_diagnostics过滤出"所在工作区启用了流式诊断"的文件集合,见 server.rs。该集合随后被保存到字段currently_streaming_diagnostics_for_handles中。
3.2 通过订阅者回调即时发布
接着服务器为事务注册一个订阅者(subscriber),其回调逻辑是:每当某个句柄被验证且其内容确实发生变化(changed为真)、且该句柄在流式集合内时,立即调用publish_for_handles发布该文件的诊断,来源标记为DiagnosticSource::Streaming。相关代码见 server.rs。
结合validate_in_memory_for_transaction优先验证当前打开文件的策略,意味着重检过程中打开文件的诊断会"验证一个、发布一个",这就是流式反馈的数据通路。
3.3 事务提交后二次发布,保证收敛
重检事务执行完毕后,服务器会提交事务,并通过publish_for_handles以DiagnosticSource::CommittingTransaction为来源再发布一遍所有打开文件的诊断,同时清空currently_streaming_diagnostics_for_handles(见 server.rs)。这一步确保流式期间可能遗漏或过期的诊断最终以权威结果收敛。
3.4 非可提交事务的避让策略
当后台同时存在一个不可提交(non-committable)事务时,它会读取currently_streaming_diagnostics_for_handles快照,跳过所有正在被可提交事务流式发布的文件,只对集合外的文件发布诊断,避免两个事务对同一文件发出互相冲突的publishDiagnostics。若所有打开文件都处于流式状态,则直接跳过发布并记录日志"Skip publishDiagnostics, all open files are currently being rechecked",见 server.rs。
3.5 字段的语义设计
字段currently_streaming_diagnostics_for_handles的注释(server.rs)完整定义了流式诊断的生命周期语义:
None:当前没有正在进行的重检;- 空集合:存在重检,但重检开始时所有打开文件都已被后续修改(此时不再流式发布);
- 非空集合:正在流式发布这些文件;
- 如果某个文件在重检开始后被修改,它会被移出该集合——此时本地诊断可以基于"旧状态 + 本地编辑"继续展示;
- 后台重检结束后,文件移出集合,并再运行一次事务确保诊断收敛。
四、配置项:streamDiagnostics与should_stream_diagnostics
流式诊断并非强制行为,而是可以由客户端按工作区开关的配置项。
4.1 服务端存储与更新
在 workspace.rs 中,每个工作区持有一个stream_diagnostics: Option<bool>字段(workspace.rs)。LSP 初始化时服务器根据客户端传入的pyrefly.streamDiagnostics调用update_stream_diagnostics按作用域(具体工作区或默认工作区)写入该值,见 workspace.rs。
4.2 判断函数与默认值
判断某个文件是否启用流式诊断的逻辑集中在 workspace.rs:
pub fn should_stream_diagnostics(&self, path: &Path) -> bool { self.get_with(path.to_path_buf(), |(_, workspace)| { workspace.stream_diagnostics.unwrap_or(true) }) }默认值为true,即未显式配置时流式诊断始终开启。
4.3 编辑器端设置
对应地,VSCode 扩展中该开关名为python.pyrefly.streamDiagnostics,类型为 boolean,默认true。官方说明位于 website/docs/IDE.mdx:
若为 true,Pyrefly 会在重检过程中按可用性流式发布诊断,提供增量反馈;设为
false则只在完整重检结束后统一发布。
在非 VSCode 客户端中,可以通过 LSP 的initializationOptions传入该设置(键名去掉python.前缀)。IDE.mdx 给出了包含所有默认值的完整示例,其中pyrefly一节即包含"streamDiagnostics": true:
{ "pyrefly": { "typeCheckingMode": "auto", "disableTypeErrors": false, "displayTypeErrors": "default", "diagnosticMode": "openFilesOnly", "extraSearchPaths": [], "streamDiagnostics": true, "syncNotebooks": true, "runnableCodeLens": false } }实用建议:日常开发建议保持默认true以获得即时反馈;若你的客户端对publishDiagnostics的高频推送敏感、或需要稳定的完整诊断快照(例如批量流水线场景),可将其设为false换取"一次性全量发布"的确定性行为。
五、边界场景:recheck 期间的并发编辑与文件打开
流式诊断机制还必须处理"用户在重检进行中继续编辑/打开文件"的并发场景,仓库用同一套 streaming 夹具覆盖了这些边界情况:
5.1 编辑文件发生在重检期间
test_edit_file_during_recheck(diagnostic.rs)演示了这样的流程:先打开b.py与d.py,保存b.py触发重检;在重检被测试钩子do_not_commit_next_recheck挂起期间,通过didChange编辑d.py(追加Y: int = ''与Z: int = ''两行带错误代码)。此时:
- 流式错误被替换为"基于旧状态 + 本地编辑"的诊断——
d.py立即收到 2 条错误; - 恢复重检并提交后,基于最新状态再次发布——
d.py收敛为 3 条错误(原有 1 条 + 新增 2 条)。
这个测试验证了注释中描述的语义:文件在重检开始后被修改,就会从流式集合中移除,本地诊断基于"旧状态 + 本地编辑"继续显示,并在后台完成后由新事务收敛。
5.2 打开文件发生在重检期间
test_open_file_during_recheck(diagnostic.rs)则验证:仅打开b.py并保存触发重检,在重检挂起期间打开d.py——由于新文件基于旧状态计算,d.py先收到 0 条错误的初始诊断;重检提交后,d.py的错误数收敛为 1。该测试目前标记为#[ignore],原因注释指向 CI 上的偶发不稳定性。
5.3 快速撤销编辑时的"无闪烁"保障
test_stream_diagnostics_no_flicker_after_undo_edit(diagnostic.rs)模拟了"连续两次快速编辑":先把b.py的1改成''(制造错误),在第一次重检事务尚未提交时立即还原b.py内容并再次保存。由于第二次保存发生时第一次事务还挂着,收敛后d.py的错误数必须在 0~1 之间(expect_publish_diagnostics_must_have_error_count_between),最终第二次重检后收敛为 0。这保证了流式发布 + 并发编辑不会造成诊断的"闪烁"(先报错、又消失、再报错的抖动)。该测试同样被标记为#[ignore],注释说明其在 GitHub CI 上偶发不稳定。
5.4 文件系统事件突发
在 file_watcher.rs 中,同一 streaming 夹具还被用于test_consecutive_file_watcher_events:模拟git checkout这类"多个文件同时变化"的场景,先发送多个针对未变化文件(a.py)的DidChangeWatchedFilesNotification噪音事件,再发送真正的变更(清空b.py),验证连续的 watcher 事件最终都会被处理,且噪音不会导致误报。
六、诊断来源标识与调试线索
流式机制在发布诊断时通过DiagnosticSource区分触发来源,常见取值包括:
Streaming:重检过程中按文件验证完成顺序的增量发布;CommittingTransaction:可提交事务提交后的最终发布(收敛);NonCommittableTransaction:不可提交事务对非流式文件的发布;DidClose:文件关闭时清空诊断。
这些来源同时服务于遥测与测试断言,帮助开发者判断一次publishDiagnostics究竟来自流式阶段还是收敛阶段——例如test_stream_diagnostics_after_save中先用"最终错误数为 1"的宽松断言接收流式发布,再用"必须为 1"的严格断言确认事务提交后的收敛发布。
七、小结
Pyrefly 的流式诊断机制以"打开文件优先验证 + 订阅者即时回调"为骨架,配合currently_streaming_diagnostics_for_handles的状态机语义,在重检过程中沿模块依赖链逐文件推送诊断,并在事务提交后二次发布保证收敛;同时通过"非可提交事务避让流式文件"与"重检中修改的文件移出流式集合"两条规则,避免并发编辑场景下的诊断冲突与闪烁。streamDiagnostics配置项(默认开启)让客户端可以按工作区灵活关闭该行为。
如果想进一步研究,推荐阅读以下仓库路径:
- 测试夹具与依赖图:streaming 目录(含 README.md 与五个模块文件);
- 核心实现:server.rs 中的
invalidate_queue与currently_streaming_diagnostics_for_handles字段注释; - 工作区配置:workspace.rs 中
should_stream_diagnostics与update_stream_diagnostics; - LSP 交互测试:diagnostic.rs 与 file_watcher.rs 中引用
streaming夹具的全部用例; - 编辑器端设置文档:IDE.mdx 的
streamDiagnostics小节与initializationOptions示例。
- 开发工具
- 静态分析
- IDE
- 代码质量
【免费下载链接】pyrefly
A fast type checker and language server for Python
相关推荐
LSP 3.17 Pull Diagnostics 诊断拉取机制详解:从服务器推送通知到客户端驱动的诊断计算
LSP 3.17 Pull Diagnostics 诊断拉取机制详解:从服务器推送通知到客户端驱动的诊断计算 本文基于当前仓库 Language Server
开发工具TypeScript Go 编译器本地化诊断(Localized Diagnostics)机制全解析
TypeScript Go 编译器本地化诊断(Localized Diagnostics)机制全解析 本文围绕 tsc/internal/diagnostics
编程语言编译器开发工具Claude Code 系统提示词解析:New Diagnostics Detected 诊断通知机制
Claude Code 系统提示词解析:New Diagnostics Detected 诊断通知机制 本篇技术指南围绕 Claude Code 系统提示词仓库
文档提示工程人工智能
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考