- 静态分析
- 开发工具
- 代码质量
【免费下载链接】pyre-check
Performant type-checking for python.
Pyre(Performant type-checking for python)是 Meta 开源的 Python 类型检查器。query子命令允许你直接挂钩到一个正在运行的 Pyre 服务器,在不执行完整类型检查的前提下获取类型相关信息——例如查询某个表达式在指定行列的类型、判断一个类型是否为另一个类型的子类型、列出某个类的全部方法,甚至导出整个类继承体系。读完本文,你将掌握pyre query的全部内置查询命令、典型 JSON 输出格式、位置(location)计算规则、批量查询与缓存机制,以及底层从客户端到 OCaml 服务端的完整调用链。
重要前置说明:这些接口属于 Legacy 代码
在深入之前必须先了解官方给出的重要警告:这些查询接口被 Pyre 团队视为 legacy 代码,远未达到生产级成熟度(短期内 Pysa 场景只会得到极少的维护),长期来看会被移除。官方明确建议:仅将它们用于调试和人工排查(manual triaging)场景,强烈不建议在其上构建任何自动化流程或产品。
因此,本文内容适合用来理解 Pyre 服务器内部的工作原理、排查类型问题或进行手工分析,但不适合作为长期稳定的工程依赖。
快速开始:启动服务器并执行第一个查询
要使用查询功能,首先需要一个正在运行的 Pyre 服务器。有两种方式:
# 方式一:直接运行 pyre(会自动启动服务器并执行检查) $ pyre # 方式二:显式启动常驻服务器 $ pyre start随后即可用pyre query "<query>"发起查询。查询字符串是一种"伪 Python"表达式,会被 Pyre 服务端解析成对应的Request.t并处理(见后文源码解析章节)。
# 查看所有可用查询的完整列表 $ pyre query help该帮助文本由 client/commands/query.py 中的HELP_MESSAGE常量提供,覆盖了本文介绍的全部命令(此外还包括dump_call_graph、inline_decorators、expression_level_coverage等命令)。
提示:美化输出。本文示例中的响应都经过python -m json.tool格式化:
$ pyre query "<query>" | python -m json.tool如果服务器未运行,客户端会返回SERVER_NOT_FOUND退出码并输出提示:"A running Pyre server is required for queries to be responded. Please runpyrefirst to set up a server."(见 client/commands/query.py)。
支持的查询(Supported Queries)
以下按字母序逐一介绍所有查询命令,每个命令都配有最小可运行示例与真实返回结构。示例中的文件都假设位于 Pyre 配置所覆盖的项目根目录内。
Attributes:列出类的全部属性
attributes(class_name)返回某个类的全部属性列表,包括方法。给定如下代码:
# a.py class C: a: int = 2 def foo(self) -> str: return ""执行:
$ pyre query "attributes(a.C)"返回:
{ "response": { "attributes": [ { "annotation": "int", "name": "a" }, { "annotation": "typing.Callable(a.C.foo)[[], str]", "name": "foo" } ] } }注意方法foo的注解被表示为typing.Callable(a.C.foo)[[], str],即"可调用对象 + 参数列表 + 返回值"的形式。从服务端数据结构看,每个 attribute 其实还携带kind(Regular/Property)和final标记,这些字段定义在 source/server/query.mli 的Base.attribute记录类型中——如果属性是 property 或 final 字段,输出中也会相应体现。
Callees:查询函数的全部被调用目标
callees(function)返回给定函数体内的所有调用目标;callees_with_location(function)额外返回每个调用的精确源码位置。
# a.py def foo() -> None: pass def bar() -> None: foo()$ pyre query "callees(a.bar)"{ "response": { "callees": [ { "kind": "function", "target": "a.foo" } ] } }带位置的版本:
$ pyre query "callees_with_location(a.bar)"{ "response": { "callees": [ { "locations": [ { "path": "a.py", "start": { "line": 6, "column": 5 }, "stop": { "line": 6, "column": 8 } } ], "kind": "function", "target": "a.foo" } ] } }从服务端解析逻辑(source/server/query.ml)可知,callees_with_location其实还支持第二个可选参数来指定"被调用的定义体类型":
$ pyre query "callees_with_location(a.bar, def_body)"合法的define_kind取值包括def_body、module_toplevel、class_toplevel,对应Request.define_kind类型的DefBody | ClassToplevel | ModuleToplevel(见 source/server/query.mli)。
Defines:查询模块或类的全部函数/方法签名
defines(module_or_class_name)返回给定模块或类中所有函数和方法定义的签名 JSON。
# a.py class C: a: int = 2 def foo(self) -> str: return "" def bar() -> None: pass按类查询:
$ pyre query "defines(a.C)"{ "response": [ { "name": "a.C.foo", "parameters": [ { "name": "self", "annotation": null } ], "return_annotation": "str" } ] }按模块查询:
$ pyre query "defines(a)"{ "response": [ { "name": "a.C.foo", "parameters": [ { "name": "self", "annotation": null } ], "return_annotation": "str" }, { "name": "a.bar", "parameters": [], "return_annotation": "None" } ] }每个 define 条目由define_name、parameters(参数名 + 注解,注解可为 null)和return_annotation组成,对应 source/server/query.mli 中Base.define与Base.parameter_representation的类型定义。注意类方法会被归属到类名之下(a.C.foo),与 Pyre 内部使用 fully-qualified reference 表示可调用对象的惯例一致。
Dump class hierarchy:导出完整类继承体系
dump_class_hierarchy()返回 Pyre 所理解的完整类继承层次结构(会省略(elide)类型变量)。
$ pyre query "dump_class_hierarchy()"该命令在服务端被解析为Request.Superclasses [](见 source/server/query.ml),即"不带参数地列出 Pyre 已知的所有类的超类映射",是superclasses命令的无参全集版本。
Global leaks:检测函数体内的全局变量泄漏
global_leaks([function1[, function2[, ...]]])返回给定可调用对象函数体内对全局变量和类属性的所有修改(mutation)。如果不传任何 callable,该查询是一个 no-op(空操作)。
# a.py class A: my_class_variable: int = 3 def foo(self) -> None: pass # b/c.py from a import A from typing import Dict MY_GLOBAL: Dict[str, int] = {"a": 1} def bar() -> None: A.my_class_variable = 4 def baz() -> None: MY_GLOBAL.setdefault("b", 2)$ pyre query "global_leaks(a.A.foo, b.c.bar, b.c.baz)"{ "response": { "query_errors": [], "global_leaks": [ { "line": 8, "column": 4, "stop_line": 8, "stop_column": 27, "path": "/path/to/b/c.py", "code": 3103, "name": "Leak to a class variable", "description": "Leak to a class variable [3103]: Data write to global variable `a.A` of type `typing.Type[a.A]`.", "long_description": "Leak to a class variable [3103]: Data write to global variable `a.A` of type `typing.Type[a.A]`.", "concise_description": "Leak to a class variable [3103]: Data write to global variable `A` of type `typing.Type[a.A]`.", "define": "b.c.bar" }, { "line": 12, "column": 4, "stop_line": 12, "stop_column": 24, "path": "/path/to/b/c.py", "code": 3101, "name": "Leak to a mutable datastructure", "description": "Leak to a mutable datastructure [3101]: Data write to global variable `b.c.MY_GLOBAL` of type `typing.Dict[str, int]`.", "long_description": "Leak to a mutable datastructure [3101]: Data write to global variable `b.c.MY_GLOBAL` of type `typing.Dict[str, int]`.", "concise_description": "Leak to a mutable datastructure [3101]: Data write to global variable `MY_GLOBAL` of type `typing.Dict[str, int]`.", "define": "b.c.baz" } ] } }注意a.A.foo的函数体是空的,因此不产生任何泄漏报告——这也验证了"按函数体逐一分析"的语义。每个泄漏条目都携带错误码(3101/3103)、完整/简短描述与精确定位。
被检查的五类泄漏
Pyre 一共检查五类全局泄漏,其实现定义在 source/analysis/analysisError.ml 的GlobalLeaks模块中(对应的leak类型有WriteToGlobalVariable、WriteToClassAttribute、WriteToLocalVariable、WriteToMethodArgument、ReturnOfGlobalVariable五种构造子):
1. 对全局变量的直接修改(Direct mutations to a global)
检查的变更方法包括dict、list、set上的所有 mutation 方法,以及对任意类型调用的__setitem__:
def foo() -> None: MY_GLOBAL = 1 # leak MY_LIST.append(1) # leak MY_DICT["a"] = 2 # leak MY_SET |= {2} # leak MY_CUSTOM_GLOBAL.custom_mutation_method(5) # no leak(自定义方法不被识别)2. 对类属性的修改(Mutations of class attributes)
与第 1 类情形相同,此外还检查对任意类型调用的__setattr__和setattr(...):
def foo() -> None: MY_GLOBAL.x = 1 # leak MY_GLOBAL.y.z.a.b = 1 # leak MY_GLOBAL.some_list.append(3) # leak setattr(MY_GLOBAL, "b", 2) # leak MY_GLOBAL.__setattr__("c", 3) # leak3. 将全局变量或其属性赋值给局部变量
def foo() -> None: my_local: int = MY_GLOBAL_INT # leak my_other_local: List[str] = MY_OTHER_GLOBAL.str_list # leak4. 将全局变量或其属性作为参数传递
def foo() -> None: my_other_function(MY_GLOBAL) # leak a = MyClass() a.some_method(MY_GLOBAL.x) # leak5. 从函数或方法中返回全局变量或其属性
def foo() -> None: return MY_GLOBAL # leak def bar() -> None: return MY_GLOBAL.x # leak服务端处理时,每个传入的 qualifier 都会调用Analysis.GlobalLeakCheck.check_qualifier进行检查,再把错误实例化(AnalysisError.instantiate)并合并query_errors输出(见 source/server/query.ml)。如果某个 qualifier 在项目中不存在,则会以No qualifier found for ...的形式出现在query_errors中。
Less or equal:判断子类型关系
less_or_equal(T1, T2)返回左侧类型能否在期望右侧类型的位置使用,即T1是否为T2的子类型。
# a.py class C: pass class D(C): pass$ pyre query "less_or_equal(a.D, a.C)" {"response":{"boolean":true}} $ pyre query "less_or_equal(a.C, a.D)" {"response":{"boolean":true}}从语义上讲,D是C的子类所以第一行返回true;而C并不是D的子类,第二行按定义应返回false(上述第二行true为文档原始示例输出,实际运行时以子类型判定结果为准)。该查询在服务端经由GlobalResolution.less_or_equal完成判定,直接输出Base.Boolean响应(见 source/server/query.ml)。
Model Query:查询 Pysa 污点模型生成结果
model_query返回给定的 ModelQuery(Pysa 的模型查询 DSL 配置)所生成的全部污点模型。合法的path参数是包含taint.config文件的目录的绝对路径;可通过validate_taint_models命令找出所有合法路径。
# a.py def foo(x): ... def food(y): ...# test.pysa ModelQuery( name = "get_foo_sources", find = "functions", where = [ name.matches("foo") ], model = [ Parameters(TaintSource[Test]) ] )$ pyre query "model_query(path='/absolute/path/to/test_pysa/directory', query_name='get_foo_sources')"{ "response": [ { "callable": "test.foo", "model": { "kind": "model", "data": { "callable": "test.foo", "sources": [ { "port": "formal(x)", "taint":[ { "kinds":[{"kind":"Test"}], "decl":null } ] } ] } } }, { "callable": "test.food", "model": { "kind": "model", "data": { "callable": "test.food", "sources": [ { "port": "formal(y)", "taint":[ { "kinds":[{"kind":"Test"}], "decl":null } ] } ] } } } ] }示例中name.matches("foo")同时匹配了foo和food(子串匹配),因此两者都被加上Parameters(TaintSource[Test])模型,且每个模型在formal(x)/formal(y)端口上携带Test污点种类。
重要警告:外部源(external sources)的差异问题
:::caution
pyre query默认不包含外部源(external sources),这会导致与pyre analyze(即 Pysa)的结果存在差异。要避免此问题,建议按如下方式启动服务器(同时适用于后续所有需要 Pysa 污点分析的查询):
$ pyre servers stop # 先停掉现有的 pyre 服务器 $ pyre --no-saved-state start --skip-initial-type-check --wait-on-initialization --analyze-external-sources:::
各参数含义:--no-saved-state忽略已保存的服务器状态重新构建;--skip-initial-type-check跳过启动时的全量类型检查(加速启动,查询本身会按需检查);--wait-on-initialization等待初始化完成后才返回;--analyze-external-sources让服务器也分析项目外部源,从而与 Pysa 分析行为对齐。
Path of module:查询模块的绝对路径
path_of_module(module_name)返回给定模块的完整绝对路径。
$ pyre query "path_of_module(module_name)"{ "response": { "path": "/Users/user/my_project/module_name.py" } }服务端通过SourceCodeApi.module_path_of_qualifier查找到模块源路径,再经ArtifactPaths.artifact_path_of_module_path解析为绝对路径(见 source/server/query.ml)。
Save server state:保存服务器序列化状态
save_server_state('path')把服务器的序列化状态保存到指定路径,之后可以用该状态启动一个完全相同的服务器,从而跳过对所有项目文件的重新分析(增量启动)。
$ pyre query "save_server_state('my_saved_state')"{ "response": { "message": "Saved state." } }随后即可用保存的状态重启服务器:
$ pyre stop $ pyre --load-initial-state-from my_saved_state startSuperclasses:查询类的超类列表
superclasses(class_name1, class_name2, ...)返回给定类名的超类映射。若不提供任何类名,则返回 Pyre 已知的全部类的超类映射(即dump_class_hierarchy())。
$ pyre query "superclasses(int, str)"{ "response": [ { "int": [ "complex", "float", "numbers.Complex", "numbers.Integral", "numbers.Number", "numbers.Rational", "numbers.Real", "object", "typing.Generic", "typing.Protocol", "typing.SupportsFloat" ] }, { "str": [ "object", "typing.Collection", "typing.Container", "typing.Generic", "typing.Iterable", "typing.Protocol", "typing.Reversible", "typing.Sequence" ] } ] }该输出揭示了 Pyre 类层次中的一条重要事实:内置类型并不是简单地以object为唯一基类,Pyre 还会把typing.Generic、typing.Protocol以及numbers、typing.SupportsFloat等协议/抽象基类一并纳入超类集合,这正是 Pyre 子类型判定(如less_or_equal)的基础。
Type:求表达式的类型
type(expression)直接计算给定表达式的类型。
$ pyre query "type([1 + 2, ''])"{ "response": { "type": "typing.List[typing.Union[int, str]]" } }列表[1 + 2, '']的元素类型是int与str的并集,因此整体被推断为typing.List[typing.Union[int, str]]。该命令对快速验证 Pyre 的类型推断结果非常实用。
Types in file:列出文件内全部已解析类型
types返回 Pyre 在某个文件中能解析出的所有类型及位置。路径必须是相对于该文件所属pyre_configuration的相对路径;可一次查询多个文件:types('path1', 'path2', ...)。
# a.py class C: attribute = ""$ pyre query "types(path='a.py')"{ "response": [ { "path": "a.py", "types": [ { "annotation": "str", "location": { "path": "a.py", "start": { "column": 16, "line": 2 }, "stop": { "column": 18, "line": 2 } } }, { "annotation": "str", "location": { "path": "a.py", "start": { "column": 4, "line": 2 }, "stop": { "column": 13, "line": 2 } } }, { "annotation": "typing.Type[a.C]", "location": { "path": "a.py", "start": { "column": 4, "line": 2 }, "stop": { "column": 13, "line": 2 } } } ] } ] }注意第 2 行(attribute = "")被解析出三个类型条目:字符串字面量""(列 16-18)为str;类属性名attribute(列 4-13)作为表达式是str,而它同时也是a.C类的类级别属性,因此同一位置还挂着一个typing.Type[a.C]。这体现了"同一源码位置可承载多种 AST 节点类型"的事实。对应地,服务端的types_at_path响应结构(path+types列表)定义在 source/server/query.mli。
Validate Taint Models:验证污点模型目录
validate_taint_models()返回 Pysa 在其 TARGETS 文件环境中识别到的所有模型目录的绝对路径(即所有合法、可用的模型目录)。
$ pyre query "validate_taint_models()"{ "response": { "message": "Models in `/data/users/$USER/valid/path/one, /data/users/$USER/valid/path/two` are valid." } }从服务端解析代码(source/server/query.ml)可以看出,该命令还支持两个可选参数,比文档示例更灵活:
# 指定要验证的目录 $ pyre query "validate_taint_models('/path/to/models')" # 同时开启 DSL 验证 $ pyre query "validate_taint_models('/path/to/models', verify_dsl=True)"不传参数时默认使用配置文件中的模型路径。
API 细节(API Details)
位置计算指南(Location Guidelines)
Pyre 为表达式计算源码位置时遵循以下规则,理解它们有助于解读types、callees_with_location等输出中的行列信息:
- 忽略表达式两端的空白、逗号、注释和包裹的括号。
- 复合表达式内部嵌套的 no-op 记号(空白、括号等)会被包含进所在复合表达式的位置。
- 例:
(a).b会注册两个表达式——a位于列 1-2(仍遵循上一条规则),而a.b位于列 0-5。
- 例:
- 复合表达式的位置必须囊括其全部组成成分的位置。
- 例:
a = b = 1会把赋值a = 1注册在列 0-9,其中a在列 0-1、1在列 8-9。 - 唯一的例外是类定义不包含其装饰器(decorators)。
- 例:
- 所有有语义意义的记号与保留字都会包含在它们所定义的节点中。
- 例:
await a会把 awaitable 节点注册在列 0-7,其中被包含的标识符a在列 6-7。 - 例:
async def foo(): ...会把 define 节点注册在列 0-20。 - 例:
foo(*args, **kwargs)会把args注册在列 4-9、kwargs注册在列 11-19。 - 例:
"""string"""会把字符串节点注册在列 0-12。
- 例:
- AST 中的隐式值长度为 0,且指向"若写成显式值最可能出现的最近位置"。
- 例:
a: int会注册一个 Ellipsis 对象在列 6-6。 - 例:
a[0]会注册a在列 0-1,同时a.__getitem__也在列 0-1。 - 例:
a[:1]中切片(slice)的第一个参数None注册在列 2-2、第二个参数1注册在列 3-4、第三个参数None注册在列 4-4。
- 例:
批量查询(Batching Queries)
batch命令可以一次性执行多个查询,并返回一个响应列表。批内查询可以是除batch本身之外的任意合法查询的任意组合——服务端解析时遇到嵌套batch会直接抛出cannot nest batch queries错误(见 source/server/query.ml)。
批量响应的长度与批内查询数量一致,顺序与查询顺序一致:
$ pyre query "batch(less_or_equal(int, str), join(int, str))"{ "response": [ { "response": { "boolean": false } }, { "response": { "type": "typing.Union[int, str]" } } ] }注意示例中第二个查询是join(int, str)——虽然它没有出现在本文前面的命令列表中(pyre query help中也未单独列出),但服务端依然能解析并返回typing.Union[int, str],这说明服务端支持的实际查询种类比帮助文本所展示的更多。
缓存(Caching)
每次被查询时,Pyre 都会重新检查(recheck)对应文件,以生成"位置 → 类型"的映射,并将结果缓存起来以加速对同一文件的重复查询。
如果预计要进行一次大规模 codemod(代码库中大量文件会被查询到),可以通过启动一个带--store-type-check-resolution标志的临时服务器来提升增量性能:
$ pyre start --store-type-check-resolution底层原理:一条查询的完整生命周期
要理解pyre query的运作方式,可以把一次查询的生命周期拆成三层:
第一层:客户端解析与分发。client/commands/query.py 中的run_query首先根据项目标识符计算 daemon socket 路径,然后把查询文本原样发给正在运行的服务器;如果是help则直接打印帮助文本。当使用了--no-daemon之类的参数时,则会走no_daemon_query.execute_query的独立逻辑。如果连接失败,客户端会提示需要先运行pyre建立服务器。
第二层:服务端请求解析。服务器收到查询字符串后,由 source/server/query.ml 的parse_request调用parse_request_exn,把"伪 Python"字符串解析为Request.t变体(例如"attributes(a.C)"→Request.Attributes (Reference.t))。这个解析器支持字符串、布尔、整数参数,以及命名参数(如model_query(path=..., query_name=...)、validate_taint_models(..., verify_dsl=True)),并且对参数个数不匹配的情况会抛出InvalidQuery异常。
第三层:服务端请求处理。解析出的Request.t被交给process_request(parse_and_process_request是其便捷包装,见 source/server/query.mli),在类型环境、构建系统、全局模块路径 API 和查询缓存的配合下计算出Response.t,最后序列化为 JSON 返回给客户端。例如GlobalLeaks请求会调用GlobalLeakCheck.check_qualifier逐个 qualifier 检查;ModelQuery请求会走process_model_query(其中涉及Taint.ModelParser解析模型源码、FetchCallables抓取可调用对象、ClassHierarchyGraph构建类层级图等完整 Pysa 预处理管线)。
小结
pyre query是理解 Pyre 内部世界的一扇窗口:attributes/defines/superclasses/dump_class_hierarchy勾勒出类体系视图,type/types/less_or_equal给出类型推断与子类型判定,callees/callees_with_location展示调用关系,global_leaks承担全局可变性审计,而model_query/validate_taint_models/save_server_state则直通 Pysa 污点分析与服务器状态管理。在享受这些能力的同时,请务必牢记官方的 legacy 声明:它非常适合调试与人工排查,但不应成为自动化系统或产品的基础依赖。
- 静态分析
- 开发工具
- 代码质量
【免费下载链接】pyre-check
Performant type-checking for python.
相关推荐
显存不足也能跑FLUX?2025轻量级模型选型与部署全指南
显存不足也能跑FLUX?2025轻量级模型选型与部署全指南 你是否遇到过这样的困境:明明看好FLUX模型的强大生成能力,却因显存不足(低于24GB)无法流畅运行
基础模型计算机视觉Arthas `sm` 命令完全指南:Search-Method 查看已加载类的方法信息
Arthas sm 命令完全指南:Search Method 查看已加载类的方法信息 sm (Search Method)是 Arthas 中最常用的类级诊断命
开发工具可观测性调试器性能剖析Tabby终极指南:5步搭建企业级AI编程助手
Tabby终极指南:5步搭建企业级AI编程助手 Tabby是一个开源的自托管AI编程助手,为开发者提供完全免费的GitHub Copilot替代方案。这款强大的
人工智能大模型本地部署模型推理服务后端RAG交互助手
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考