ty 类型检查器中的 `LiteralString`:完整语义、用法约束与类型推断实现解析
发布时间:2026/9/11 12:11:17来源:尧图网络
ty 类型检查器中的LiteralString完整语义、用法约束与类型推断实现解析【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruffLiteralString是 Python 类型系统PEP 675中表示源码内直接写出的字符串字面量的特殊类型用于构建高安全性的字符串插值 API。本文以 ruff 仓库中 ty 类型检查器的官方测试规范文档 literal_string.md 为核心骨架结合 types.rs 与 special_form.rs 等源码实现系统讲解LiteralString在 ty 中的用法限制、诊断修复auto-fix、类型推断规则与窄化行为帮助读者掌握这一类型在静态检查中的完整行为模型并理解 mdtest 文档即测试的验证方式。背景什么是LiteralStringLiteralString表示一个要么直接在源码中定义、要么由这类组件拼接而成的字符串。换句话说它标记的是信任边界内的字符串——不是从外部输入用户输入、文件读取、网络请求、环境变量来的不可信字符串。在 types.rs 中ty 通过Type::literal_string()构造该类型源码注释点明了它的一个关键性质/// Create a LiteralString. fn literal_string() - Self { // Note that LiteralStrings are never implicitly inferred, and so are always unpromotable. Self::LiteralValue(LiteralValueType::unpromotable( LiteralValueTypeKind::LiteralString, )) }也就是说LiteralString永远不会被隐式推断出来例如一个普通字符串字面量的表达式类型是Literal[abc]而非LiteralString它只会作为显式注解出现并且在内部被建模为一种不可提升unpromotable的字面值类型。这与下文字面量可赋值给LiteralString、但反之不能收窄到具体字面量的规则直接呼应。在符号层面special_form.rs 将其登记为特殊形式Special Form/// The symbol typing.LiteralString (which can also be found as typing_extensions.LiteralString) LiteralString,注意ty 同时把typing.LiteralString与typing_extensions.LiteralString视为同一个特殊形式并通过type_form_argument见 special_form.rs将其解析为Type::literal_string()。这意味着在类型检查层面两者完全等价。本文档来自crates/ty_python_semantic/resources/mdtest/annotations/目录属于 ty 的 Markdown 测试套件mdtestMarkdown 文件本身就是对类型推断与类型检查行为的测试由tests/mdtest.rs集成测试执行见 resources/README.md。文档中的reveal_type(...)输出# revealed: ...# error: [code]与# snapshot: invalid-type-form后跟的snapshot代码块都是期望的诊断输出是测试断言的一部分。开发时可用 mdtest.py 以python mdtest.py annotations/literal_string.md的方式只运行该文件的用例。使用位置LiteralString在哪里合法LiteralString可以出现在任何接受类型的位置包括函数参数、返回值、变量注解等from typing_extensions import LiteralString def _(x: LiteralString): reveal_type(x) # revealed: LiteralString一个值得注意的细节在本测试文件的所有示例中即使只使用了typing_extensions也无需显式开启[environment] python-version配置即可解析LiteralString。这是因为typing_extensions.LiteralString在较老版本解释器上同样可用typing.LiteralString则需要 Python 3.11见下文专门小节。用法限制与诊断修复不能出现在Literal内部LiteralString表示一整类字符串而Literal[...]要求参数是具体的值二者语义冲突因此 ty 直接报错invalid-type-formfrom typing_extensions import Literal, LiteralString bad_union: Literal[hello, LiteralString] # error: [invalid-type-form] bad_nesting: Literal[LiteralString] # error: [invalid-type-form]从实现上看这与 types.rs 中is_literal_or_union_of_literals的判断一致——该方法枚举了可以作为Literal参数的字面值种类String、Bytes、Int、Bool、Enum唯独对LiteralValueTypeKind::LiteralString返回false从类型构造层面就杜绝了LiteralString混入Literal参数的可能。不能被参数化LiteralString[...]报错并提供Literal修复LiteralString不接受类型参数。ty 会报告invalid-type-form并且——当参数是合法的字面量值时——主动建议用Literal替换必要时自动补上typing导入from typing_extensions import LiteralString # snapshot: invalid-type-form a: LiteralString[str]期望诊断输出error[invalid-type-form]: LiteralString expects no type parameter -- src/mdtest_snippet.py:4:4 | 4 | a: LiteralString[str] | ^^^^^^^^^^^^^^^^^^注意这里str本身并不是一个合法的Literal参数Literal[str]非法所以没有提供Literal修复。而当参数是字符串字面量时# snapshot: invalid-type-form b: LiteralString[foo]error[invalid-type-form]: LiteralString expects no type parameter -- src/mdtest_snippet.py:6:4 | 6 | b: LiteralString[foo] | -------------^^^^^^^ | | | Did you mean Literal? help: Replace LiteralString with Literal | 1 from typing import Literal 2 | from typing_extensions import LiteralString -------------------------------------------------------------------------------- 6 | # snapshot: invalid-type-form - b: LiteralString[foo] 7 b: Literal[foo] | note: This is an unsafe fix and may change runtime behavior这个自动修复是不安全修复unsafe fixLiteralString与Literal[foo]的运行时语义不同前者在运行时就是typing_extensions.LiteralString这个对象后者是typing.Literal[foo]类型检查器只是从检查通过的角度给出等价替换因此明确标注may change runtime behavior。字面值变量不是合法的Literal参数一个变量可以拥有字面量类型例如name value被推断为Literal[value]但它本身并不是一个合法的Literal参数Literal[name]非法。此时 ty 仍然报告invalid-type-form但不会建议Literal因为替换后同样无法通过检查from typing_extensions import LiteralString name value # snapshot: invalid-type-form value: LiteralString[name]error[invalid-type-form]: LiteralString expects no type parameter -- src/mdtest_snippet.py:6:8 | 6 | value: LiteralString[name] | ^^^^^^^^^^^^^^^^^^^修复参数化的模块成员保留括号、注释与参数当LiteralString通过模块别名访问并被参数化时例如(t.LiteralString)[...]ty 的修复会替换完整的属性访问表达式同时保留外围括号、注释和所有参数。该用例要求 Python 3.11 环境[environment] python-version 3.11import typing as t # snapshot: invalid-type-form value: (t.LiteralString)[ first, # A supported value. bsecond, True, None, 42, ]error[invalid-type-form]: LiteralString expects no type parameter -- src/mdtest_snippet.py:4:8 | 4 | value: (t.LiteralString)[ | ^--------------- Did you mean Literal? | ________| | | 5 | | first, # A supported value. 6 | | bsecond, 7 | | True, 8 | | None, 9 | | 42, 10 | | ] | |_^ help: Replace LiteralString with Literal | 3 | # snapshot: invalid-type-form - value: (t.LiteralString)[ 4 value: (t.Literal)[ 5 | first, # A supported value. | note: This is an unsafe fix and may change runtime behavior从诊断渲染可以看出错误的主标注覆盖了(t.LiteralString)[到]的完整区间修复只将t.LiteralString替换为t.Literal参数列表first、bsecond、True、None、42以及注释# A supported value.原样保留。这验证了文档开头声明的修复策略The fix replaces the complete attribute access, preserving the surrounding parentheses, comments, and arguments.参数化的字符串注解错误恢复LiteralString的参数在字符串注解deferred annotation延迟求值中会被检查但由于字符串注解不查找语义索引中缺失的赋值表达式例如海象运算符(name : missing)中的name绑定缺失的名字会保留各自的诊断unresolved-reference、not-subscriptable而不是被吞掉或产生级联崩溃。runtime.pyfrom typing_extensions import LiteralString # error: [invalid-type-form] # error: [unresolved-reference] Name missing used when not defined a: LiteralString[(name : missing)] # error: [invalid-type-form] # error: [unresolved-reference] Name missing used when not defined # error: [not-subscriptable] b: LiteralString[int[(name : missing)]] c: LiteralString[(name : 0).real] # error: [invalid-type-form] # error: [invalid-type-form] # error: [unresolved-reference] Name missing used when not defined d: LiteralString[lambda defaultmissing: None] # error: [invalid-type-form] # error: [unresolved-reference] Name missing_direct used when not defined direct: LiteralString[missing_direct] # error: [invalid-type-form] # error: [unresolved-reference] Name missing_nested used when not defined # error: [not-subscriptable] nested: LiteralString[int[missing_nested]] # error: [invalid-type-form] # error: [unresolved-reference] Name missing_call used when not defined # error: [unresolved-reference] Name missing_argument used when not defined call: LiteralString[missing_call(missing_argument)] # error: [invalid-type-form] # error: [unresolved-reference] Name missing_binary used when not defined binary: LiteralString[missing_binary 1] # error: [invalid-type-form] # error: [unresolved-reference] Name missing_list used when not defined collection: LiteralString[[missing_list]]每一条都同时报告invalid-type-form因为LiteralString不可参数化与参数内部真实存在的名字解析错误两者互不干扰。同样的错误恢复在 stub 文件.pyi中同样成立stub.pyifrom typing_extensions import LiteralString # error: [invalid-type-form] # error: [unresolved-reference] Name missing used when not defined value: LiteralString[(name : missing)] # error: [invalid-type-form] # error: [unresolved-reference] Name missing_direct used when not defined direct: LiteralString[missing_direct]参数化的已求值注解当注解处于求值evaluated模式——例如不写成字符串、直接写LiteralString[...]——参数内部同样报告错误。该用例要求 Python 3.13 环境[environment] python-version 3.13from typing_extensions import LiteralString # error: [invalid-type-form] # error: [unresolved-reference] Name missing used when not defined value: LiteralString[(name : missing)]作为基类运行时错误LiteralString是typing_extensions提供的对象不是可以继承的类。在类型注解位置直接子类化它ty 报告invalid-basefrom typing_extensions import LiteralString class C(LiteralString): ... # error: [invalid-base]这是因为LiteralString与Literal等特殊形式一样是检查期类型运行时对class C(LiteralString)的求值本身就会抛TypeError。字符串注解中的字面量建议字符串注解中参数化的LiteralString依然适用建议Literal的规则且判断依据是参数能否作为合法的Literal参数。先看字面量别名的情况——Alias Literal[value]本身是合法的Literal参数所以给出修复from typing_extensions import Literal, LiteralString Alias Literal[value] # snapshot: invalid-type-form alias: LiteralString[Alias]error[invalid-type-form]: LiteralString expects no type parameter -- src/mdtest_snippet.py:6:9 | 6 | alias: LiteralString[Alias] | -------------^^^^^^^ | | | Did you mean Literal? help: Replace LiteralString with Literal | 1 import typing 2 | from typing_extensions import Literal, LiteralString -------------------------------------------------------------------------------- 6 | # snapshot: invalid-type-form - alias: LiteralString[Alias] 7 alias: typing.Literal[Alias] 8 | MultipleValues Literal[a, b] | note: This is an unsafe fix and may change runtime behavior注意这里修复采用typing.Literal全限定名由于示例中import typing语句由修复本身插入且Literal已从typing_extensions导入为避免名称冲突而选择了typing.Literal前缀形式。包含多个字面值的别名同样合法MultipleValues Literal[a, b] # snapshot: invalid-type-form multiple_values: LiteralString[MultipleValues]error[invalid-type-form]: LiteralString expects no type parameter -- src/mdtest_snippet.py:10:19 | 10 | multiple_values: LiteralString[MultipleValues] | -------------^^^^^^^^^^^^^^^^ | | | Did you mean Literal? help: Replace LiteralString with Literal | 1 import typing 2 | from typing_extensions import Literal, LiteralString -------------------------------------------------------------------------------- 10 | # snapshot: invalid-type-form - multiple_values: LiteralString[MultipleValues] 11 multiple_values: typing.Literal[MultipleValues] 12 | value value | note: This is an unsafe fix and may change runtime behavior而普通变量不是合法的Literal参数——即使它的值是字符串value value被推断为Literal[value]也不会给出Literal建议value value # snapshot: invalid-type-form variable: LiteralString[value]error[invalid-type-form]: LiteralString expects no type parameter -- src/mdtest_snippet.py:14:12 | 14 | variable: LiteralString[value] | ^^^^^^^^^^^^^^^^^^^^综合上面几个例子可以总结出 ty 的修复判定策略当参数是可作为Literal参数的类型表达式字符串/字节/整数/布尔字面量、Literal别名、以及它们的联合时提供修复否则仅报告错误。这一判定与 types.rs 中is_literal_or_union_of_literals对字面值种类的枚举一一对应。类型推断常见字符串操作的结果ty 对LiteralString的推断遵循参与操作的双方都是字面量字符串或LiteralString时结果才是LiteralString一旦混入普通str就退化为str的规则。以下用例全部来自Inference Common operations一节from typing_extensions import LiteralString def _(literal_a: LiteralString, literal_b: LiteralString, a_str: str): # Addition reveal_type(literal_a literal_b) # revealed: LiteralString reveal_type(literal_a a_str) # revealed: str reveal_type(a_str literal_a) # revealed: str # In-place addition combined_literal literal_a combined_literal literal_b reveal_type(combined_literal) # revealed: LiteralString combined_non_literal1 literal_a combined_non_literal1 a_str reveal_type(combined_non_literal1) # revealed: str combined_non_literal2 a_str combined_non_literal2 literal_a reveal_type(combined_non_literal2) # revealed: str # Join reveal_type(literal_a.join((abc, foo, literal_a, literal_b))) # revealed: LiteralString reveal_type(a_str.join((abc, foo, literal_a, literal_b))) # revealed: str reveal_type(literal_a.join((abc, foo, a_str))) # revealed: str # .format(…) reveal_type({}, {}.format(literal_a, literal_b)) # revealed: LiteralString reveal_type({}, {}.format(literal_a, a_str)) # revealed: str # f-string reveal_type(f{literal_a} {literal_b}) # revealed: LiteralString reveal_type(f{literal_a} {a_str}) # revealed: str if literal_a ! foo: reveal_type(literal_a) # revealed: LiteralString ~Literal[foo] # the handling for LiteralString works even for subtypes of LiteralString, # such as LiteralString ~Literal[foo], not just LiteralString itself reveal_type(f{literal_a} {literal_b}) # revealed: LiteralString # Repetition reveal_type(literal_a * 10) # revealed: LiteralString逐条解读加法LiteralString LiteralString保持LiteralString只要一侧是普通str结果即为str。就地加法规则与一致且注意combined_non_literal2 a_str; combined_non_literal2 literal_a的结果是str——即使参与运算的是str LiteralString结果也退化。str.joinLiteralString.join(可迭代)的结果取决于可迭代元素的类型——元素全部是LiteralString/字面量时结果是LiteralString分隔符是str或元素中出现str结果都是str。str.format所有位置参数都是LiteralString时结果保持LiteralString混入str即退化。f-string插值项全部是LiteralString时保持LiteralString否则退化为str。窄化后的子类型if literal_a ! foo:分支内literal_a被窄化为LiteralString ~Literal[foo]LiteralString与不等于 foo的否定交集。f-string 的推断对LiteralString的子类型同样生效——f{literal_a} {literal_b}在该分支内仍revealed: LiteralString。这验证了文档的注释the handling forLiteralStringworks even for subtypes ofLiteralString。重复*literal_a * 10保持LiteralString重复操作不引入外部数据仍是字面量派生值。这些规则保证了LiteralString的闭包性对字面量字符串做的纯字符串运算不会意外丢失其可信来源标签这正是一系列安全 API如sqlite3的?占位、日志格式串所依赖的语义。可赋值性AssignabilityLiteralString在类型层级上处于具体字面量与str之间形成单向兼容链Literal[abc]以及Literal[]、多值Literal[abc, def]可赋值给LiteralStringLiteralString可赋值给str反向均不成立LiteralString不能赋值给任何具体的Literal[...]str也不能赋值给LiteralString。from typing_extensions import Literal, LiteralString from ty_extensions import static_assert from ty_extensions._internal import is_assignable_to static_assert(is_assignable_to(Literal[], LiteralString)) static_assert(is_assignable_to(Literal[abc], LiteralString)) static_assert(is_assignable_to(Literal[abc, def], LiteralString)) static_assert(not is_assignable_to(LiteralString, Literal[])) static_assert(not is_assignable_to(LiteralString, Literal[abc])) static_assert(not is_assignable_to(LiteralString, Literal[abc, def])) static_assert(is_assignable_to(LiteralString, str)) static_assert(not is_assignable_to(str, LiteralString))这里使用了ty_extensions._internal.is_assignable_to与static_assertty 内部的测试断言工具在编译期静态验证可赋值关系。这套规则也解释了为何 subscript.rs 中 TypedDict 的下标键检查会以可赋值给LiteralString但不包括LiteralString本身作为仅允许字符串字面量键的近似判据。窄化NarrowingLiteralString变量在与具体字符串字面量做相等比较时可以窄化为对应的Literal[...]类型比较结束后恢复为LiteralStringfrom typing_extensions import LiteralString lorem: LiteralString lorem * 1_000_000_000 reveal_type(lorem) # revealed: LiteralString if lorem ipsum: reveal_type(lorem) # revealed: Literal[ipsum] reveal_type(lorem) # revealed: LiteralString if lorem ipsum: reveal_type(lorem) # revealed: Literal[ipsum]几个要点示例先通过lorem * 1_000_000_000构造了一个超长重复字符串——它虽然是字面量运算结果但注解为LiteralString不会展开成亿级长度的字面量类型。if lorem ipsum:分支内窄化为Literal[ipsum]可以在分支里对该字符串做精确模式匹配。链式比较 lorem ipsum同样能触发窄化比较链的条件成立意味着lorem ipsum成立。分支结束后lorem恢复为LiteralString说明窄化只在条件成立的控制流路径内生效。typing.LiteralString与版本前提typing_extensions.LiteralString在较早版本即可使用而标准库的typing.LiteralString仅在 Python 3.11 及以后可用。测试环境必须显式声明python-version 3.11[environment] python-version 3.11from typing import LiteralString def _(x: LiteralString): reveal_type(x) # revealed: LiteralString这一版本门槛与 PEP 675LiteralString的引入 PEP的时间线一致。需要说明的是示例中的[environment]配置是 mdtest 测试文件的元数据用于指定测试代码运行的解释器版本在实际工程中并不需要这样的配置节——项目只需确保目标 Python 版本 ≥ 3.11 即可直接from typing import LiteralString。在 mdtest 中验证与运行本文档本质上是一份可执行测试规范。其验证机制是Markdown 中的每个 Python 代码块被提取为一个测试用例src/mdtest_snippet.py诊断信息中的文件路径即由此而来# snapshot: name标记的用例会与紧随其后的snapshot代码块中的期望输出逐字比对# error: [code]与# revealed: ...是内联断言直接校验诊断码与推断类型。运行单个文件的测试cargo test --package ty_python_semantic --test mdtest -- mdtest::annotations/literal_string.md或者使用 mdtest.py 开发运行器该脚本还支持--no-snapshot-updates等选项并会监视.rs、.md、vendored typeshed 变化自动重跑。若快照过时可在 snapshots 目录中查看更新后的.snap文件。小结LiteralString在 ty 类型检查器中是一个双重特殊的类型符号层面它同时映射typing与typing_extensions两个来源值层面它是一个不可提升、永不隐式推断的字面值类型。本文档规范的行为可归纳为三组核心规则使用约束可用在任何类型位置但不得进入Literal、不得被参数化参数合法时提供替换为Literal的 unsafe fix、不得作为基类推断闭包加法、join、.format、f-string、重复等纯字符串运算在全部操作数为字面量时保持LiteralString混入str即退化赋值与窄化Literal[...] ⊆ LiteralString ⊆ str的单向链以及相等比较将LiteralString窄化为Literal[...]的控制流敏感行为。这套规则为在类型层面标记可信字符串、从而让 SQL 拼接、日志格式串、命令执行等场景获得静态安全保障提供了精确且可验证的检查语义。如需进一步了解LiteralString与Literal的关系可对照阅读同目录下的 literal.md 与 string.md 测试文档。【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网站建设高端定制企业官网