Ruff/ty 字符串类型注解(String Annotations)全解析:前向引用、延迟求值与部分字符串化的完整行为规范
导读
本篇文章以 Ruff 仓库中 ty 类型检查器的字符串注解规格测试文档(crates/ty_python_semantic/resources/mdtest/annotations/string.md)为核心,系统讲解 Python 字符串形式类型注解在静态类型检查中的全部行为:从简单的前向引用、嵌套解包、部分字符串化(partially stringified)注解的运行时风险检测,到各种字符串字面量种类(raw 字符串、f-string、字节串、隐式拼接、转义字符)的合法性判定。读完本文,你将掌握 ty 在解析 "int"、"'int'"、int | "Foo" 这类注解时的完整判定规则、对应诊断码的含义,以及这些规则背后的源码实现路径。
该文档是 ty 的 mdtest 规格测试(snapshot 测试)体系的一部分,每个代码片段中的 # error: [...]、# revealed: ...、# snapshot 都是可验证的断言标记。本文在完整继承这些规格的基础上,结合其底层实现 crates/ty_python_semantic/src/types/string_annotation.rs 与对应的 lint 文档,对每一类行为给出源码级的解释。
一、字符串注解在 Python 类型系统中的作用
Python 允许在类型注解位置使用字符串字面量,称为字符串注解(string annotation / forward reference)。它存在的核心意义是解决前向引用问题:当注解引用的名字(类、别名)在注解出现时尚未定义时,将其写成字符串,延迟到名字定义之后再求值。例如:
def f(v: "Foo"): ...
class Foo: ...
ty 的类型检查器(位于 crates/ty_python_semantic)需要解析并求值这些字符串内容。解析入口是 parse_string_annotation 函数,它定义在 crates/ty_python_semantic/src/types/string_annotation.rs。该函数接收类型推断上下文、推断标志和字符串字面量 AST 节点,返回一个已解析的模块表达式(Parsed<ModExpression>)。
从源码看,parse_string_annotation 的执行流程依次是:
- 检查字符串前缀:如果字符串带
r前缀(raw string),报告raw-string-type-annotation诊断; - 比较原始内容与解析内容:将源码中的原始字符串内容(去掉引号)与解析后的字符串内容比较,若一致则调用
parsed_string_annotation尝试把内容解析为 Python 表达式;解析失败则报告invalid-syntax-in-forward-annotation; - 检测转义字符:若两者不一致(说明字符串中含转义序列,如
\x69、\N{...}),报告escape-character-in-forward-annotation; - 检测隐式拼接:若非单一部分字符串(即隐式拼接字符串
"in" "t"),报告implicit-concatenated-string-type-annotation。
这四个诊断码正好对应 crates/ty_python_semantic/resources/lint_docs 目录下的四个 lint 文档。我们会在后续章节逐一展开。
二、基础行为:简单、嵌套、类型表达式与部分字符串化
2.1 简单字符串注解(Simple)
最基础的形式:注解内容是单个名字,解析后类型立即确定。
def f(v: "int"):
reveal_type(v) # revealed: int
这里 reveal_type 是测试/调试用内建函数,# revealed: int 是断言:参数 v 的推断类型就是 int。
2.2 嵌套字符串注解(Nested)
字符串注解的内容可以是另一个被引号包裹的字符串,ty 会递归解包一层:
def f(v: "'int'"):
reveal_type(v) # revealed: int
2.3 完整类型表达式(Type expression)
字符串内容可以是任意合法的类型表达式,包括联合类型和泛型:
def f1(v: "int | str", w: "tuple[int, str]"):
reveal_type(v) # revealed: int | str
reveal_type(w) # revealed: tuple[int, str]
2.4 部分字符串化(Partial)
字符串注解可以只覆盖类型表达式的一部分,其余部分是真实表达式:
def f(v: tuple[int, "str"]):
reveal_type(v) # revealed: tuple[int, str]
这里 tuple[...] 是真实语法,内部的 "str" 是字符串注解,最终整体推断为 tuple[int, str]。
2.5 延迟求值(Deferred)
字符串注解的典型价值是延迟求值:Foo 在函数定义时尚未存在,直到类定义之后才解析。
def f(v: "Foo"):
reveal_type(v) # revealed: Foo
def f(x: "int | 'Foo'"): ...
class Foo: ...
f("not an int or a Foo") # error: [invalid-argument-type]
f(Foo()) # fine
f(42) # fine
注意 "int | 'Foo'" 内部还包含一层嵌套字符串 'Foo',这是第 2.2 节嵌套规则的组合用法。调用阶段会基于最终解析出的类型 int | Foo 做参数类型检查。
2.6 延迟求值但名字未定义(Deferred undefined)
如果字符串注解引用的名字始终没有定义,ty 报告 unresolved-reference,且该注解的类型被推断为 Unknown:
# error: [unresolved-reference]
def f(v: "Foo"):
reveal_type(v) # revealed: Unknown
Unknown 是 ty 在无法确定类型时的兜底类型,后续对该值的操作不会产生级联误报。
三、部分字符串化注解(Partially deferred annotations)与运行时风险
3.1 Python 3.14 之前的运行时 TypeError 检测
这是本规格文档中篇幅最大、也最贴近真实运行行为的章节。核心背景是:部分字符串化的 PEP-604 联合类型(如 int | "Foo")在 Python 3.14 之前的运行时求值可能抛出 TypeError。ty 会尽力检测这类常见的运行时错误。
例如,int | "Foo" 在运行时先对 int 调用 __or__,而 int 的 __or__ 并不接受字符串参数,于是抛错。以下规格(配置 python-version = "3.13")完整演示了 ty 的分类判断:
[environment]
python-version = "3.13"
from typing import Any, TypeVar, Callable, Protocol, TypedDict, TYPE_CHECKING
class TD(TypedDict): ...
class P(Protocol):
x: int
class Meta(type):
def __or__(cls, other: str) -> Any:
return "wow, so fancy, bet type checkers can't handle this"
class UsesMeta(metaclass=Meta): ...
T = TypeVar("T")
# fmt: off
def f(
# error: [unsupported-operator]
a: int | "Foo",
# error: [unsupported-operator]
b: int | "memoryview" | bytes,
# error: [unsupported-operator]
c: "TD" | None,
# error: [unsupported-operator]
d: "P" | None,
# fine: `TypeVar.__or__` accepts strings at runtime
e: T | "Foo",
# fine: _SpecialForm.__ror__` accepts strings at runtime
f: "Foo" | Callable[..., None],
# also fine due to the custom metaclass
g: UsesMeta | "Foo",
# error: [unsupported-operator]
h: None | None,
# error: [unresolved-reference] "SomethingUndefined"
# error: [unresolved-reference] "SomethingAlsoUndefined"
i: SomethingUndefined | SomethingAlsoUndefined,
# error: [unsupported-operator]
# error: [unsupported-operator]
j: list["int" | None] | "bytes",
):
reveal_type(a) # revealed: int | Foo
reveal_type(b) # revealed: int | memoryview[int] | bytes
reveal_type(c) # revealed: TD | None
reveal_type(d) # revealed: P | None
reveal_type(e) # revealed: T@f | Foo
reveal_type(f) # revealed: Foo | ((...) -> None)
reveal_type(g) # revealed: UsesMeta | Foo
reveal_type(h) # revealed: None
reveal_type(i) # revealed: Unknown
# fmt: on
class Foo: ...
# error: [unsupported-operator]
X = list["int" | None]
if TYPE_CHECKING:
bar: "int" | "None"
def foo(x: "int" | "None"): ...
class Bar:
# no error because this annotation is resolved inside a scope
# fully defined inside an `if TYPE_CHECKING` block
def f(x: "int" | "None"): ...
这份规格中的关键判定逻辑可以归纳为一张对照表:
| 表达式形式 | 运行时行为 | ty 的诊断 |
|---|---|---|
| `int | "Foo"` | int.__or__ 不接受字符串,抛 TypeError |
| `int | "memoryview" | bytes` |
| `"TD" | None`(TypedDict) | TypedDict 元类不提供字符串版 __or__ |
| `"P" | None`(Protocol) | _ProtocolMeta 无字符串版 __or__ |
| `T | "Foo"`(TypeVar) | TypeVar.__or__ 运行时接受字符串 |
| `"Foo" | Callable[..., None]` | _SpecialForm.__ror__ 运行时接受字符串 |
| `UsesMeta | "Foo"`(自定义元类) | 自定义元类的 __or__ 接受字符串 |
| `SomethingUndefined | SomethingAlsoUndefined` | 名字未定义 |
| `list["int" | None]` | 列表字面量在注解位置本身非法 |
还需要注意两点细节:
bar: "int" | "None"、def foo(x: "int" | "None")这类注解放在if TYPE_CHECKING:块内,在 Python 3.14 之前的运行时根本不会执行(TYPE_CHECKING为False时该块不执行),因此不存在运行时错误,ty 不报告诊断;块内类方法上的同类注解同理。X = list["int" | None]是模块级变量注解,运行时实际求值,因此无论是否字符串化都报告unsupported-operator。
从源码角度,unsupported-operator 这类诊断由联合类型的推断逻辑(位于 crates/ty_python_semantic/src/types 下的 union/operator 相关实现)负责,它会结合当前 python-version 配置(见上述 [environment] 块)和两侧操作数的元类/特殊形式特征决定是否报错。
3.2 Protocol 元类(Protocol metaclasses)
源码协议的默认 _ProtocolMeta 元类不提供接受字符串的 __or__ 方法。因此在 Python 3.14 之前,含协议类及其子类的部分字符串化联合在运行时会失败:
[environment]
python-version = "3.13"
from typing import Protocol
class P(Protocol): ...
class Child(P): ...
def f(
# error: [unsupported-operator]
x: P | "P",
# error: [unsupported-operator]
y: "Child" | Child,
): ...
而如果自定义元类在 __or__ 中接受字符串,并且从 type(Protocol) 派生(以兼容协议运行时的元类),则联合合法:
from typing import Any
class Meta(type(Protocol)):
def __or__(cls, other: str) -> Any:
return other
class Custom(P, metaclass=Meta): ...
def g(x: Custom | "Custom"): ...
这个对比展示了 ty 是如何结合类型元类层面的运行时语义来预测错误的:Meta 继承自 type(Protocol) 意味着 Custom 的元类是协议元类的子类,因此可以安全接受字符串拼接。
3.3 在 stub 文件中永不报告
stub 文件(.pyi)不会被 Python 运行时执行,因此这类错误永远不会在 stub 文件上发出:
[environment]
python-version = "3.13"
# fine
def f(x: "int" | None): ...
3.4 使用 __future__ annotations 规避
在 Python 3.14 之前,如果代码顶部使用了 from __future__ import annotations,则所有注解都被当作字符串存储、不立即求值,因此类型注解上下文中的部分字符串化联合不再触发运行时错误:
[environment]
python-version = "3.13"
from __future__ import annotations
def f(v: int | "Foo"): # fine
reveal_type(v) # revealed: int | Foo
class Foo:
def __init__(self):
self.x: "int" | "str" = 42
d = {}
# error: [invalid-type-form]
d[0]: "int" | "str" = 42
# error: [unsupported-operator]
X = list["int" | None]
注意几个仍然报错的场景:
d[0]: "int" | "str" = 42是下标赋值(subscript assignment)上下文,__future__注解不会将其字符串化,因此报告invalid-type-form;X = list["int" | None]是模块级变量,注解会立即执行,报告unsupported-operator。
3.5 Python >= 3.14 的行为
Python 3.14 对联合类型的字符串处理做了改进(PEP 604 相关运行时调整),部分字符串化注解的运行时错误大幅减少:
[environment]
python-version = "3.14"
def f(v: int | "Foo"): # fine
reveal_type(v) # revealed: int | Foo
class Foo: ...
# error: [unsupported-operator]
X = list["int" | None]
即便在 3.14 下,X = list["int" | None] 这种模块级变量注解中的字符串化联合依然非法(因为该表达式会被求值),因此仍报告 unsupported-operator。这再次印证:ty 的版本相关诊断严格依赖配置中的 python-version 环境项,读者在真实项目中应确保该配置与运行环境一致。
四、typing.Literal 与字符串注解
字符串注解在 Literal 中语义特殊:Literal["Foo", "Bar"] 中的字符串是字面量值而不是前向引用。ty 能正确处理带引号与不带引号的 Literal 写法:
from typing import Literal
def f1(v: Literal["Foo", "Bar"], w: 'Literal["Foo", "Bar"]'):
reveal_type(v) # revealed: Literal["Foo", "Bar"]
reveal_type(w) # revealed: Literal["Foo", "Bar"]
class Foo: ...
这里的 w 整体是一个字符串注解,内容又是一个 Literal[...] 类型表达式,内部字符串是字面量元素。ty 通过"先解析字符串内容、再按类型表达式求值"的两阶段流程正确处理这种嵌套。
五、各种字符串字面量种类的合法性
5.1 参数注解位置
字符串注解的解析对字面量的前缀与结构有严格要求。以下规格逐项覆盖 raw 字符串、f-string、字节串、隐式拼接、转义字符:
def f1(
# error: [raw-string-type-annotation] "Raw string literals are not allowed in parameter annotations"
a: r"int",
# error: [raw-string-type-annotation] "Raw string literals are not allowed in parameter annotations"
b: list[r"int"],
# error: [invalid-type-form] "F-strings are not allowed in parameter annotations"
c: f"int",
# error: [invalid-type-form] "F-strings are not allowed in parameter annotations"
d: list[f"int"],
# error: [invalid-type-form] "Bytes literals are not allowed in this context in a parameter annotation"
e: b"int",
f: "int",
# error: [implicit-concatenated-string-type-annotation] "Type expressions cannot span multiple string literals"
g: "in" "t",
# error: [implicit-concatenated-string-type-annotation] "Type expressions cannot span multiple string literals"
h: list["in" "t"],
# error: [escape-character-in-forward-annotation] "Escape characters are not allowed in parameter annotations"
i: "\N{LATIN SMALL LETTER I}nt",
# error: [escape-character-in-forward-annotation] "Escape characters are not allowed in parameter annotations"
j: "\x69nt",
k: """int""",
# error: [invalid-type-form] "Bytes literals are not allowed in this context in a parameter annotation"
l: "b'int'",
# error: [invalid-type-form] "Bytes literals are not allowed in this context in a parameter annotation"
m: list[b"int"],
): # fmt:skip
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: list[Unknown]
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: list[Unknown]
reveal_type(e) # revealed: Unknown
reveal_type(f) # revealed: int
reveal_type(g) # revealed: Unknown
reveal_type(h) # revealed: list[Unknown]
reveal_type(i) # revealed: Unknown
reveal_type(j) # revealed: Unknown
reveal_type(k) # revealed: int
reveal_type(l) # revealed: Unknown
reveal_type(m) # revealed: list[Unknown]
把该规格与 string_annotation.rs 的实现对应起来:
| 字面量 | 触发诊断码 | 判定依据(源码逻辑) |
|---|---|---|
r"int" |
raw-string-type-annotation |
string_literal.flags.prefix().is_raw() 为真 |
f"int" |
invalid-type-form |
f-string 无法作为类型表达式求值 |
b"int" |
invalid-type-form |
字节串不允许出现在该上下文中 |
"int" |
无诊断 | 正常解析 |
"in" "t" |
implicit-concatenated-string-type-annotation |
非单部分字符串(as_single_part_string() 返回 None) |
"\N{...}nt" / "\x69nt" |
escape-character-in-forward-annotation |
源码原始内容与解析后内容不一致,说明含转义序列 |
"""int""" |
无诊断 | 三引号是普通单部分字符串 |
"b'int'" |
invalid-type-form |
内容解析出的表达式是字节串字面量 |
list[b"int"] |
invalid-type-form |
泛型参数中出现字节串 |
被拒绝的注解类型统一回退为 Unknown,list[r"int"] 这类嵌套形式回退为 list[Unknown],从而保证类型检查不因单点错误而崩溃。
5.2 在 typing.Literal 中
Literal 的元素是字面量值,因此字符串种类的要求与注解上下文不同——raw 字符串、字节串、隐式拼接、转义字符在 Literal 中都是合法的字面量值,ty 会统一规整为规范的字符串值:
from typing import Literal
def f(v: Literal["a", r"b", b"c", "d" "e", "\N{LATIN SMALL LETTER F}", "\x67", """h"""]): # fmt:skip
reveal_type(v) # revealed: Literal["a", "b", "de", "f", "g", "h", b"c"]
注意两点:
"d" "e"隐式拼接后成为单个字面量"de";\N{LATIN SMALL LETTER F}和\x67转义后分别成为"f"、"g";- 字节串
b"c"作为Literal元素保留其字节串身份(显示为b"c")。
5.3 相关 lint 文档的官方表述
- raw-string-type-annotation.md:检测注解位置的 raw 字符串,原因是 ty 等静态分析工具无法分析 raw 字符串记号;例如
def test() -> r"int"应改为def test() -> "int"。 - implicit-concatenated-string-type-annotation.md:检测注解位置的隐式拼接字符串,例如
"Literal[" "5" "]"应改为"Literal[5]"。 - escape-character-in-forward-annotation.md:检测含转义字符的前向注解,例如
"intt\b"。 - invalid-syntax-in-forward-annotation.md:检测字符串内容无法作为 Python 表达式解析的注解。typing 规范要求字符串注解内容必须是可解析的合法 Python 表达式,例如
"instance of C"不是表达式,应改为"C"。
这四个 lint 在 string_annotation.rs 中均以 declare_lint! 宏声明,状态为 stable("0.0.1-alpha.1"),默认级别为 Error,对应诊断码分别为 raw-string-type-annotation、implicit-concatenated-string-type-annotation、invalid-syntax-in-forward-annotation、escape-character-in-forward-annotation。
六、类变量与注解赋值
6.1 类变量(Class variables)
类作用域内,字符串注解会在完整类体上下文中解析,因此能看到类体内后续定义的别名,且与真实表达式的求值结果一致:
MyType = int
class Aliases:
MyType = str
forward: "MyType" = "value"
not_forward: MyType = "value"
reveal_type(Aliases.forward) # revealed: str
reveal_type(Aliases.not_forward) # revealed: str
关键结论:"MyType" 字符串注解与真实表达式 MyType 解析到同一个名字(类体内的 str),因为类体内的名字遮蔽了模块级的 int。
6.2 注解赋值(Annotated assignment)
字符串注解同样适用于模块级与函数内的赋值语句,包括多重嵌套与后续赋值更新类型:
a: "int" = 1
b: "'int'" = 1
# snapshot
c: """'"int"'""" = 1
d: "Foo"
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to `Foo`"
e: "Foo" = 1
# snapshot
f: "'str | int | bool | Foo | Bar'" = 1
class Foo: ...
d = Foo()
reveal_type(a) # revealed: Literal[1]
reveal_type(b) # revealed: Literal[1]
reveal_type(c) # revealed: Literal[1]
reveal_type(d) # revealed: Foo
reveal_type(e) # revealed: Foo
reveal_type(f) # revealed: Literal[1]
其中 # snapshot 标记的两处会产生快照诊断。第一个快照对应 c: """'"int"'""" = 1,第三个嵌套层过深:
error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation
--> src/mdtest_snippet.py:4:8
|
4 | c: """'"int"'""" = 1
| ^^^^^ Too many levels of nested string annotations; remove the redundant nested quotes
第二个快照对应 f: "'str | int | bool | Foo | Bar'" = 1,嵌套字符串过长:
error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation
--> src/mdtest_snippet.py:9:5
|
9 | f: "'str | int | bool | Foo | Bar'" = 1
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Nested string annotation is too long; remove the redundant nested quotes
这印证了 2.2 节嵌套解包规则的边界:ty 只接受一层嵌套字符串注解,过深或过长的冗余嵌套会触发 invalid-syntax-in-forward-annotation。另外 e: "Foo" = 1 中 1 与 Foo 不匹配,报告 invalid-assignment;而 d = Foo() 的后续赋值将 d 的类型更新为 Foo,且不影响最初注解约束的其他变量。
七、无效表达式与错误恢复(Invalid expressions)
7.1 非法的注解表达式不应导致崩溃
字符串注解的内容并非总能解析成合法的类型表达式。ty 的规格明确要求:这类表达式虽在上下文中非法,但检查器不应 panic。以下回归测试覆盖了大量非法形式(部分来自 ty 历史 issue 的回归场景,如 stringified_fstring_with_conditional 系列):
# Regression test for https://github.com/astral-sh/ty/issues/1865
# error: [invalid-type-form]
stringified_fstring_with_conditional: "f'{1 if 1 else 1}'"
# error: [invalid-type-form]
stringified_fstring_with_boolean_expression: "f'{1 or 2}'"
# error: [invalid-type-form]
stringified_fstring_with_generator_expression: "f'{(i for i in range(5))}'"
# error: [invalid-type-form]
stringified_fstring_with_list_comprehension: "f'{[i for i in range(5)]}'"
# error: [invalid-type-form]
stringified_fstring_with_dict_comprehension: "f'{ {i: i for i in range(5)} }'"
# error: [invalid-type-form]
stringified_fstring_with_set_comprehension: "f'{ {i for i in range(5)} }'"
# error: [invalid-type-form]
a: "1 or 2"
# error: [invalid-type-form]
b: "(x := 1)"
# error: [invalid-type-form]
c: "1 + 2"
# Regression test for https://github.com/astral-sh/ty/issues/1847
# error: [invalid-type-form]
c2: "a*(i for i in [])"
# error: [invalid-type-form]
d: "lambda x: x"
# error: [invalid-type-form]
e: "x if True else y"
# error: [invalid-type-form]
f: "{'a': 1, 'b': 2}"
# error: [invalid-type-form]
g: "{1, 2}"
# error: [invalid-type-form]
h: "[i for i in range(5)]"
# error: [invalid-type-form]
i: "{i for i in range(5)}"
# error: [invalid-type-form]
j: "{i: i for i in range(5)}"
# error: [invalid-type-form]
k: "(i for i in range(5))"
# error: [invalid-type-form]
l: "await 1"
# snapshot
m: "yield 1"
# snapshot
n: "yield from 1"
# error: [invalid-type-form]
o: "1 < 2"
# error: [invalid-type-form]
p: "call()"
# error: [invalid-type-form] "List literals are not allowed"
r: "[1, 2]"
# error: [invalid-type-form] "Tuple literals are not allowed"
s: "(1, 2)"
# snapshot
t: "list[yield from 1]"
# snapshot
u: "type]"
这些非类型表达式统一报告 invalid-type-form,推断结果为 Unknown。特殊地,"yield 1"、"yield from 1"、"list[yield from 1]"、"type]" 四例走快照路径,因为它们触发的不是普通的"类型形式非法",而是字符串内容语法解析失败,报告码为 invalid-syntax-in-forward-annotation,并且 ty 会给出可执行的修复建议。
7.2 语法错误快照与自动修复
以 m: "yield 1" 为例,快照诊断如下:
error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation
--> src/mdtest_snippet.py:43:5
|
43 | m: "yield 1"
| ^^^^^^^ Yield expression cannot be used here
help: Did you mean `typing.Literal["yield 1"]`?
help: Wrap in `Literal[...]`
|
2 | # error: [invalid-type-form]
3 + from typing import Literal
4 | stringified_fstring_with_conditional: "f'{1 if 1 else 1}'"
--------------------------------------------------------------------------------
43 | # snapshot
- m: "yield 1"
44 + m: Literal["yield 1"]
45 | # snapshot
|
note: This is an unsafe fix and may change runtime behavior
"list[yield from 1]" 与 "type]" 的快照与之类似。该修复逻辑在 string_annotation.rs 中实现:当解析错误类型不是 StringAnnotationError 且字符串不含换行时,ty 提示 Did you mean typing.Literal[...]?,并调用 autofix_with_literal 生成"用 Literal[...] 包裹"的修复(标记为不安全修复,因为会改变运行时行为)。"type]" 则是因为 ] 出现在表达式末尾属于"表达式末尾出现意外 token"的语法错误。
7.3 相关错误恢复策略
规格文档 annotations/invalid.md 对字符串注解中的无效表达式有更细致的错误恢复策略,值得一并了解:
- 避免级联诊断:
x: [[int]]只报告外层列表字面量非法,忽略内层同样非法的列表; - 无效节点内的运行时错误仍报告:
x: [[foo]]中foo未定义属于"比类型规格洁癖更严重"的错误,仍会报告unresolved-reference; - 字符串注解中的未解析引用不报告:
x: "[[foo]]"只报告invalid-type-form,因为字符串注解从不运行时执行,foo未定义的信息只是噪音; - 非法的下标操作数不求值:
a: "(lambda value=missing: None)[int]"不会产生级联的unresolved-reference,赋值表达式(name := int)也不会导致 panic。
八、多行字符串注解(Multi-line annotation)
多行字符串注解应按被括号包围的方式解析。合法的写法是:内容整体像一个表达式,括号不匹配即报语法错误。
合法的多行注解(内部换行与缩进被忽略,等价于 (int | str)):
def valid(
a1: """(
int |
str
)
""",
a2: """
int |
str
""",
):
reveal_type(a1) # revealed: int | str
reveal_type(a2) # revealed: int | str
非法的多行注解(括号不匹配/多余括号),各自触发 invalid-syntax-in-forward-annotation 快照:
def invalid(
# snapshot
a1: """
int |
str)
""",
# snapshot
a2: """
int) |
str
""",
# snapshot
a3: """
(int)) """,
):
pass
error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation
--> src/mdtest_snippet.py:19:4
|
17 | a1: """
| ____________-
18 | | int |
19 | | str)
| |____^-
| |
| Unexpected token at the end of an expression
a1 中 str) 后面缺少闭合括号,a2 中 int) | 同理,a3 中 (int)) 多了一个右括号——三例都报告"表达式末尾出现意外 token"。注意 string_annotation.rs 中语法错误修复提示对含换行的字符串注解会被跳过(!string_literal.contains('\n') 条件不满足),因此多行注解不会给出 Literal[...] 修复建议。
九、注解解析的底层调用链与扩展阅读
9.1 源码调用路径
字符串注解的完整解析链路为:
- 类型推断过程中遇到字符串形式的注解 AST 节点(
ExprStringLiteral); - 调用 string_annotation.rs 的
parse_string_annotation; - 其中依次处理 raw 前缀、内容一致性(转义检测)、调用
ruff_db::parsed::parsed_string_annotation(实现在 crates/ruff_db/src/parsed.rs)完成内容解析、以及隐式拼接检测; - 解析成功返回
Parsed<ModExpression>供类型求值;失败则生成对应 lint 诊断并返回None(类型回退为Unknown)。
该函数同时被 crates/ty_python_semantic/src/semantic_model.rs 调用,用于语义模型中解析字符串注解。
9.2 相关测试与文档
- 规格测试主文档:crates/ty_python_semantic/resources/mdtest/annotations/string.md
- 无效类型表达式与错误恢复:crates/ty_python_semantic/resources/mdtest/annotations/invalid.md
- 相关 lint 文档:raw-string-type-annotation.md、implicit-concatenated-string-type-annotation.md、escape-character-in-forward-annotation.md、invalid-syntax-in-forward-annotation.md
9.3 实践建议汇总
- 前向引用写字符串,但不滥用嵌套:
"Foo"合法,"'Foo'"也合法(ty 解包一层),但三层嵌套或过长的冗余嵌套会报invalid-syntax-in-forward-annotation; - 避免 raw 字符串与隐式拼接:
r"int"、"in" "t"在注解位置一律报错,改用普通字符串; - 注意转义字符:
"\x69nt"这类含转义的字符串会触发escape-character-in-forward-annotation,因为解析器无法可靠还原其字面内容; - 部分字符串化联合与 Python 版本强相关:
int | "Foo"在 Python <3.14 且无__future__时报告unsupported-operator;在 stub 文件、TYPE_CHECKING块、__future__注解或 Python >=3.14 下均合法。配置[environment] python-version应与项目实际运行环境一致,否则会产生与运行时行为不一致的诊断; - 非法表达式不 panic,但会污染类型:字符串内容若含
yield、赋值表达式、函数调用等非法类型表达式,报告invalid-type-form且类型回退Unknown;其中纯语法错误(如"yield 1")会得到Literal[...]包裹的(不安全)自动修复建议。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust4.21 K637- DDeepSeek-V4.1-FlashDeepSeek-V4.1-Flash 是一个多模态混合专家(MoE)模型,拥有 5520 亿骨干参数,并支持最多一百万 token 的上下文长度。该模型原生支持图像和文本输入,并以自回归方式生成文本Python270
cherry-studio🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端TypeScript2 K146
hello-agents📚 《从零开始构建智能体》——从零开始的智能体原理与实践教程Python46066
new-apiAI模型聚合管理中转分发系统,一个应用管理您的所有AI模型,支持将多种大模型转为统一格式调用,支持OpenAI、Claude、Gemini等格式,可供个人或者企业内部管理与分发渠道使用。🍥 A Unified AI Model Management & Distribution System. Aggregate all your LLMs into one app and access them via an OpenAI-compatible API, with native support for Claude (Messages) and Gemini formats.Go20143
JeecgBoot🔥企业级低代码平台集成了AI应用平台,帮助企业快速实现低代码开发和构建AI应用!前后端分离架构 SpringBoot,SpringCloud、Mybatis,Ant Design4、 Vue3.0、TS+vite!强大的代码生成器让前后端代码一键生成,无需写任何代码! 引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE,显著的提高效率,又不失灵活~Java34051