news 2026/9/9 23:47:07

ty 类型检查器中的语义级语法错误诊断:invalid-syntax 规则的完整解析与源码实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ty 类型检查器中的语义级语法错误诊断:invalid-syntax 规则的完整解析与源码实现

ty 类型检查器中的语义级语法错误诊断:invalid-syntax 规则的完整解析与源码实现

【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff

本文基于 ruff 仓库中 ty 类型检查器的 mdtest 测试文档 semantic_syntax_errors.md,系统讲解一类容易被忽视的诊断:Python 的“语义级语法错误”。这些语句能被语法解析器完整解析,但违反语言规范,需要在理解作用域、上下文和 Python 版本之后才能判定。读完后你将掌握:每一类invalid-syntax诊断的触发条件与适用版本、ty 如何区分它们与常规类型错误,以及底层检查器 SemanticSyntaxChecker 的核心工作机制。

什么是“语义级”语法错误

Python 语法错误通常发生在词法/语法解析阶段,但有一大类错误在 AST 层面是完全合法的,只有结合语义上下文才能判定为SyntaxError

  • 该名称在当前作用域中是否已被声明为参数、全局或 nonlocal;
  • 该语句是否位于 async 函数、循环、类体、推导式等特定上下文中;
  • 项目配置的目标 Python 版本是否允许该写法(例如 3.11 才开放的嵌套 async 推导式)。

ruff 仓库把这类检查实现为ruff_python_parsercrate 中的通用检查器:crates/ruff_python_parser/src/semantic_errors.rs 定义了SemanticSyntaxChecker结构体,通过visit_stmt/visit_expr遍历 AST,并依赖SemanticSyntaxContexttrait 向宿主程序询问上下文信息。ty 类型检查器与 ruff linter 分别实现了该 trait(见 crates/ty_python_core/src/builder.rs 与 crates/ruff_linter/src/checkers/ast/mod.rs),因此同一套检查逻辑可以同时服务两个工具。

ty 对这类错误的诊断 code 统一为invalid-syntax。它与invalid-type-form(类型表达式形式错误)、unresolved-reference语义类型错误相互独立:一段代码可能同时触发多个诊断,后文示例中可以看到它们并排出现。

检查器依赖的上下文:SemanticSyntaxContext trait

从源码结构看,判定“语义语法错误”的关键在于宿主提供上下文。semantic_errors.rs 中的SemanticSyntaxContexttrait 声明了检查器需要回答的问题:

trait 方法作用
future_annotations_or_stub()当前是否启用了__future__注解或处于 stub 文件
python_version()目标 Python 版本,用于版本相关的兼容性检查
global(name)名称在当前作用域声明为global的位置
has_nonlocal_binding(name)外层作用域是否存在同名绑定
in_async_context()当前是否位于 async 函数内
in_sync_comprehension()当前是否位于同步推导式内
in_await_allowed_context()await当前是否合法(lambda、任意函数内均允许,嵌套类体内不允许)
in_yield_allowed_context()yield/yield from当前是否合法(仅函数与 lambda 体)
lazy_import_context()lazyimport 所处的最近限制上下文(函数/类/try)

例如 trait 文档明确解释了in_await_allowed_contextin_async_context的区别:await在 lambda 中允许(尽管 lambda 不是 async 的),在任意函数内也允许,只有嵌套类定义会使该检查失败。这正是后文“awaitoutside of an asynchronous function”一节中(await cor async for cor in f()) # ok这类边界写法成立的底层原因。

版本相关:async 推导式嵌套在同步推导式中

这是文档中篇幅最大的一类,因为它的合法性随目标版本变化:Python 3.11 之前,async推导式不能出现在外层同步推导式内部,即使整体位于 async 函数中(对应 CPython issue 77527)。

[environment] python-version = "3.10"环境下:

async def elements(n): yield n async def f(): # snapshot: invalid-syntax return {n: [x async for x in elements(n)] for n in range(3)}

ty 输出的诊断为:

error[invalid-syntax]: cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11) --> src/mdtest_snippet.py:6:19 | 6 | return {n: [x async for x in elements(n)] for n in range(3)} | ^^^^^^^^^^^^^^^^^^^^^^^^^^

注意错误范围精确覆盖内层async for子句,而非整行。

关键区分点:如果所有层级的推导式都是 async 的,即使在 3.10 上代码依然合法

async def test(): # snapshot: not-iterable return [[x async for x in elements(n)] async for n in range(3)]

此处仅剩一个类型诊断:

error[not-iterable]: Object of type `range` is not async-iterable --> src/mdtest_snippet.py:9:59 info: It has no `__aiter__` method

文档还覆盖了嵌套作用域的正确性:[x for x in [1]] and [x async for x in elements(1)]这类“and 表达式中混入 async 推导式”、以及 async 函数内再定义嵌套同步函数后使用 async 推导式的情况,都用于验证SemanticSyntaxContext的嵌套作用域接线是否正确(async 上下文以最近的函数作用域为准)。

切换到[environment] python-version = "3.11"后,上面 3.10 的非法写法全部变为合法,不再产生任何诊断。

源码印证:semantic_errors.rs 中的async_comprehension_in_sync_comprehension方法开头即有版本闸门:

fn async_comprehension_in_sync_comprehension<Ctx: SemanticSyntaxContext>( ctx: &Ctx, generators: &[ast::Comprehension], ) { let python_version = ctx.python_version(); if python_version >= PythonVersion::PY311 { return; } // async allowed at notebook top-level if ctx.in_notebook() && ctx.in_module_scope() { return; } if !ctx.in_sync_comprehension() { return; } // ... 对每个 generator.is_async 的生成子句添加诊断

可以看到三个豁免分支:版本 ≥ 3.11 直接返回;notebook 顶层(顶层代码被视为异步执行上下文)允许;不在同步推导式内则跳过。

__future__导入的两类错误

位置过晚的__future__导入

from __future__必须位于文件顶部,任何其他语句之后使用即报诊断:

from collections import namedtuple # error: [invalid-syntax] "__future__ imports must be at the top of the file" from __future__ import print_function

延迟(lazy)导入不是__future__导入

在 Python 3.15 环境下,lazy from __future__ import annotations中的__future__只是模块名,不构成真正的 future 导入,因此会触发两条不同的诊断:

# [environment] python-version = "3.15" # error: [invalid-syntax] "lazy from __future__ import is not allowed" lazy from __future__ import annotations # error: [invalid-syntax] "__future__ imports must be at the top of the file" from __future__ import generator_stop

第一行因 lazy 导入被禁止而报错;第二行则因位置在文件中部而报错。检查器中对应的check_lazy_import_context方法通过lazy_import_context()询问宿主,返回最近的限制上下文(函数、类、try/except三者优先级递减)。

非法注解:walrus 表达式不能作为类型

在 Python 3.12 环境下,返回注解(y := 3)会同时触发两个诊断——这体现了 ty 中“形式层”与“语义层”错误并行报告的设计:

from __future__ import annotations # error: [invalid-type-form] "Named expressions are not allowed in return type annotations" # error: [invalid-syntax] "named expression cannot be used within a type annotation" def f() -> (y := 3): ...

invalid-type-form说明该表达式形式不属于类型语言;invalid-syntax说明该表达式形式上就是不允许出现在注解位置的。文档注释坦言这条与invalid-type-form略有冗余,但两者信息维度不同,均予保留。

match语句相关的三类诊断

映射模式中重复键

Python 3.10 环境下,match的映射模式不允许重复键:

match 2: # error: [invalid-syntax] "mapping pattern checks duplicate key `"x"`" case {"x": 1, "x": 2}: pass

类模式中重复属性名

类模式(class pattern)的属性名必须唯一:

class Point: pass obj = Point() match obj: # error: [invalid-syntax] "attribute name `x` repeated in class pattern" case Point(x=1, x=2): pass

不可反驳模式必须放在最后

通配符_与变量捕获模式是“不可反驳”的(任何值都能匹配),若其后仍有case,后续分支将不可达:

value = 5 match value: # error: [invalid-syntax] "wildcard makes remaining patterns unreachable" case _: # Irrefutable wildcard pattern pass case 5: pass match value: # error: [invalid-syntax] "name capture `variable` makes remaining patterns unreachable" case variable: # Irrefutable capture pattern pass case 10: pass

对应检查器方法为 semantic_errors.rs 中的irrefutable_match_case,它会扫描StmtMatch中所有 case,一旦遇到 wildcard 或裸 name 捕获,就对后续 case 报告不可达诊断。

重复关键字参数:区分“语法错误”与“调用错误”

Python 3.12 环境下,同一调用的两个重复关键字属于语法错误

def f(x: int) -> None: ... # error: [invalid-syntax] "Duplicate keyword argument `x`" f(x=1, x=2)

而位置参数 + 关键字参数重复属于调用期错误,ty 用不同的诊断 code 报告:

# error: [parameter-already-assigned] "Multiple values provided for parameter `x` of function `f`" f(1, x=2)

类定义中同样适用该规则,包括带类型参数的泛型类:

# error: [invalid-syntax] "Duplicate keyword argument `metaclass`" class C(metaclass=type, metaclass=type): ... # error: [invalid-syntax] "Duplicate keyword argument `metaclass`" class GenericT: ...

实现上由 semantic_errors.rs 的duplicate_keyword_args方法遍历Arguments节点完成。

函数外的return/yield/yield from/await

这四类语句/表达式出现在模块级或同步函数中时各有一条独立诊断:

class C: def __await__(self): ... # error: [invalid-syntax] "`return` statement outside of a function" return # error: [invalid-syntax] "`yield` statement outside of a function" yield # error: [invalid-syntax] "`yield from` statement outside of a function" yield from [] # error: [invalid-syntax] "`await` outside of an asynchronous function" await C() def f(): # error: [invalid-syntax] "`await` outside of an asynchronous function" await C() (await cor async for cor in f()) # ok (await cor for cor in f()) # ok ([await c for c in cor] async for cor in f()) # ok

其中最后三行# ok是关键的边界案例:生成器表达式是惰性求值的,其体部并不立即执行,因此模块级的生成器表达式里出现await是被允许的。源码中yield_outside_function(L1119)与await_outside_async_function(L1078)两个方法分别处理,且后者依赖上下文in_await_allowed_context()而非简单的 async 判断。

生成器惰性求值同理适用于 async 生成器表达式:

async def g(): yield 1 (x async for x in g()) # 合法,模块级即可

walrus 表达式与推导式变量:最复杂的一组规则

不得重新绑定推导式变量

:=不能重绑定已被推导式用作迭代器的变量。文档给出了完整规则集:

# error: [invalid-syntax] "assignment expression cannot rebind comprehension variable" [x := 2 for x in range(10)] # error: [invalid-syntax] "assignment expression cannot rebind comprehension variable" {y := 5 for y in range(10)} # error: [invalid-syntax] "assignment expression cannot rebind comprehension variable" [(a := 0) for a in range(3)] # error: [unresolved-reference] reveal_type(a) # revealed: Unknown # error: [invalid-syntax] "assignment expression cannot rebind comprehension variable" [i for i in range(5) if (i := 0)]

值得注意的细节:

  1. 变量泄漏行为[(a := 0) for a in range(3)]执行后a不可见(reveal_type(a)得到Unknown)。这是 CPython 的既有行为——推导式变量不泄漏到外层,而非法的 walrus 目标同样不生效。
  2. 外层目标在内层作用域依然活跃
# An active outer target remains active within a nested result. # error: [invalid-syntax] "assignment expression cannot rebind comprehension variable" [[(outer := 1) for _ in []] for outer in []] # An active outer target remains active within a nested filter. # error: [invalid-syntax] "assignment expression cannot rebind comprehension variable" [[a for a in [] if (outer := 1)] for outer in []]
  1. lambda 隔离:lambda 体自成一个作用域,其中的 walrus 目标属于 lambda 本身,因此[x for x in range(3) if (lambda: (x := 1))()]不报invalid-syntax(只报unresolved-reference);但嵌套推导式在 lambda 内时,外层的outer依然活跃,[outer for outer in range(3) if (lambda: [(outer := 1) for _ in [0]])()]仍会报错。
  2. 属性访问也不豁免[(x := 1).bit_length() for x in [0]]中 walrus 目标依然是x,照常报错。

walrus 在推导式中的非法位置

  • 类体内:推导式内定义的 walrus 变量不能逃逸到类体作用域。
class C: # error: [invalid-syntax] "assignment expression within a comprehension cannot be used in a class body" [(x := y) for y in range(3)] # error: [unresolved-reference] reveal_type(x) # revealed: Unknown

同样有 lambda 的例外:[(lambda: (local := 1))() for local in [0]]合法;但lambda 的默认参数在推导式作用域中求值,所以[(lambda value=(default := 1): value)() for item in [0]]依然报错。

  • 推导式的 iterable 表达式中:第一层for的迭代对象不能用 walrus 包裹:
def returns_list() -> list[int]: return [1, 2, 3] # error: [invalid-syntax] "assignment expression cannot be used in a comprehension iterable expression" [x for x in (y := returns_list())] # error: [invalid-syntax] "assignment expression cannot be used in a comprehension iterable expression" [x for x in (z := returns_list()).copy()]

子句顺序较靠后的for的 iterable 同样受限(escaped := [1]之后reveal_type(escaped)得到Unknown):

def invalid_later_iterable(): # error: [invalid-syntax] "assignment expression cannot be used in a comprehension iterable expression" [item for item in [0] for _ in (escaped := [1])] # error: [unresolved-reference] reveal_type(escaped) # revealed: Unknown
  • 上述限制可与其他错误叠加出现,例如[a for a in [(b := 1) for b in [1]]]同时报两条invalid-syntax(iterable 中的 walrus + 重绑定推导式变量)。

实现上,这些检查集中在 semantic_errors.rs 的check_generator_clausescheck_rebound_variables方法中,配合check_class_body_expr处理类体逃逸场景。

模式匹配中的重复名称绑定

同一模式内一个名称只能被赋值一次:

x = [1, 2] match x: # error: [invalid-syntax] "multiple assignments to name `a` in pattern" case [a, a]: pass case _: pass d = {"key": "value"} match d: # error: [invalid-syntax] "multiple assignments to name `b` in pattern" case {"key": b, "other": b}: pass

重复类型参数(PEP 695)

Python 3.12 环境下,泛型参数列表中的名称必须唯一:

# error: [invalid-syntax] "duplicate type parameter" class C[T, T]: pass # error: [invalid-syntax] "duplicate type parameter" def f[X, Y, X](): pass

对应实现为 semantic_errors.rs 的duplicate_type_parameter_name

Star 表达式的非法使用位置

*表达式不能用于:return/yield的表达式、for语句的目标或迭代对象:

def func(): # error: [invalid-syntax] "Starred expression cannot be used here" return *[1, 2, 3] def gen(): # error: [invalid-syntax] "Starred expression cannot be used here" yield *[1, 2, 3] # error: [invalid-syntax] "Starred expression cannot be used here" for *x in range(10): pass # error: [invalid-syntax] "Starred expression cannot be used here" for x in *range(10): pass

星号解包赋值必须处于列表或元组上下文

单独的*a = ...不合法,必须位于列表/元组目标中:

# error: [invalid-syntax] "starred assignment target must be in a list or tuple" *a = [1, 2, 3, 4]

禁止写入__debug__

__debug__是内建常量,三种赋值途径均被拦截(Python 3.12 环境):

# error: [invalid-syntax] "cannot assign to `__debug__`" __debug__ = False # error: [invalid-syntax] "cannot assign to `__debug__`" def process(__debug__): pass # error: [invalid-syntax] "cannot assign to `__debug__`" class Generic[__debug__]: pass

类型表达式中的非法表达式

Python 3.12 的type语句与 PEP 695 泛型把“类型表达式”扩展到了更多位置,因此yield与 walrus 表达式在这些位置会产生成对诊断:

def _(): # error: [invalid-type-form] "`yield` expressions are not allowed in type expressions" # error: [invalid-syntax] "yield expression cannot be used within a TypeVar bound" type X[T: (yield 1)] = int def _(): # error: [invalid-type-form] "`yield` expressions are not allowed in type alias values" # error: [invalid-syntax] "yield expression cannot be used within a type alias" type Y = (yield 1) # error: [invalid-type-form] "Named expressions are not allowed in return type annotations" # error: [invalid-syntax] "named expression cannot be used within a generic definition" def fT -> (y := 3): return x def _(): # error: [invalid-syntax] "yield expression cannot be used within a generic definition" class CT): pass

注意yield在基类位置(class CT))只报invalid-syntax而不报invalid-type-form,因为基类表达式本身是合法的表达式上下文,只是不允许yield

async 语句族在同步函数中的限制

awaitasync forasync with与 async 推导式四种形式在同步函数中各有一条独立诊断,便于精确定位具体写法:

async def elements(n): yield n def _(): # error: [invalid-syntax] "`await` outside of an asynchronous function" await elements(1) # error: [invalid-syntax] "`async for` outside of an asynchronous function" async for _ in elements(1): pass # error: [invalid-syntax] "`async with` outside of an asynchronous function" async with elements(1) as x: pass # error: [invalid-syntax] "asynchronous comprehension outside of an asynchronous function" [x async for x in elements(1)]

global声明的三种时序/冲突错误

先用后声明

名称在global声明之前被读取,诊断落在global语句上:

x: int def f(): x = 1 global x # error: [invalid-syntax] "name `x` is used prior to global declaration"

参数与全局变量同名

函数参数aglobal a不能同时存在,诊断消息为name \a` cannot refer to a parameter and a global variable`。文档覆盖了多个作用域变体:

a = None def f(a): global a # error: [invalid-syntax] def g(a): if True: global a # error: [invalid-syntax] def h(a): def inner(): global a # 合法:global 位于嵌套函数中,不冲突外层参数 def i(a): try: global a # error: [invalid-syntax] except Exception: pass def f(a): a = 1 global a # error: [invalid-syntax] def f(a): a = 1 a = 2 global a # error: [invalid-syntax] def f(a): class Inner: global a # 合法:类体是独立作用域

即“只要global与参数处于同一函数作用域内”就报错;嵌套函数或类体会切分作用域,从而合法。

参数与 nonlocal 变量同名

规则同构于 global 场景:nonlocal a与参数a在同一函数作用域内冲突时报告name \a` cannot refer to a parameter and a nonlocal variable。文档中的对照案例表明:inner中引用外层函数的参数(非局部赋值)合法;类体内的nonlocal因类体作用域隔离而合法;参数默认值(def f(a=1)`)不影响判定。

breakcontinue必须位于循环中

循环判定以最近的块作用域为准:嵌套函数和类体都会“隔断”外层循环:

break # error: [invalid-syntax] continue # error: [invalid-syntax] for x in range(42): break # fine continue # fine def _(): break # error: [invalid-syntax] continue # error: [invalid-syntax] class Fine: # this is invalid syntax despite it being in an eager-nested scope! break # error: [invalid-syntax] continue # error: [invalid-syntax]

快照输出中每条诊断的范围只覆盖关键字本身(^精确落在break/continue上),并分别给出error[invalid-syntax]: \break` outside loop`continue` outside loop` 两种消息。

测试组织方式与版本控制机制

上述所有用例都通过 ruff 仓库的mdtest 框架运行:# error: [code] "message"行内注释断言单条诊断;# snapshot: code注释则收集诊断与源码片段做快照断言。每个.md文件可内嵌[environment]TOML 块切换目标 Python 版本

[environment] python-version = "3.10"

这正是前文 3.10/3.11/3.12/3.15 分段用例的机制:检查器经SemanticSyntaxContext::python_version()拿到该值,版本相关的规则(如 async 嵌套推导式、PEP 695 类型参数、type语句)据此启用或静默。mdtest 文档目录中还有 version_related_syntax_errors.md 专门测试另一类版本闸门——语法本身在某版本才存在(如 3.10 之前的match)。

小结

ty 的invalid-syntax诊断覆盖了 CPython 在编译期(而非运行期)拒绝的语义级错误,其核心设计可归纳为三点:

  1. 通用检查器 + 上下文 traitSemanticSyntaxChecker(crates/ruff_python_parser/src/semantic_errors.rs)只负责遍历与规则,作用域/版本/async 状态全部经SemanticSyntaxContext由宿主(ty 或 ruff linter)注入,实现一份代码服务两个工具;
  2. 版本感知:规则按python_version()分级启用,嵌套 async 推导式(3.11 起合法)、PEP 695(3.12)等写法只在低于阈值的版本上报错;
  3. 与类型诊断并行invalid-syntaxinvalid-type-formparameter-already-assignedunresolved-reference各自独立,同一行代码可产出多条不同维度的诊断。

完整用例与期望快照可直接在 crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md 中查看,相关 mdtest 快照目录位于 crates/ty_python_semantic/resources/mdtest/snapshots/。

【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 23:46:46

Servlet+JSP图书管理系统开发:从分工设计到连接池、事务与JSP调试

简介&#xff1a;这是一套基于Servlet与JSP的图书管理系统完整项目源码&#xff0c;面向Java Web初学者及课程设计/毕业设计人群&#xff0c;通过实际案例展示图书信息增删改查等核心管理功能的实现方式。代码结构清晰&#xff0c;涵盖Servlet请求处理、JSP界面展示、JavaScrip…

作者头像 李华
网站建设 2026/9/9 23:46:34

Python操作剪映关键帧:JSON解析与批量自动化实战

简介&#xff1a;这是一份基于Python开发的剪映关键帧自动化工具桌面版源码&#xff0c;面向需要批量、高效处理视频关键帧的剪辑爱好者与开发者。工具可自动识别视频关键帧&#xff0c;并依据用户设定条件筛选最适合的剪辑点&#xff0c;也支持按需调节参数&#xff0c;实现个…

作者头像 李华
网站建设 2026/9/9 23:45:11

NDIS 6.0 Filter驱动实战:收发数据包与MAC地址查询实现

简介&#xff1a;一份基于Windows 10 x64平台的NDIS 6.0 Filter驱动示例&#xff0c;主要面向具有C/C和Windows驱动基础的开发者&#xff0c;演示在KMDF框架下实现网络数据包处理功能&#xff1a;支持发送OID请求&#xff0c;能构造并发送ICMP自定义数据包&#xff0c;也可实时…

作者头像 李华