Poietra/manim-lint

GitHub: Poietra/manim-lint

用 Rust 编写的 Manim 场景静态分析器,在渲染前检测正确性、渲染、性能和确定性等问题。

Stars: 0 | Forks: 0

# manim-lint **为 [Manim Community](https://www.manim.community/) 场景提供静态分析 —— 在渲染前捕获确定的运行时错误、静默的错误渲染、性能乘数以及非确定性。** English | [日本語](README.ja.md) `manim-lint` 是一个独立的静态分析器,适用于 Manim Community **0.20** 项目,使用 Rust 编写。它解析你的 Python 源代码,并将其与 精选的、带有版本控制的 Manim 语义模型进行检查 —— 它**从不导入或 执行** Manim 或你的代码。它不进行 API 名称的模式匹配,而是运行 一个生命周期抽象解释器来模拟 `Scene.play` 实际所做的 事情(参数编译、自动添加、引入器/移除器、更新器),以及一个 知道哪些代码只运行一次、哪些代码每帧运行的 符号成本模型。 ## 示例 `scenes/demo.py`: ``` from manim import * class TrackerDemo(Scene): def construct(self): title = Text("Tracking x", font_size=0) square = Square() tracker = ValueTracker(0) label = always_redraw(lambda: MathTex(f"x={tracker.get_value():.2f}")) self.add(title, square, label) self.play(square.shift(RIGHT)) square.add_updater(lambda m: m.rotate(0.05)) self.play(tracker.animate.set_value(8), run_time=8) self.wait(0) ``` ``` $ manim-lint check . --format concise scenes/demo.py:6:46: MLR115 error `Text(font_size=0)` is not positive; text sizing requires font_size > 0 scenes/demo.py:9:39: MLP226 warning Each invocation constructs a `MathTex` and performs a cache-key lookup, and this f-string key varies per frame: every rendered frame can mint a distinct Text/TeX cache key and disk asset (`K_resource ≈ F`). Across the 1 play(s) where this callback provably executes it may create at least ~480 distinct keys. scenes/demo.py:11:19: MLC102 error `square.shift(...)` mutates the mobject immediately and returns the mobject itself, not an Animation; use `.animate` (e.g. `square.animate.shift(...)`) inside `Scene.play()`. scenes/demo.py:12:38: MLD301 warning Updater lambda applies `rotate` with a fixed step every frame but declares no `dt` parameter; the motion speed depends on the profile frame rate scenes/demo.py:14:9: MLC112 warning This `wait()` renders a single frozen frame: nothing makes it dynamic, and the updater registered at line 9 reads frame-varying state without a `dt` parameter, so its visual change never renders during the wait. Pass `frozen_frame=False`, or declare a `dt` parameter on the updater. scenes/demo.py:14:19: MLC104 error Use a positive `duration`: the literal `0` is non-positive and playing it aborts the render. ``` 其中两个会导致渲染崩溃(`MLC102`,`MLC104`),一个会渲染出 不可见的标题(`MLR115`),一个会随着帧率静默改变速度 (`MLD301`),一个会冻结作者期望产生动画的等待 (`MLC112`),还有一个会在每个渲染帧启动外部 TeX 编译器以获取新的缓存 键(`MLP226`)—— 并且 linter 可以估算其 成本:在 60 FPS 下 `run_time=8` 证明至少会生成约 480 个不同的键。 ## 它检查什么 规则分为四大类: - **MLC — 生命周期/正确性。** 确定的运行时错误和生命周期 错误会导致渲染出错误的画面:传给 `Scene.play` 的非 Animation 参数(`MLC102`),在任何 路径上没有调用 `generate_target()` 的 `MoveToTarget`(`MLC107`),没有 `save_state()` 的 `Restore`(`MLC120`),在一次 play 中有两个 动画写入同一个 mobject 的同一个通道 (`MLC108`),被 `Scene.remove(child)` 撤销后又重新添加了存活的 父对象(`MLC115`)。 - **MLR — 渲染。** 代码可以渲染,但结果不是你想要的:Python 转义符破坏了非原生 `MathTex` 字面量中的 TeX 命令(`MLR103`), 导致 Manim 运行时精确搜索失败的资产路径 (`MLR104`),传递给纯 `Text` 的 Pango 标记 (`MLR124`),`Transform(mob, mob)`(`MLR113`)。 - **MLP — 性能。** 带有机器可读证据的成本乘数: 在 updater 或 `always_redraw` 中构造 `Text`/`MathTex`/`SVGMobject`(`MLP201`),每帧生成一个磁盘 资产的随帧变化的 TeX 缓存键 (`MLP226`),每帧不断增长的场景图 (`MLP204`),没有 `dissipating_time` 的 `TracedPath`(`MLP220`)。 - **MLD — 确定性/可移植性。** 在不同机器、 帧率或渲染器之间不同的渲染:没有 `dt` 缩放的 固定每帧步长(`MLD301`),帧回调中未设置种子的 全局随机性 (`MLD302`),在区分大小写的目标上的 仅大小写不同的资产路径不匹配(`MLD305`)。 ### 语义深度,而非名称匹配 分析器的核心原则(DESIGN §1):绝不单独对 API 名称发出警告。 `FadeOut(mob)` 用于从未添加过的 mobject 是没问题的 —— play 的准备工作 会自动添加它,随后移除器会将其删除。因此流水线会首先 构建真实的事实: - **生命周期抽象解释器**:函数内 CFG,过程间 助手摘要,带有 `super()` 分派的每个 Scene MRO 组合, 分配点身份,场景成员资格/顺序/updater 追踪, 以及 play 组语义; - **符号成本模型**:热上下文传播(updater, `always_redraw`,停止条件,插值覆盖)和仅从 字面持续时间推导出的帧计数区间 —— 上面的成本报告说 `duration 8 s -> frames ~480`,因为在 60 FPS 下 `run_time=8` 是可证明的, 否则打印 `unknown`,绝不是捏造的数字。 每个诊断都将**严重性**(`error`/`warning`/`info`)与 **置信度**(`certain`/`high`/`medium`/`low`)区分开来,并且依赖于状态的规则 只在确定的、全路径证据下才会触发。当一个值无法进行静态 解析时,它会降级为 `Unknown`,并且 linter 会保持**沉默,而不是 猜测** —— 这是一个刻意的设计立场,贯穿于每一条规则。 ## 安装说明 对于已发布的版本,请选择以下任何一个入口点。PyPI 包 安装的是原生的 Rust 可执行文件;它不导入 Manim,安装后也 不需要 Python 运行时。 ``` # Python tooling uv tool install manim-lint # or: pipx install manim-lint # Rust tooling (builds from source; Rust 1.85+) cargo install manim-lint --locked ``` 适用于 Linux、macOS 和 Windows 的独立安装程序和带有校验和的压缩包 附在每个 GitHub Release 中: ``` # macOS / Linux curl --proto '=https' --tlsv1.2 -LsSf \ https://github.com/Poietra/manim-lint/releases/latest/download/manim-lint-installer.sh | sh ``` 在首次注册表发布之前,或者要安装当前检出的代码,请从 源码构建: ``` git clone https://github.com/Poietra/manim-lint.git cd manim-lint cargo install --path . ``` 不需要安装 Python、Manim 或 LaTeX:分析器解析 源代码并参考带有版本控制的知识配置文件,从不导入或 执行 Manim 或其分析的代码。 ## 快速入门 ``` manim-lint check . # analyze; rich in a terminal, concise when piped manim-lint check . --format rich # force source frames and colour manim-lint check . --format concise # one line per diagnostic manim-lint check scenes --format full # explanations + evidence manim-lint check . --format json # schemas/diagnostics-v1.json manim-lint check . --format sarif # SARIF 2.1.0 manim-lint check . --format github # GitHub Actions annotations manim-lint explain MLC102 # full documentation for a rule manim-lint rules # every rule ID, phase, and status manim-lint config # resolved effective configuration manim-lint cost scenes/demo.py # per-scene cost breakdown manim-lint coverage . # what the analysis could not resolve manim-lint static-facts . > facts.json # StaticFacts v0 semantic projection manim-lint change-impact --before old --after new > impact.json manim-lint source-bridge . --request patch.json > candidates.json ``` 退出代码:`0` —— 没有报告的诊断达到 `fail-level`;`1` —— 至少 有一个达到;`2` —— 命令行、配置或内部错误。 ### 输出格式 如果不使用 `--format`,`check` 会根据其写入的位置选择输出。 附加到终端时,它会打印 `rich` 格式:每个发现对应一个横幅,带有 下划线范围的违规 源代码行、解释以及摘要。 ``` ✖ MLC104 scene.py:10:42 ─────────────────────────────────────────────── Use a positive `run_time`: the literal `0` is non-positive and playing it aborts the render. 8 │ group = AnimationGroup() 9 │ self.add(title, eq) > 10 │ self.play(Write(title), run_time=0) │ ^ 11 │ self.wait() ℹ Manim validates durations when a play executes, not when an animation is constructed … ✖ 2 errors ⚠ 1 warning in 1 file ``` 当重定向到文件或管道时,它会打印 `concise` 格式 —— 每个 诊断对应一行稳定的内容,没有转义序列 —— 这样脚本和 CI 就能保持 它们今天所解析的格式。传递 `--format` 可以在任一方向上覆盖此 选择。 颜色遵循 `--color auto|always|never`。`auto` 仅对终端使用样式, `NO_COLOR`(任何值)会禁用样式,而 `--color always` 即使 在重定向时也会使用样式。只有 `rich` 才会带有样式。`COLUMNS` 设置横幅 和换行使用的宽度。 有用的 `check` 选项:`--select` / `--ignore`,`--min-confidence`, `--fail-level`,`--profile`,`--renderer`,`--fps`, `--resolution WIDTHxHEIGHT`,`--color`,`--statistics`,`--analysis-summary`(即 下方的覆盖率报告,在诊断后打印到 stderr;stdout 和退出代码不受影响),以及下文描述的 baseline/fix 选项。 `--no-cache` 强制进行完整的分析,而不读取、 写入或创建缓存状态。`--select` 还会缩小分析本身:不需要的事实层 (生命周期解释器,符号成本 模型)会被跳过,因此缩小选择范围比完整运行更快。 无论哪种方式,报告的诊断都是相同的 —— 取代 所选规则的规则仍会运行,因此缩小选择范围绝不会恢复被取代的 诊断。 `--format full` 会在 每个诊断下打印解释和机器可读的证据: ``` scenes/demo.py:9:39: MLP226 warning Each invocation constructs a `MathTex` and performs a cache-key lookup, ... A frame-varying key defeats the `MathTex` cache: instead of one shaping/compile job reused every frame, each frame pays construction plus a cache miss, and for TeX classes each distinct key also launches the external TeX compiler and `dvisvgm`, leaving one disk asset per key. ... evidence.distinct_resource_keys: {"lower":480,"upper":null} evidence.execution: {"plays":[{"certainty":"proven","kind":"play","location":"scenes/demo.py:13:9"},{"certainty":"maybe","kind":"play","location":"scenes/demo.py:11:9"}],"unresolved_entries":false} evidence.frames: {"lower":480,"upper":null} evidence.invocation_context: "frame-callback" evidence.multiplicity: ["frames"] evidence.state_path: ["construct","always_redraw:9"] applies to profiles: production ``` ## 分析缓存 正常的 `check` 运行会在 `.manim-lint-cache/cache-v2.sqlite3` 处保留一个一次性的 SQLite 缓存。相同的第二次运行会验证 文件系统依赖项,并在不 启动前端的情况下重用整个项目的诊断 JSON。在编辑源代码后,缓存 v2 仍然会解析并索引 完整的项目,然后使用解析出的导入、调用、 基类和模块名 冲突将项目文件划分为弱依赖 组件。未更改的组件重用 JSON 方法摘要和过滤后的 诊断;只有更改后的组件重新运行摘要、Scene 生命周期 和成本分析。AST 和已分析的代码从不被序列化或执行。 键涵盖了分析器构建、解析出的语义配置、Manim 知识配置文件、完整的源代码布局以及相关的源代码字节。字面 资产候选者和区分大小写的目录遍历会按条目标记, 因此资产更改会使受影响的组件失效。SQLite WAL 数据库 支持并发冷写入,并保留 16 个最近使用的 项目快照以及 256 个组件快照。它从来不是正确性所必需的: 损坏会通过警告重建,其他失败会继续 进行分析。`--no-cache` 禁用所有缓存文件系统活动。`--fix`、 baselines 和 `--analysis-summary` 特意运行完整的分析, 因为它们在生成诊断后需要实时的源码或索引状态。 将 `.manim-lint-cache/` 添加到项目的忽略文件中。较旧的 `cache-v1.sqlite3` 文件不会被使用,可以删除。 冷运行使用有限的工作池并行化独立的摘要组件、Scene 生命周期运行 和规则。递归的摘要不动点和 前端/项目索引保持有序和顺序执行。输出会被收集并 稳定排序,测试证明在 1 个和 4 个工作进程下 JSON 字节完全相同。 ## 配置 配置存在于 `pyproject.toml` 的 `[tool.manim-lint]` 中,通过 从检查的路径向上遍历来找到。渲染配置文件是 `[[tool.manim-lint.profile]]` 条目: ``` [tool.manim-lint] manim-version = "0.20" target-python = "3.11" select = ["MLC", "MLR", "MLP", "MLD"] ignore = [] min-confidence = "high" fail-level = "warning" default-profile = "production" knowledge-profile = "upstream_0_20" respect-manim-cfg = true exclude = [".venv/**", "media/**"] per-file-ignores = { "tests/fixtures/**" = ["MLP", "MLD"] } [[tool.manim-lint.profile]] name = "production" renderer = "cairo" platform = "linux" pixel-width = 1920 pixel-height = 1080 frame-rate = 60 assets-dir = "." allowed-fonts = ["Noto Sans", "Noto Sans CJK JP"] ``` 优先级,从高到低: ``` CLI > selected profile > pyproject base > manim.cfg > builtin defaults ``` 当启用 `respect-manim-cfg`(默认值)时,`manim.cfg` 会 在 pyproject 设置下提供 resolution/fps/renderer 的默认值。未知的键、 未知的规则选择器、重复的配置文件名和未知的配置文件 引用都是配置错误(退出代码 2)。`--profile all` 会分析每个 定义的配置文件,并合并具有相同证据的诊断,列出每个诊断受影响的 配置文件。 配置得到了诚实的验证(违反时退出代码 2): - 声明的 `manim-version` 必须落在配置的 知识配置文件所支持的 Manim 范围内(例如 `upstream_0_20` 支持 `>=0.20,<0.21`);不存在时,不验证任何内容。 - `target-python` 必须是介于 3.6 和 3.12 之间的 `MAJOR.MINOR`。上限 是捆绑的解析器(rustpython-parser 0.4) 实现的 Python 语法;下限是 无法再保证语法控制的底线(更早的目标会被拒绝并退出代码 2,而 不是被静默地不予执行)。语法是固定的(没有 `feature_version` 锁定),因此解析本身永远不会改变;相反,对 AST、token 流和 f-string 文本的解析后控制会 将所有比目标更新的构造报告为 `MLC000`: `async def` 之外的 `async`/`await` 语法 (3.7),`:=`,仅限位置参数的 `/`,以及 f-string 自记录的 `=` (3.8),带有 `as` 的宽松装饰器和带括号的上下文管理器 (3.9),`match` (3.10),下标中的 `except*` 和 PEP 646 `*` 解包 (3.11),`type` 别名,PEP 695 类型参数和 PEP 701 f-string 表达式 (3.12)。保证控制静默通过的文件可以被 目标自身的解析器解析。受控制的文件仍会被完全分析,并且 引入此类语法的 `--fix` 将被回滚。有关 完整的覆盖范围表,请参见 `manim-lint explain MLC000`。 - 帧率为零、负数或非有限,以及分辨率的 维度为零,无论它们来自哪里(`--fps` / `--resolution`,配置文件,还是 `manim.cfg`),都会被拒绝。 - `stub-paths` 尚未实现;非空列表将被拒绝, 而不是被静默忽略。 `manim-lint config` 打印解析出的配置以及 `enforcement` 部分,说明哪些设置是强制执行的,哪些是 信息性的。 ## 使用优化的 fork 配置文件 使用本地修补过的 Manim fork 渲染的项目(配置文件 `local_0_20_1_4d25c031`)可以告诉 manim-lint 这样做,并解锁 特定于 fork 的分析层: ``` [tool.manim-lint] knowledge-profile = "local_0_20_1_4d25c031" default-profile = "production" [[tool.manim-lint.profile]] name = "production" renderer = "cairo" platform = "linux" cairo-fork-workers = 4 cairo-static-layers = true ``` 这在上游配置文件提供的所有功能之上,还启用了: - **`manim-lint cost` 中的“fork fast paths”部分**:每次 play,是否应用 fork-per-play Cairo 流水线(`cairo-fork-workers`),静态层 保留路径(`cairo-static-layers`),以及打包插值 —— 当它们没有应用时(例如由于 Scene updater),会给出确切的阻碍因素及其源代码范围,包括在第一次串行 play 之后渲染器范围的单调禁用链。该部分从不建议移除某个功能;它 解释渲染路径的后果。 - **`MLP214`**:标记在场景的第一次 play 之前串行构造的 四个或更多不同的 TeX 编译键,并引用 fork 的预编译 API(`MathTex.precompile`,`tex_to_svg_file_async`)。 - **`MLP217`**:标记在热回调中随帧变化的 `use_svg_cache=True` 键,这些键会导致 fork 声明的进程全局 SVG 缓存每帧 增长。 - **`MLP225`**(通过 `--select MLP225` 选择性启用):将成本报告的 fast-path 阻碍因素解释作为每次 play 的诊断输出。 在 `upstream_0_20` 下,以上所有内容都是无效的:成本报告不包含 fork 部分,并且这三个规则从不触发,即使被选中也是如此。 ## 抑制 ``` self.play(square.shift(RIGHT)) # manim-lint: ignore[MLC102] # same statement # manim-lint: ignore[MLP201] # next statement label = always_redraw(...) # manim-lint: file-ignore[MLP] # whole file; must appear in the file header ``` 抑制针对的是**整个语句**,而不是单行:行内尾部的 注释(或者紧接其上方的独立注释)涵盖了整个 语句,包括多行调用的续行,因此 固定在语句内任何位置的诊断都会被抑制。对于 复合语句(`def`,`for`,`if`,`with`,...),抑制 仅涵盖到其冒号为止的头部 —— 一个注释永远无法静默 整个代码套件。 内联抑制中未知的规则 ID **不会**抑制 任何内容;它将作为专门的警告被报告: ``` scene.py:8:41: MLC001 warning unknown rule ID in suppression: MLC999 ``` 对于整个目录,请使用 `pyproject.toml` 中的 `per-file-ignores`(参见 上文)。 ## 逐步采用:baselines 在现有项目上采用 linter,而无需先修复所有问题: ``` manim-lint check . --write-baseline .manim-lint-baseline.json # record today's findings manim-lint check . --baseline .manim-lint-baseline.json # report only new findings ``` Baseline 指纹(`schemas/baseline-v1.json`)**不包含行 号** —— 它们由规则 ID、相对路径、限定的场景名称 和周围的 token 哈希构建 —— 因此在文件的其他位置插入不相关的行不会 使条目失效。`scene` 字段记录了限定的 封闭 Scene 类(在任何场景之外为空),因此不同场景中的相同发现 会获得不同的指纹。写入的文件带有一个 `scene_attribution: "attributed"` 出处标记:它们为空的 `scene` 字面意思就是“在任何 Scene 之外”,并且完全匹配。在 场景归属之前写入的 Baselines(没有标记)仍然会被读取,只有在这些 Baselines 中 空的 `scene` 才会作为通配符匹配。损坏或 schema 错误的 baseline 文件会以清晰的消息退出,代码为 2。 ## 自动修复 ``` manim-lint check . --fix # apply SAFE fixes only manim-lint check . --fix --unsafe-fixes # also apply UNSAFE fixes ``` 安全和不安全的修复是严格分开的:单独使用 `--fix` 仅应用 保留行为的编辑(例如 `MLC127` 从一个 `add()`/`VGroup()` 调用中移除重复的子对象,`MLR104` 更正仅大小写不同的资产路径)。 不安全的修复可能会更改运行时语义(例如为了 `MLC102` 将 `play(mob.shift(...))` 重写为 `play(mob.animate.shift(...))`),并且 需要显式的额外标志。每个修复后的文件都会被重新解析以进行 验证;修复未通过重新解析的文件将被回滚。 ``` $ manim-lint check . --fix scene.py:8:40: MLC127 info Remove the duplicate `square` from this `VGroup(...)` call: Manim warns and ignores repeated children of a single add. fixed 1 issue(s) in 1 file(s) ``` ## 成本命令 `manim-lint cost` 打印每个场景的符号成本细分 —— 包含帧间隔的 play 列表,带有来源以及回调可证明执行的那些 play 的热上下文,每帧的构造,以及资源键 增长。未知持续时间打印为 unknown,从不是捏造的 数字: ``` $ manim-lint cost scenes/demo.py profiles: production (cairo, 1920x1080, 60 fps) scene scenes.demo.TrackerDemo (scenes/demo.py) plays: scenes/demo.py:11:9 play duration unknown -> frames per-frame scenes/demo.py:13:9 play duration 8 s -> frames ~480 scenes/demo.py:14:9 wait duration 0 s -> frames ~0 hot contexts: scenes/demo.py:9:31 entry always_redraw; path construct -> always_redraw:9; factors frames; proven execution plays: scenes/demo.py:13:9 scenes/demo.py:12:28 entry updater; path construct -> updater:12; factors frames; proven execution plays: scenes/demo.py:13:9 per-frame constructions: scenes/demo.py:9:39 MathTex construction x at least ~480 invocations across 1 proven play(s) resource-key growth: scenes/demo.py:9:39 MathTex distinct cache keys: at least ~480 across 1 proven play(s) (f-string key varies per frame) ``` 在本地 fork 知识配置文件下,报告会获得每个场景的 “fork fast paths”部分(见下文)。例如,在配置文件中有 `cairo-fork-workers = 4` 和 `cairo-static-layers = true` 时: ``` $ manim-lint cost scene.py ... fork fast paths (profile production, knowledge local_0_20_1_4d25c031): fork-per-play (cairo_fork_workers 4): scene.py:9:9 play #1: no static blocker found (fork-eligible pending the runtime audit) scene.py:10:9 play #2: no static blocker found (fork-eligible pending the runtime audit) static layers (cairo_static_layers on): scene.py:9:9 play #1: no static blocker found scene.py:10:9 play #2: no static blocker found packed interpolation: scene.py:9:9 play #1: canonical per-member interpolation because the animation type FadeIn is outside the audited allowlist at scene.py:9:19 (blocker unsupported_animation_type); an updater-bearing mobject is in the scene family (updater registered here) at scene.py:8:9 (blocker updater_bearing_family) scene.py:10:9 play #2: canonical per-member interpolation because an updater-bearing mobject is in the scene family (updater registered here) at scene.py:8:9 (blocker updater_bearing_family) evidence: measured packed interpolation on the calibration machine, 300 members / 60 frames: 130.658 -> 33.004 ms/play, steady state 2.0761 -> 0.1890 ms/frame (docs/research/perf-evidence.md) note: the features named above can be correct expression; this section explains the render-path consequence and never advises removing them ``` ## 分析覆盖率 分析器的保守静默是正确的但不可见:一次干净的 运行不会告诉你是不存在问题,还是项目的一半 无法分析。`manim-lint coverage`(以及 `manim-lint check --analysis-summary`,它会将相同的报告打印到 stderr,而不触及 stdout 或退出代码)展示了分析**无法**解析的所有 内容: 对于一个包含来自无法解析模块的星号导入,逃离了项目树的相对导入,高于 `target-python = "3.9"` 的 `match` 语句,以及包装在未解析的助手调用中的 play 的文件: ``` $ manim-lint coverage . analysis coverage (knowledge profile upstream_0_20, target-python 3.9) scene.py constructs above target-python (MLC000): 1 star imports from unresolved modules: 1 unresolved relative imports: 1 calls with no resolved target: 1 of 6 (mystery x1) scene scene.Demo (scene.py) plays with unknown duration: 1 of 2 .animate builders with unknown target: 0 of 0 project files parsed: 1 of 1 calls resolved: 5 of 6 play durations known: 1 of 2 scene constructors resolved: 1 of 1 constructs above target-python (MLC000): 1 unresolved imports: 2 (1 star, 1 relative) manim APIs not in the knowledge profile: 0 helper calls summarized, not inlined: 0 top unresolved calls: mystery x1 analysis confidence: 1/1 files parsed, 5/6 calls resolved, 1/2 play durations known, 1/1 scene constructors resolved (counts of analyzed facts, not estimates) ``` 每个数字都是计算事实的计数;唯一的比率是简单的 `resolved / total` 计数对。`--format json` 将相同的数据作为 稳定的、机器可读的文档发出,包含顶层键 `knowledge_profile`,`target_python`,`files[]`(`path`,`parsed`, `gated_constructs`,`unresolved_star_imports`, `unresolved_relative_imports`,`calls`,`unresolved_calls`, `unresolved_call_names`,`apis_not_in_profile`),`scenes[]`(`name`, `path`,`constructor_state_unknown`,`plays`, `plays_with_unknown_duration`,`builders`, `builders_with_unknown_target`),以及 `project`(总数加上 `top_unresolved_call_names` 和 `helper_inline_fallbacks` —— 这些是指 内联回退到效果摘要的助手调用点,记录在 项目范围内,并在共享助手链的场景中去重,因此 计数仅出现在 `project` 上)。对于相同的输入,输出是确定性的 且字节稳定的。 ## CI 集成 直接在 PR diff 上的 GitHub Actions 批注: ``` name: manim-lint on: [push, pull_request] jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - run: cargo install --path . --locked working-directory: manim-lint # path to your manim-lint checkout - run: manim-lint check . --format github ``` 或者上传 SARIF,以便发现出现在 GitHub 代码扫描 UI 中: ``` - run: manim-lint check . --format sarif > manim-lint.sarif continue-on-error: true - uses: github/codeql-action/upload-sarif@v3 with: sarif_file: manim-lint.sarif ``` ## 预执行准入检查 因为 `manim-lint` 从不导入或执行它读取的代码,所以 渲染用户提供场景的服务可以在花费 sandbox、 CPU 和 GPU 时间*之前*运行它 —— 拒绝在渲染时一定会失败的场景,并 标记成本模型预测会爆炸的场景。 不受信任的输入需要整个契约,而不仅仅是规则: - **限制是被强制执行的,而不是假定的。** 超过 4 MiB 的源码、超过 96 的嵌套深度,或超过 64 的前缀运算符运行,会在 解析之前作为 `MLC000` 被拒绝,因此恶意输入无法耗尽堆栈(参见 [MLC000](docs/rules/MLC000.md))。其他一切都仍然是调用者的 预算:对进程施加你自己的运行时间和内存限制。 - **从观察模式开始。** 在真实渲染结果旁边记录发现,而不是 基于它们进行阻断;只有当规则的 预测通过实际渲染的检查后,才将其提升为阻断。 - **仅在 `certain` 时阻断。** 正确的源代码可能会故意带有错误严重性的 发现 —— 断言 `VGroup(3.0)` 会引发异常的测试就是 最典型的情况。在真实语料库上测量的发现位于 [docs/research/corpus-evidence.md](docs/research/corpus-evidence.md) 中。 ``` # Observe: record everything, never fail the request. manim-lint check "$SCENE_DIR" --format json --fail-level error > findings.json || true # Block: refuse only what the analyzer is certain about. manim-lint check "$SCENE_DIR" --format json \ --min-confidence certain --fail-level error ``` 退出代码 1 表示已达到阈值;退出代码 2 表示使用或 配置错误,绝不应被视为被拒绝的场景。 ## 规则目录 该目录包含四大类的 92 个规则 ID;**所有 92 个都已 实现**: | 类别 | 已实现 | 已保留 | | --- | --- | --- | | MLC 生命周期/正确性 | 31 | 0 | | MLR 渲染 | 27 | 0 | | MLP 性能 | 27 | 0 | | MLD 确定性/可移植性 | 7 | 0 | 一个已实现的规则是选择性的:`MLP225` 具有 `default_enabled: false` 并且 从不加入正常的 `check` 运行;只有在本地 fork 配置文件下精确的 `--select MLP225` 才会对其进行评估。 带有每个规则状态、严重性和置信度的完整索引位于 [docs/rules/README.md](docs/rules/README.md) 中;每个已实现的规则在那里都有一个 文档页面,也可以通过 `manim-lint explain ` 获得。 ## 架构 ``` Python sources | SourceManager ............ encoding (PEP 263), newlines, Unicode columns | knowledge profile ........ versioned Manim 0.20 semantics (no import, ever) | frontend ................. imports/aliases, project index, qualified call facts | semantic ................. lifecycle abstract interpreter -> LifecycleFacts | cost ..................... hot contexts, frame intervals -> CostFacts | rules .................... MLC / MLR / MLP / MLD over the fact layers | suppressions, supersedes, baseline | output ................... concise | full | json | sarif | github, fixes, cost report ``` [docs/architecture.md](docs/architecture.md) 引导新的贡献者 了解此流水线:每个事实层提供什么,它位于何处, 知识配置文件系统,以及一个诊断是如何进行端到端流转的。 [`DESIGN.md`](DESIGN.md) 是语义模型、规则目录 和每个公共契约的权威规范。JSON 输出遵循 [`schemas/diagnostics-v1.json`](schemas/diagnostics-v1.json);baselines 遵循 [`schemas/baseline-v1.json`](schemas/baseline-v1.json)。由 `manim-lint static-facts` 发出的 Poietra/fast-manim 语义桥接由 [`StaticFacts v0`](docs/rfcs/0001-static-facts-v0.md) 及其 [`JSON Schema`](schemas/static-facts-v0.json) 指定。它发布快照范围内的 Scene/object/play/animation/updater ID,编码感知的源锚点, 带有原因的未知项,渲染器风险,以及覆盖率边界,而不会 暴露分析器句柄。它报告阻碍因素,但从不授予跳过或 fork 渲染的权限。对于相同的输入,输出是确定性的 且字节稳定的。 独立于缓存的 [`SemanticDependencyGraph v0`](docs/rfcs/0002-semantic-dependency-graph-v0.md) 是用于缓存组件分区和保守 源代码更改影响的共享事实层。它保留锚定的 Unknown 边界,而不是 猜测动态依赖边缘。 [`ChangeImpact v0`](docs/rfcs/0003-change-impact-v0.md) 比较两个源 快照,并发出经过 schema 验证的、带有原因的 Scene/play/object 影响 候选对象,包括从目标树中删除的关系。 [`SourceBridge v0`](docs/rfcs/0004-source-bridge-v0.md) 生成受哈希保护的 本地补丁候选对象,根据内存中的重新分析对它们进行验证,并且 报告 `match | ambiguous | missing` 而不写入项目文件。 ## 已知局限性 - **目标版本。** 按照发布,知识配置文件涵盖 Manim Community **仅 0.20** 版本。其他版本尚无配置文件。 - **资产检查探测运行 lint 的机器。** `MLR104` 使用 Manim 自身的运行时搜索,在运行 lint 的机器上解析字面 资产路径。对于项目树之外的绝对路径,这是关于 lint 主机的证据,不一定是渲染主机的(例如,CI 对在其他地方渲染的存储库进行 lint);这些诊断带有 `environment_dependent: true` 作为证据。仅大小写不匹配的报告 仅针对区分大小写的目标平台(`linux`);当所有受影响的配置文件都针对 windows/macos 时,声明的渲染会按原样解析文件,并且 linter 保持沉默。 - **源代码编码。** PEP 263 声明通过 WHATWG 标签 加上 CPython 编解码器别名表(`latin-1`,`cp932`,`koi8_r` 等)进行解析。 linter 无法表示的罕见 Python 编解码器将通过明确的 `MLC000` “not supported by manim-lint”通知被跳过 —— 绝不意味着 目标 Python 无法解码该文件。 - **持续时间仅来自字面量。** 一个持续时间依赖于 Manim *默认值*的 play(`self.play(m.animate.shift(RIGHT))` 在任何地方都没有 `run_time`,`self.wait()`)被报告为 unknown —— 帧计数 使用每帧的措辞而不是数字(保守:缺失,从不 捏造)。字面的 play 级别的 `run_time` 精确地决定整个 play 的 持续时间 —— 包括 Scene 助手中的 play,每个调用点, 即使调用点向参数上的 `.animate` builder 传递不同(或未追踪)的 mobject —— 并且一个*非字面量*的 `run_time` (或 `**kwargs` 展开)诚实地扩大了它所覆盖的构造函数字面量。 - **源自摘要的 play 是保守的。** 当助手内联回退到 效果摘要时(递归,一个无法解析的调用 —— 在覆盖率报告中计为 `helper calls summarized, not inlined`),助手的 play 仍然会 实例化,但作为具有开放重复次数的 `Maybe` 确定性记录:诸如 `MLC104` 之类的字面持续时间检查仍会在那里触发, 而每个依赖于调用者状态的判断都会保持降级状态。 - **`TracedPath` 在构造函数中注册的 updater 仅用于成本计算。** `TracedPath` 在构造时在自身上注册的 updater 是为了成本目的而建模的(`MLP220` 范围,traced lambda 的热上下文条目 但它不是生命周期 updater 注册:在生命周期模型中,单独的 `TracedPath` 不会使默认的 `wait()` 动态化,并且 绑定的方法 `traced_point_func` 主体不会被分析为热上下文。 - **故意的保守静默。** 一些检测比 其目录描述的范围更窄,并保持沉默而不是猜测:`MLR106` 仅看到 字面形式的 NaN/inf,而不是通过 `float("nan")` 调用;`MLD301` 仅证明缺少 `dt` 参数的 updater 的 FPS 依赖性( 声明但未使用的 `dt` 不被标记);`MLC113`/`MLC124` 仅识别 它们记录的调用形状;`MLR102` 需要解释器证明 播放的裸 builder 的目标未更改;`MLR105` 验证已验证的 Pango 子集(允许裸 `&`);`MLD304` 仅实现 ThreeDScene 固定对象清理差异。`manim-lint explain ` 说明了每个规则的确切范围。 - **尚未实现。** 针对渲染 baselines 的阈值校准; 每晚的渲染比较 CI。 ## 开发 ``` cargo fmt --check cargo build cargo test --all-features cargo clippy --all-targets --all-features -- -D warnings ``` 必须通过所有四个关卡。 知识配置文件维护:`sync_manim_knowledge` 二进制文件静态地 读取 Manim 检出,生成可审查的配置文件候选对象,并检查 发布的配置文件是否存在偏移(矛盾时退出代码 1)—— 参见 [src/knowledge/profiles/README.md](src/knowledge/profiles/README.md)。 来源被拆分:`upstream_0_20` 描述了**干净**的上游基础 提交 `4d25c031`(通过 `git archive` 读取,从不读取工作树),并且 `local_0_20_1_4d25c031` 覆盖层带有同属 fork 的工作 树在其之上添加的内容: ``` # working tree (fork) — informational against upstream cargo run --features dev-tools --bin sync_manim_knowledge -- --manim-root ../manim --diff # clean upstream base — must be contradiction-free cargo run --features dev-tools --bin sync_manim_knowledge -- --manim-root ../manim --manim-ref 4d25c031 --diff cargo test --test knowledge_drift -- --ignored # layer-9 drift gate (both) ``` ### 发布质量关卡(DESIGN §11.4) 三个额外的关卡保护发布: ``` # Labeled corpus gate — runs automatically inside `cargo test`. # tests/corpus/manifest-v1.json pins sha256 + exact expected diagnostics # (true positives and false-positive guards) for every corpus case, # including pinned real-Manim example_scenes snapshots and the # adversarial review probes. cargo test --test corpus_gate # Benchmark gate — explicit, release build, quiet machine. # Cold ≤ 2 s / warm hit ≤ 0.5 s / one-of-20 incremental ≤ 0.5 s / # peak RSS < 300 MiB over the pinned 10k-LOC fixture # (tests/corpus/benchmark_10kloc); thresholds assert only on the machine # matching benchmarks/reference-machine.json, informational elsewhere. # The gate proves cold is a miss, warm a validated hit, and incremental a partial hit. # Three-run median on the reference machine (2026-07-20): cold 0.422 s, # warm 0.006 s, incremental 0.171 s, peak RSS 246.5 MiB — all within budget. cargo test --release --test benchmark_gate -- --ignored benchmark # Knowledge drift gate — needs the sibling Manim checkout; in CI it runs # on schedule/dispatch against a shallow clone of the pinned base commit. cargo test --test knowledge_drift -- --ignored ``` 语料库案例绝不会机械地重新记录:不匹配意味着 在 [CONTRIBUTING.md](CONTRIBUTING.md#corpus-labeling) 中的标记协议下重新裁定。 有关 仓库布局、添加规则的分步指南以及 每次更改必须保持的不变量,请参见 [CONTRIBUTING.md](CONTRIBUTING.md);有关流水线和 事实层概述,请参见 [docs/architecture.md](docs/architecture.md)。`DESIGN.md` 是权威的;对 公共契约的更改必须更新它、其 schema 测试以及规则文档 。 ## License [MIT](LICENSE)。 依赖许可证,以及在发布 预构建二进制文件之前值得了解的一个后果 —— Python 解析器引入了一个 LGPL-3.0-only 的大整数 crate,Rust 将其静态链接 —— 记录在 [THIRD-PARTY-LICENSES.md](THIRD-PARTY-LICENSES.md) 中。从源码 安装(`cargo install`)不受影响。 预构建的发布版本包含 LGPL/GPL 文本、精确锁定的源代码,以及 [重新链接说明](RELINKING.md),适用于所有发行格式。如果 缺少该材料,发布关卡将拒绝发布。 `manim-lint` 是一个独立的项目。Manim Community 未参与 也不认可它。
标签:Manim, Python, Rust, SOC Prime, 代码质量检查, 动画引擎, 可视化界面, 开发工具, 无后门, 网络流量审计, 逆向工具, 通知系统, 错误基检测, 静态代码分析