docs: bilingual comments for the CI workflow, manifests and scripts
Extends the checker to the module level `//!` docs, the tests, the examples, and the non-Rust prose comments in the workflow, the manifests and the shell and Python scripts.
This commit is contained in:
parent
26a21a23bd
commit
fd829e797f
|
|
@ -20,6 +20,8 @@ jobs:
|
||||||
- name: Check
|
- name: Check
|
||||||
# --all-features matters: the facade crate enables nothing by default,
|
# --all-features matters: the facade crate enables nothing by default,
|
||||||
# so without it almost all of its code, and every example, is skipped.
|
# so without it almost all of its code, and every example, is skipped.
|
||||||
|
# `--all-features` 很重要:门面 crate 默认不启用任何特性,没有它
|
||||||
|
# 几乎其全部代码以及每个示例都会被跳过。
|
||||||
run: cargo check --workspace --all-targets --all-features
|
run: cargo check --workspace --all-targets --all-features
|
||||||
- name: Clippy
|
- name: Clippy
|
||||||
run: cargo clippy --workspace --all-targets --all-features -- -D warnings
|
run: cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||||
|
|
@ -30,6 +32,9 @@ jobs:
|
||||||
# are also meant to be read and run by hand. `cargo test` does not run
|
# are also meant to be read and run by hand. `cargo test` does not run
|
||||||
# them, so without this step a change that breaks one layer's
|
# them, so without this step a change that breaks one layer's
|
||||||
# assumptions about another would pass CI silently.
|
# assumptions about another would pass CI silently.
|
||||||
|
# 跨层检查是示例而非测试,因为它们也意在供人阅读并手动运行。
|
||||||
|
# `cargo test` 不会运行它们,故没有这一步,破坏某层对另一层假设的
|
||||||
|
# 改动会无声地通过 CI。
|
||||||
run: |
|
run: |
|
||||||
cargo run -p apsis-rs --features network --example integrate_network
|
cargo run -p apsis-rs --features network --example integrate_network
|
||||||
cargo run -p apsis-rs --features ops --example integrate_ops
|
cargo run -p apsis-rs --features ops --example integrate_ops
|
||||||
|
|
@ -38,6 +43,8 @@ jobs:
|
||||||
# The benchmarks are not run in CI, because their timings are only
|
# The benchmarks are not run in CI, because their timings are only
|
||||||
# meaningful on a quiet machine, but they must still compile so that
|
# meaningful on a quiet machine, but they must still compile so that
|
||||||
# they do not rot.
|
# they do not rot.
|
||||||
|
# 基准在 CI 中不运行,因为其计时只在安静的机器上有意义,但它们
|
||||||
|
# 仍必须能编译,以免腐坏。
|
||||||
run: |
|
run: |
|
||||||
cargo build -p apsis-rs --features full --example bench
|
cargo build -p apsis-rs --features full --example bench
|
||||||
cargo build -p apsis-rs --features full --example bench_scale
|
cargo build -p apsis-rs --features full --example bench_scale
|
||||||
|
|
@ -45,6 +52,7 @@ jobs:
|
||||||
- name: Documentation
|
- name: Documentation
|
||||||
# Catches a doc link that no longer resolves, which is how a doc
|
# Catches a doc link that no longer resolves, which is how a doc
|
||||||
# comment drifts away from the code it describes.
|
# comment drifts away from the code it describes.
|
||||||
|
# 捕获不再解析的文档链接,文档注释正是这样与其所述代码渐行渐远。
|
||||||
env:
|
env:
|
||||||
RUSTDOCFLAGS: -D warnings
|
RUSTDOCFLAGS: -D warnings
|
||||||
run: cargo doc --workspace --all-features --no-deps
|
run: cargo doc --workspace --all-features --no-deps
|
||||||
|
|
@ -52,12 +60,17 @@ jobs:
|
||||||
# README.md states that a crate may only depend on layers to its
|
# README.md states that a crate may only depend on layers to its
|
||||||
# left. Nothing else in the build enforces that, so a reverse
|
# left. Nothing else in the build enforces that, so a reverse
|
||||||
# dependency would otherwise pass CI silently.
|
# dependency would otherwise pass CI silently.
|
||||||
|
# README.md 规定一个 crate 只能依赖其左侧的层。构建中没有别的
|
||||||
|
# 东西强制这一点,故反向依赖否则会无声地通过 CI。
|
||||||
run: bash scripts/check-layers.sh
|
run: bash scripts/check-layers.sh
|
||||||
- name: Verify dependency freedom
|
- name: Verify dependency freedom
|
||||||
run: |
|
run: |
|
||||||
# Every package in the graph must be a workspace crate. `--prefix
|
# Every package in the graph must be a workspace crate. `--prefix
|
||||||
# none` prints bare `name version` lines, one per package, which the
|
# none` prints bare `name version` lines, one per package, which the
|
||||||
# box drawing prefix of the default format would otherwise hide.
|
# box drawing prefix of the default format would otherwise hide.
|
||||||
|
# 依赖图中的每个包都必须是工作区 crate。`--prefix none` 打印
|
||||||
|
# 裸的 `name version` 行,每包一行;否则默认格式的制表前缀会把
|
||||||
|
# 它隐藏。
|
||||||
external=$(cargo tree --workspace --all-features \
|
external=$(cargo tree --workspace --all-features \
|
||||||
--edges normal,build,dev --prefix none 2>/dev/null \
|
--edges normal,build,dev --prefix none 2>/dev/null \
|
||||||
| sed -E 's/ \(\*\)$//; s/ \([^)]*\)$//' \
|
| sed -E 's/ \(\*\)$//; s/ \([^)]*\)$//' \
|
||||||
|
|
|
||||||
23
Cargo.toml
23
Cargo.toml
|
|
@ -3,25 +3,30 @@
|
||||||
# The workspace resolves without any external dependency so that the
|
# The workspace resolves without any external dependency so that the
|
||||||
# numerical kernels stay self-contained and auditable. Every member crate
|
# numerical kernels stay self-contained and auditable. Every member crate
|
||||||
# is unpublished.
|
# is unpublished.
|
||||||
|
#
|
||||||
|
# ApsisRS Rust 工作区根。
|
||||||
|
#
|
||||||
|
# 工作区在没有任何外部依赖的情况下解析,使各数值内核保持自包含且可
|
||||||
|
# 审计。每个成员 crate 都不发布。
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
members = [
|
members = [
|
||||||
# foundation
|
# foundation / 基础层
|
||||||
"crates/foundation/apsis-core",
|
"crates/foundation/apsis-core",
|
||||||
"crates/foundation/apsis-units",
|
"crates/foundation/apsis-units",
|
||||||
"crates/foundation/apsis-math",
|
"crates/foundation/apsis-math",
|
||||||
"crates/foundation/apsis-macros",
|
"crates/foundation/apsis-macros",
|
||||||
|
|
||||||
# physics
|
# physics / 物理层
|
||||||
"crates/physics/apsis-time",
|
"crates/physics/apsis-time",
|
||||||
"crates/physics/apsis-leap",
|
"crates/physics/apsis-leap",
|
||||||
"crates/physics/apsis-coord",
|
"crates/physics/apsis-coord",
|
||||||
"crates/physics/apsis-geodesy",
|
"crates/physics/apsis-geodesy",
|
||||||
"crates/physics/apsis-frames",
|
"crates/physics/apsis-frames",
|
||||||
|
|
||||||
# orbit
|
# orbit / 轨道层
|
||||||
"crates/orbit/apsis-tle",
|
"crates/orbit/apsis-tle",
|
||||||
"crates/orbit/apsis-sgp4",
|
"crates/orbit/apsis-sgp4",
|
||||||
"crates/orbit/apsis-kepler",
|
"crates/orbit/apsis-kepler",
|
||||||
|
|
@ -29,7 +34,7 @@ members = [
|
||||||
"crates/orbit/apsis-maneuver",
|
"crates/orbit/apsis-maneuver",
|
||||||
"crates/orbit/apsis-orbit",
|
"crates/orbit/apsis-orbit",
|
||||||
|
|
||||||
# rf
|
# rf / 射频层
|
||||||
"crates/rf/apsis-rf",
|
"crates/rf/apsis-rf",
|
||||||
"crates/rf/apsis-antenna",
|
"crates/rf/apsis-antenna",
|
||||||
"crates/rf/apsis-doppler",
|
"crates/rf/apsis-doppler",
|
||||||
|
|
@ -38,7 +43,7 @@ members = [
|
||||||
"crates/rf/apsis-interference",
|
"crates/rf/apsis-interference",
|
||||||
"crates/rf/apsis-link",
|
"crates/rf/apsis-link",
|
||||||
|
|
||||||
# network
|
# network / 网络层
|
||||||
"crates/network/apsis-constellation",
|
"crates/network/apsis-constellation",
|
||||||
"crates/network/apsis-coverage",
|
"crates/network/apsis-coverage",
|
||||||
"crates/network/apsis-access",
|
"crates/network/apsis-access",
|
||||||
|
|
@ -48,21 +53,21 @@ members = [
|
||||||
"crates/network/apsis-handover",
|
"crates/network/apsis-handover",
|
||||||
"crates/network/apsis-mesh",
|
"crates/network/apsis-mesh",
|
||||||
|
|
||||||
# ops
|
# ops / 运行层
|
||||||
"crates/ops/apsis-resource",
|
"crates/ops/apsis-resource",
|
||||||
"crates/ops/apsis-task",
|
"crates/ops/apsis-task",
|
||||||
"crates/ops/apsis-scheduler",
|
"crates/ops/apsis-scheduler",
|
||||||
"crates/ops/apsis-conflict",
|
"crates/ops/apsis-conflict",
|
||||||
"crates/ops/apsis-schedule",
|
"crates/ops/apsis-schedule",
|
||||||
|
|
||||||
# sim
|
# sim / 仿真层
|
||||||
"crates/sim/apsis-sim-engine",
|
"crates/sim/apsis-sim-engine",
|
||||||
"crates/sim/apsis-scenario",
|
"crates/sim/apsis-scenario",
|
||||||
"crates/sim/apsis-event",
|
"crates/sim/apsis-event",
|
||||||
"crates/sim/apsis-output",
|
"crates/sim/apsis-output",
|
||||||
"crates/sim/apsis-sim",
|
"crates/sim/apsis-sim",
|
||||||
|
|
||||||
# facade
|
# facade / 门面层
|
||||||
"crates/apsis-rs",
|
"crates/apsis-rs",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -81,6 +86,8 @@ missing_docs = "deny"
|
||||||
# Numeric literals here follow the grouping of the quantity they express,
|
# Numeric literals here follow the grouping of the quantity they express,
|
||||||
# such as the digits of a reference value, rather than a fixed width. The
|
# such as the digits of a reference value, rather than a fixed width. The
|
||||||
# style lint would force a regrouping that obscures the source of a value.
|
# style lint would force a regrouping that obscures the source of a value.
|
||||||
|
# 此处的数值字面量按其所表达量的分组来分隔,例如某参考值的各位数字,
|
||||||
|
# 而非固定宽度。风格 lint 会强制重新分组,从而模糊某值的来源。
|
||||||
inconsistent_digit_grouping = "allow"
|
inconsistent_digit_grouping = "allow"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ path = "src/lib.rs"
|
||||||
|
|
||||||
# Examples that produce validation input. Each needs the layers it reads
|
# Examples that produce validation input. Each needs the layers it reads
|
||||||
# from, so it is built only when the matching feature is enabled.
|
# from, so it is built only when the matching feature is enabled.
|
||||||
|
# 产生验证输入的示例。每个都需要它所读取的层,故只在匹配的特性启用时
|
||||||
|
# 才被构建。
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "dump_geodesy"
|
name = "dump_geodesy"
|
||||||
|
|
@ -37,6 +39,7 @@ workspace = true
|
||||||
default = []
|
default = []
|
||||||
|
|
||||||
# Enabling a layer also enables the layers it is allowed to depend on.
|
# Enabling a layer also enables the layers it is allowed to depend on.
|
||||||
|
# 启用某一层也会启用它所允许依赖的各层。
|
||||||
foundation = [
|
foundation = [
|
||||||
"dep:apsis-core",
|
"dep:apsis-core",
|
||||||
"dep:apsis-units",
|
"dep:apsis-units",
|
||||||
|
|
@ -97,6 +100,8 @@ ops = [
|
||||||
|
|
||||||
# The simulation layer aggregates every other layer, so it enables `ops`.
|
# The simulation layer aggregates every other layer, so it enables `ops`.
|
||||||
# Enable `ops` instead of `full` when the simulation layer is not needed.
|
# Enable `ops` instead of `full` when the simulation layer is not needed.
|
||||||
|
# 仿真层聚合其余每一层,故它启用 `ops`。不需要仿真层时请启用 `ops`
|
||||||
|
# 而非 `full`。
|
||||||
sim = [
|
sim = [
|
||||||
"ops",
|
"ops",
|
||||||
"dep:apsis-sim-engine",
|
"dep:apsis-sim-engine",
|
||||||
|
|
@ -108,6 +113,7 @@ sim = [
|
||||||
|
|
||||||
# Every layer, including `sim`. Enabling `sim` pulls in `ops` and, through
|
# Every layer, including `sim`. Enabling `sim` pulls in `ops` and, through
|
||||||
# it, every lower layer.
|
# it, every lower layer.
|
||||||
|
# 每一层,包括 `sim`。启用 `sim` 会引入 `ops`,并经它引入每一个更低的层。
|
||||||
full = ["sim"]
|
full = ["sim"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,17 @@ skipping.
|
||||||
Usage, from the repository root:
|
Usage, from the repository root:
|
||||||
|
|
||||||
python3 scripts/bilingual.py crates/orbit/apsis-kepler/src/elements.rs
|
python3 scripts/bilingual.py crates/orbit/apsis-kepler/src/elements.rs
|
||||||
|
|
||||||
|
对文件施加一张「英文到双语注释」的替换表。
|
||||||
|
|
||||||
|
对仓库的清扫是一长串编辑,每一次都把一段英文注释块改写为英文段后接中文
|
||||||
|
段。要把每一对写两遍(作为两次独立替换)容易出错,故此辅助工具接收一张
|
||||||
|
替换对列表,施加后重写文件,并报告有多少未匹配。未匹配的替换几乎总是
|
||||||
|
英文文本的重新折行,值得一看而非跳过。
|
||||||
|
|
||||||
|
用法(在仓库根下):
|
||||||
|
|
||||||
|
python3 scripts/bilingual.py crates/orbit/apsis-kepler/src/elements.rs
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
@ -21,7 +32,10 @@ from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
def apply(path: Path, pairs: list[tuple[str, str]]) -> tuple[int, list[str]]:
|
def apply(path: Path, pairs: list[tuple[str, str]]) -> tuple[int, list[str]]:
|
||||||
"""Replaces each pair and returns how many applied with the misses."""
|
"""Replaces each pair and returns how many applied with the misses.
|
||||||
|
|
||||||
|
替换每一对,并返回已施加的数量与未匹配者。
|
||||||
|
"""
|
||||||
text = path.read_text(encoding="utf-8")
|
text = path.read_text(encoding="utf-8")
|
||||||
missed: list[str] = []
|
missed: list[str] = []
|
||||||
applied = 0
|
applied = 0
|
||||||
|
|
|
||||||
|
|
@ -6,56 +6,117 @@ English paragraph first and the Chinese one after a blank doc line. This
|
||||||
script reports the blocks that have no Chinese yet, so the sweep can be
|
script reports the blocks that have no Chinese yet, so the sweep can be
|
||||||
driven and verified without reading every file.
|
driven and verified without reading every file.
|
||||||
|
|
||||||
|
It covers the Rust sources, including the module level `//!` docs, the
|
||||||
|
integration tests, and the examples, and also the non-Rust files that carry
|
||||||
|
prose comments: the CI workflow, the manifests, the shell and Python
|
||||||
|
scripts, and the Python docstrings.
|
||||||
|
|
||||||
It also reports the two mistakes that are easy to make while adding a
|
It also reports the two mistakes that are easy to make while adding a
|
||||||
language: a code string that was translated by accident, and a comment that
|
language: a code string that was translated by accident, and a comment that
|
||||||
mentions a term the glossary keeps in English where a Chinese name exists.
|
mentions a term the glossary keeps in English where a Chinese name exists.
|
||||||
|
|
||||||
|
检查每一处源码注释是否有中文对应。
|
||||||
|
|
||||||
|
仓库正在被改为双语:每个注释块先写英文段,再在一条空白文档行之后写中文
|
||||||
|
段。本脚本报告尚无中文的块,使这次清扫无需逐文件阅读即可被驱动与核验。
|
||||||
|
|
||||||
|
它覆盖 Rust 源码,包括模块级 `//!` 文档、集成测试与示例,也覆盖携带散文
|
||||||
|
注释的非 Rust 文件:CI 工作流、各清单、shell 与 Python 脚本,以及 Python
|
||||||
|
文档字符串。
|
||||||
|
|
||||||
|
它还会报告添加一种语言时容易犯的两种错误:被误译的代码字符串,以及提到
|
||||||
|
了词汇表在中文名存在处仍保留英文术语的注释。
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
|
|
||||||
python3 scripts/check-bilingual.py # every crate
|
python3 scripts/check-bilingual.py # the whole repository
|
||||||
python3 scripts/check-bilingual.py crates/orbit
|
python3 scripts/check-bilingual.py crates/orbit
|
||||||
python3 scripts/check-bilingual.py --quiet # only the summary line
|
python3 scripts/check-bilingual.py --quiet # only the summary line
|
||||||
|
|
||||||
|
用法:
|
||||||
|
|
||||||
|
python3 scripts/check-bilingual.py # 整个仓库
|
||||||
|
python3 scripts/check-bilingual.py crates/orbit
|
||||||
|
python3 scripts/check-bilingual.py --quiet # 只打印汇总行
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import ast
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parent.parent
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
# A comment line: a doc line, or a plain comment that is not a doc marker.
|
# A Rust comment line: a doc line, a module doc line, or a plain comment.
|
||||||
|
# Rust 注释行:文档行、模块文档行,或普通注释。
|
||||||
DOC = re.compile(r"^\s*///")
|
DOC = re.compile(r"^\s*///")
|
||||||
|
MOD = re.compile(r"^\s*//!")
|
||||||
LINE = re.compile(r"^\s*//[^/!]")
|
LINE = re.compile(r"^\s*//[^/!]")
|
||||||
|
|
||||||
|
# A hash comment line, used by the non-Rust files. The shebang is excluded
|
||||||
|
# because it is an interpreter directive rather than prose.
|
||||||
|
# 非 Rust 文件所用的井号注释行。shebang 被排除,因为它是解释器指令而非散文。
|
||||||
|
HASH = re.compile(r"^\s*#")
|
||||||
|
SHEBANG = re.compile(r"^#!")
|
||||||
|
|
||||||
# CJK, used to decide whether a block already carries the Chinese text.
|
# CJK, used to decide whether a block already carries the Chinese text.
|
||||||
|
# CJK,用于判断某个块是否已携带中文。
|
||||||
CJK = re.compile(r"[\u4e00-\u9fff]")
|
CJK = re.compile(r"[\u4e00-\u9fff]")
|
||||||
|
|
||||||
# The replacement character, which shows up when a byte sequence is mangled.
|
# The replacement character, which shows up when a byte sequence is mangled.
|
||||||
MOJIBAKE = "<EFBFBD>"
|
# 替换字符,当字节序列被损坏时出现。
|
||||||
|
MOJIBAKE = "\ufffd"
|
||||||
|
|
||||||
# An English function word sitting between Chinese characters, which means a
|
# An English function word sitting between Chinese characters, which means a
|
||||||
# half translated sentence. The glossary keeps words like `Walker` and
|
# half translated sentence. The glossary keeps words like `Walker` and
|
||||||
# `SGP4` in English, so a lowercase word between CJK is a mistake.
|
# `SGP4` in English, so a lowercase word between CJK is a mistake.
|
||||||
|
# 夹在汉字之间的英文虚词,意味着一个半翻译的句子。词汇表把 `Walker` 与
|
||||||
|
# `SGP4` 之类的词保留为英文,故 CJK 之间的小写单词是错误。
|
||||||
MIXED = re.compile(r"[\u4e00-\u9fff][a-z]{3,}[\u4e00-\u9fff]")
|
MIXED = re.compile(r"[\u4e00-\u9fff][a-z]{3,}[\u4e00-\u9fff]")
|
||||||
|
|
||||||
# A comment that is only a formula or a bare symbol carries no prose to
|
# A comment that is only a formula or a bare symbol carries no prose to
|
||||||
# translate, so demanding a second language on it would be noise. Such a
|
# translate, so demanding a second language on it would be noise. Such a
|
||||||
# comment is left as it stands, which is the whole of it.
|
# comment is left as it stands, which is the whole of it.
|
||||||
FORMULA = re.compile("^`?[A-Za-z0-9_^*+\\-/() =,;.\u3002\uff0c]*`?[\.\u3002]?$")
|
# 只含公式或纯符号的注释没有可翻译的散文,故要求它带第二语言只会是噪音。
|
||||||
|
# 这样的注释保持原样,而它就是其全部。
|
||||||
|
FORMULA = re.compile(
|
||||||
|
r"^`?[A-Za-z0-9_^*+\-/() =,;.\u3002\uff0c]*`?[.\u3002]?$"
|
||||||
|
)
|
||||||
|
|
||||||
# Three letters in a row is a word, not a symbol, so a line carrying one is
|
# Three letters in a row is a word, not a symbol, so a line carrying one is
|
||||||
# prose and needs translating even if it is mostly numbers.
|
# prose and needs translating even if it is mostly numbers.
|
||||||
|
# 连续三个字母便是单词而非符号,故带它的行是散文,即便大部分是数字也需翻译。
|
||||||
PROSE = re.compile(r"[A-Za-z]{3}")
|
PROSE = re.compile(r"[A-Za-z]{3}")
|
||||||
|
|
||||||
|
# Directories that hold generated or third party files, which carry no
|
||||||
|
# comment that this repository owns.
|
||||||
|
# 存放生成文件或第三方文件的目录,其中没有本仓库拥有的注释。
|
||||||
|
SKIP = ("target", ".pixi", "node_modules", ".git")
|
||||||
|
|
||||||
|
# File suffixes that carry hash comments rather than the Rust slashes.
|
||||||
|
# 携带井号注释而非 Rust 斜杠注释的文件后缀。
|
||||||
|
HASH_SUFFIXES = (".yml", ".yaml", ".toml", ".sh", ".py")
|
||||||
|
|
||||||
|
|
||||||
|
def strip_marker(line: str) -> str:
|
||||||
|
"""Returns the body of a comment line with its marker removed.
|
||||||
|
|
||||||
|
返回去除注释标记后的注释行正文。
|
||||||
|
"""
|
||||||
|
return re.sub(r"^\s*(///|//!|//|#)\s?", "", line).strip()
|
||||||
|
|
||||||
|
|
||||||
def is_formula_only(block: list[str]) -> bool:
|
def is_formula_only(block: list[str]) -> bool:
|
||||||
"""True when every line of the block is code, a formula, or blank."""
|
"""True when every line of the block is code, a formula, or blank.
|
||||||
|
|
||||||
|
当块中每一行都是代码、公式或空白时返回真。
|
||||||
|
"""
|
||||||
saw_content = False
|
saw_content = False
|
||||||
for line in block:
|
for line in block:
|
||||||
body = re.sub(r"^\s*(///|//[^/!])\s?", "", line).strip()
|
body = strip_marker(line)
|
||||||
if not body:
|
if not body:
|
||||||
continue
|
continue
|
||||||
saw_content = True
|
saw_content = True
|
||||||
|
|
@ -63,21 +124,58 @@ def is_formula_only(block: list[str]) -> bool:
|
||||||
return False
|
return False
|
||||||
# A single backticked token is a symbol, whatever it spells. Prose
|
# A single backticked token is a symbol, whatever it spells. Prose
|
||||||
# needs a verb, and `cos` or `eta` has none.
|
# needs a verb, and `cos` or `eta` has none.
|
||||||
|
# 单个反引号包裹的记号是符号,不论它拼出什么。散文需要动词,而
|
||||||
|
# `cos` 或 `eta` 没有。
|
||||||
if len(re.findall(r"[A-Za-z]{2,}", body)) > 1:
|
if len(re.findall(r"[A-Za-z]{2,}", body)) > 1:
|
||||||
return False
|
return False
|
||||||
return saw_content
|
return saw_content
|
||||||
|
|
||||||
|
|
||||||
def blocks(path: Path) -> list[tuple[int, list[str]]]:
|
def rust_blocks(path: Path) -> list[tuple[int, list[str]]]:
|
||||||
"""Returns the comment blocks of a file with the line each starts on."""
|
"""Returns the comment blocks of a Rust file with the line each starts
|
||||||
|
on. Each of the three markers runs within its own block, so a `//!`
|
||||||
|
module header is not merged into an adjacent `///` declaration.
|
||||||
|
|
||||||
|
返回 Rust 文件的注释块及其各自的起始行。三种标记各自在自己的块内
|
||||||
|
连续,故 `//!` 模块头部不会与相邻的 `///` 声明合并。
|
||||||
|
"""
|
||||||
lines = path.read_text(encoding="utf-8").splitlines()
|
lines = path.read_text(encoding="utf-8").splitlines()
|
||||||
found: list[tuple[int, list[str]]] = []
|
found: list[tuple[int, list[str]]] = []
|
||||||
index = 0
|
index = 0
|
||||||
while index < len(lines):
|
while index < len(lines):
|
||||||
if DOC.match(lines[index]) or LINE.match(lines[index]):
|
for marker in (DOC, MOD, LINE):
|
||||||
|
if marker.match(lines[index]):
|
||||||
|
start = index
|
||||||
|
block: list[str] = []
|
||||||
|
while index < len(lines) and marker.match(lines[index]):
|
||||||
|
block.append(lines[index])
|
||||||
|
index += 1
|
||||||
|
found.append((start + 1, block))
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
index += 1
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def hash_blocks(path: Path) -> list[tuple[int, list[str]]]:
|
||||||
|
"""Returns the hash comment blocks of a non-Rust file with the line each
|
||||||
|
starts on. A shebang is skipped because it opens the file.
|
||||||
|
|
||||||
|
返回非 Rust 文件的井号注释块及其各自的起始行。shebang 被跳过,
|
||||||
|
因为它位于文件开头。
|
||||||
|
"""
|
||||||
|
lines = path.read_text(encoding="utf-8").splitlines()
|
||||||
|
found: list[tuple[int, list[str]]] = []
|
||||||
|
index = 0
|
||||||
|
while index < len(lines):
|
||||||
|
if HASH.match(lines[index]) and not SHEBANG.match(lines[index]):
|
||||||
start = index
|
start = index
|
||||||
block: list[str] = []
|
block: list[str] = []
|
||||||
while index < len(lines) and (DOC.match(lines[index]) or LINE.match(lines[index])):
|
while (
|
||||||
|
index < len(lines)
|
||||||
|
and HASH.match(lines[index])
|
||||||
|
and not SHEBANG.match(lines[index])
|
||||||
|
):
|
||||||
block.append(lines[index])
|
block.append(lines[index])
|
||||||
index += 1
|
index += 1
|
||||||
found.append((start + 1, block))
|
found.append((start + 1, block))
|
||||||
|
|
@ -86,19 +184,80 @@ def blocks(path: Path) -> list[tuple[int, list[str]]]:
|
||||||
return found
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def docstring_blocks(path: Path) -> list[tuple[int, list[str]]]:
|
||||||
|
"""Returns the docstrings of a Python file with the line each starts on.
|
||||||
|
A docstring is the Python form of a doc comment, so it is swept too.
|
||||||
|
|
||||||
|
返回 Python 文件的文档字符串及其各自的起始行。文档字符串是 Python
|
||||||
|
形式的文档注释,故也一并清扫。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
tree = ast.parse(path.read_text(encoding="utf-8"))
|
||||||
|
except (SyntaxError, ValueError):
|
||||||
|
return []
|
||||||
|
found: list[tuple[int, list[str]]] = []
|
||||||
|
holders = (ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if not isinstance(node, holders) or not node.body:
|
||||||
|
continue
|
||||||
|
first = node.body[0]
|
||||||
|
if not isinstance(first, ast.Expr):
|
||||||
|
continue
|
||||||
|
value = first.value
|
||||||
|
if isinstance(value, ast.Constant) and isinstance(value.value, str):
|
||||||
|
found.append((first.lineno, value.value.splitlines()))
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def collect(path: Path) -> list[tuple[int, list[str]]]:
|
||||||
|
"""Returns every comment or docstring block of one file.
|
||||||
|
|
||||||
|
返回单个文件的每一个注释或文档字符串块。
|
||||||
|
"""
|
||||||
|
if path.suffix == ".rs":
|
||||||
|
return rust_blocks(path)
|
||||||
|
if path.suffix in HASH_SUFFIXES:
|
||||||
|
found = hash_blocks(path)
|
||||||
|
if path.suffix == ".py":
|
||||||
|
found.extend(docstring_blocks(path))
|
||||||
|
return found
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def candidates(base: Path) -> list[Path]:
|
||||||
|
"""Returns the files under a root that carry comments this repository
|
||||||
|
owns. A file passed directly is always included.
|
||||||
|
|
||||||
|
返回某个根目录下携带本仓库拥有注释的文件。直接传入的文件总是包含
|
||||||
|
在内。
|
||||||
|
"""
|
||||||
|
if base.is_file():
|
||||||
|
return [base]
|
||||||
|
files: list[Path] = []
|
||||||
|
for path in sorted(base.rglob("*")):
|
||||||
|
if not path.is_file():
|
||||||
|
continue
|
||||||
|
if any(part in SKIP for part in path.parts):
|
||||||
|
continue
|
||||||
|
if path.suffix == ".rs" or path.suffix in HASH_SUFFIXES:
|
||||||
|
files.append(path)
|
||||||
|
return files
|
||||||
|
|
||||||
|
|
||||||
def main(argv: list[str]) -> int:
|
def main(argv: list[str]) -> int:
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("paths", nargs="*", default=[])
|
parser.add_argument("paths", nargs="*", default=[])
|
||||||
parser.add_argument("--quiet", action="store_true")
|
parser.add_argument("--quiet", action="store_true")
|
||||||
args = parser.parse_args(argv)
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
roots = [Path(p).resolve() for p in args.paths] or [ROOT / "crates"]
|
roots = [Path(p).resolve() for p in args.paths] or [ROOT]
|
||||||
files: list[Path] = []
|
files: list[Path] = []
|
||||||
|
seen: set[Path] = set()
|
||||||
for base in roots:
|
for base in roots:
|
||||||
if base.is_file():
|
for path in candidates(base):
|
||||||
files.append(base)
|
if path not in seen:
|
||||||
else:
|
seen.add(path)
|
||||||
files.extend(sorted(base.rglob("src/**/*.rs")))
|
files.append(path)
|
||||||
|
|
||||||
total = 0
|
total = 0
|
||||||
translated = 0
|
translated = 0
|
||||||
|
|
@ -107,26 +266,23 @@ def main(argv: list[str]) -> int:
|
||||||
mixed: list[str] = []
|
mixed: list[str] = []
|
||||||
|
|
||||||
for path in files:
|
for path in files:
|
||||||
if "examples" in path.parts:
|
for line, block in collect(path):
|
||||||
continue
|
|
||||||
for line, block in blocks(path):
|
|
||||||
text = "\n".join(block)
|
text = "\n".join(block)
|
||||||
if MOJIBAKE in text:
|
if MOJIBAKE in text:
|
||||||
mojibake.append(f"{path.relative_to(ROOT)}:{line}")
|
mojibake.append(f"{path.relative_to(ROOT)}:{line}")
|
||||||
found = MIXED.search(text)
|
found = MIXED.search(text)
|
||||||
if found:
|
if found:
|
||||||
mixed.append(
|
mixed.append(f"{path.relative_to(ROOT)}:{line} {found.group(0)!r}")
|
||||||
f"{path.relative_to(ROOT)}:{line} {found.group(0)!r}"
|
|
||||||
)
|
|
||||||
if is_formula_only(block):
|
if is_formula_only(block):
|
||||||
# Nothing to translate; the symbol is the whole comment.
|
# Nothing to translate; the symbol is the whole comment.
|
||||||
|
# 无可翻译;该符号就是整条注释。
|
||||||
continue
|
continue
|
||||||
total += 1
|
total += 1
|
||||||
if CJK.search(text):
|
if CJK.search(text):
|
||||||
translated += 1
|
translated += 1
|
||||||
else:
|
else:
|
||||||
first = block[0].strip().lstrip("/!").strip()
|
first = strip_marker(block[0])[:64]
|
||||||
gaps.append(f"{path.relative_to(ROOT)}:{line} {first[:64]}")
|
gaps.append(f"{path.relative_to(ROOT)}:{line} {first}")
|
||||||
|
|
||||||
if not args.quiet:
|
if not args.quiet:
|
||||||
for gap in gaps:
|
for gap in gaps:
|
||||||
|
|
|
||||||
|
|
@ -20,21 +20,46 @@
|
||||||
# scripts/check-layers.sh
|
# scripts/check-layers.sh
|
||||||
#
|
#
|
||||||
# Exit status is zero when the rule holds and one otherwise.
|
# Exit status is zero when the rule holds and one otherwise.
|
||||||
|
#
|
||||||
|
# 检查 README.md 所述的分层依赖规则。
|
||||||
|
#
|
||||||
|
# 工作区组织为按依赖排序的各层:
|
||||||
|
#
|
||||||
|
# foundation -> physics -> orbit -> rf -> network -> ops -> sim
|
||||||
|
#
|
||||||
|
# 一个 crate 可以依赖其左侧层中的任何 crate,但绝不依赖其右侧的。本脚本
|
||||||
|
# 读取每个工作区清单,判定每个依赖属于哪一层,并在任何依赖指向右侧时
|
||||||
|
# 失败。
|
||||||
|
#
|
||||||
|
# `apsis-core` 与 `apsis-math` 作为全局前提被豁免:它们位于
|
||||||
|
# `foundation`,每一层都可使用它们,而仅向左的规则本已允许,故它们无需
|
||||||
|
# 特殊处理。
|
||||||
|
#
|
||||||
|
# 用法:
|
||||||
|
#
|
||||||
|
# scripts/check-layers.sh
|
||||||
|
#
|
||||||
|
# 规则成立时退出状态为零,否则为一。
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# The layer order, left to right. A dependency to a layer with a lower index
|
# The layer order, left to right. A dependency to a layer with a lower index
|
||||||
# is allowed; to a higher index is a reverse dependency and fails.
|
# is allowed; to a higher index is a reverse dependency and fails.
|
||||||
|
# 层的次序,从左到右。依赖到下标更低的层是允许的;到更高的下标即反向
|
||||||
|
# 依赖,会失败。
|
||||||
LAYERS=(foundation physics orbit rf network ops sim)
|
LAYERS=(foundation physics orbit rf network ops sim)
|
||||||
|
|
||||||
# The root of the workspace, taken from this script's location so that the
|
# The root of the workspace, taken from this script's location so that the
|
||||||
# check works from any working directory.
|
# check works from any working directory.
|
||||||
|
# 工作区的根,取自本脚本的位置,使检查可在任何工作目录下工作。
|
||||||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
cd "$root"
|
cd "$root"
|
||||||
|
|
||||||
# Returns the index of a layer name, or an empty string when the name is not
|
# Returns the index of a layer name, or an empty string when the name is not
|
||||||
# a layer. A crate outside these directories is not part of the layer scheme
|
# a layer. A crate outside these directories is not part of the layer scheme
|
||||||
# and is skipped rather than reported.
|
# and is skipped rather than reported.
|
||||||
|
# 返回某层名的下标;该名不是层时返回空串。位于这些目录之外的 crate 不属于
|
||||||
|
# 分层方案,会被跳过而非报告。
|
||||||
layer_index() {
|
layer_index() {
|
||||||
local name="$1"
|
local name="$1"
|
||||||
local index=0
|
local index=0
|
||||||
|
|
@ -55,6 +80,7 @@ for manifest in $(find crates -name Cargo.toml | sort); do
|
||||||
directory="$(dirname "$manifest")"
|
directory="$(dirname "$manifest")"
|
||||||
# The layer directory is the second path component, as in
|
# The layer directory is the second path component, as in
|
||||||
# `crates/<layer>/<crate>/Cargo.toml`.
|
# `crates/<layer>/<crate>/Cargo.toml`.
|
||||||
|
# 层目录是路径的第二段,如 `crates/<layer>/<crate>/Cargo.toml`。
|
||||||
layer="$(echo "$directory" | cut -d/ -f2)"
|
layer="$(echo "$directory" | cut -d/ -f2)"
|
||||||
source_index="$(layer_index "$layer")"
|
source_index="$(layer_index "$layer")"
|
||||||
[ -z "$source_index" ] && continue
|
[ -z "$source_index" ] && continue
|
||||||
|
|
@ -62,6 +88,8 @@ for manifest in $(find crates -name Cargo.toml | sort); do
|
||||||
# Every `apsis-...` name in the manifest is a dependency or a package
|
# Every `apsis-...` name in the manifest is a dependency or a package
|
||||||
# name. The package name is the crate itself, which resolves to the same
|
# name. The package name is the crate itself, which resolves to the same
|
||||||
# layer and is skipped by the index comparison below.
|
# layer and is skipped by the index comparison below.
|
||||||
|
# 清单中每个 `apsis-...` 名称要么是依赖,要么是包名。包名即该 crate
|
||||||
|
# 本身,它解析到同一层,会被下面的下标比较跳过。
|
||||||
for dependency in $(grep -oE 'apsis-[a-z-]+' "$manifest" | sort -u); do
|
for dependency in $(grep -oE 'apsis-[a-z-]+' "$manifest" | sort -u); do
|
||||||
dependency_directory="$(find crates -maxdepth 2 -type d -name "$dependency" 2>/dev/null | head -1)"
|
dependency_directory="$(find crates -maxdepth 2 -type d -name "$dependency" 2>/dev/null | head -1)"
|
||||||
[ -z "$dependency_directory" ] && continue
|
[ -z "$dependency_directory" ] && continue
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ python = "3.12.*"
|
||||||
|
|
||||||
# Reference implementations used only for cross-checking Rust output.
|
# Reference implementations used only for cross-checking Rust output.
|
||||||
# These packages are not dependencies of any Rust crate.
|
# These packages are not dependencies of any Rust crate.
|
||||||
|
# 仅用于交叉核对 Rust 输出的参考实现。这些包不是任何 Rust crate 的依赖。
|
||||||
[pypi-dependencies]
|
[pypi-dependencies]
|
||||||
numpy = "*"
|
numpy = "*"
|
||||||
sgp4 = "*"
|
sgp4 = "*"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue