playbook/docs/tsl/code_style.md

271 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# TSL 代码风格Code Style
本章节规定 TSL 代码的结构与格式约定。
相关文档:
- 命名规范:`docs/tsl/naming.md`
- 工具链与验证命令(模板):`docs/tsl/toolchain.md`
## 1. 文件与组织
### 1.1 单一职责
- 一个文件只做一件事;职责明确。
- `.tsl` 建议作为“入口/编排层”:聚合参数/配置、串起流程;可复用逻辑下沉到 `.tsf``unit`/`class`/`function`)中。
- 当一个顶层声明同时承担“协议适配 + 业务计算 + I/O/环境依赖 + 临时代码”时,优先拆分边界:核心纯逻辑 → 工具函数 → 边界适配I/O
### 1.2 文件名与顶层声明(硬约束)
- TSL 语法要求(仅 `.tsf`):每个 `.tsf` 文件只能有一个顶层声明,且文件基名必须与顶层声明同名。
- `.tsl` 允许直接写语句,不要求顶层声明;文件名不强制,但建议清晰可检索。
- 推荐文件名使用 `PascalCase` 以提升检索与协作一致性;扩展名按类型使用 `.tsl`/`.tsf`(两者都属于 TSL 源文件,风格规则一致)。
- 详细约束与命名细则见 `docs/tsl/naming.md`
### 1.3 依赖与分层
- 避免循环依赖;依赖方向应从“入口/业务层”指向“通用/底层模块”。
- 公共能力(类型/常量/纯函数)下沉到可复用模块;不要在多个文件里复制粘贴同一段工具逻辑。
- 发现环依赖时的默认处理顺序:
1. 提取共享部分到更底层的 `*Common`/`*Shared` `unit`
2. 通过参数/回调注入反转依赖(让底层不再直接引用上层)。
3. 必要时引入更明确的边界文件(例如 adapter 层),把依赖集中在边界处。
### 1.4 推荐布局(读者视角)
- 同类代码按“对外 API → 核心实现 → 辅助工具 → 测试/示例”的顺序组织。
- 对外 API尽量靠前读者先看到“怎么用”实现细节与 helper 放到后面(例如 `unit``interface`、`class` 的 `public`)。
- 对外声明尽量“收口”:
- `unit``interface` 只放对外 `const/type/function` 声明与必要注释;实现细节与 helper 放在后部。
- `class` 的对外 API 放在 `public`;内部状态与实现细节放在 `private`
- 测试/示例:优先独立文件/目录,避免夹在核心实现中间(减少无关 diff 干扰 review
## 2. 格式Formatting
### 2.1 缩进与空白
- 使用**空格缩进**,禁止 Tab。
- 默认缩进 **4 个空格**;继续缩进保持与上层语义一致。
- 行尾不留空格;文件以换行符结尾。
- 逻辑块之间用空行分隔,不要用空行堆砌。
### 2.2 行宽与换行
- 单行建议不超过 **100 字符**;超过时应换行以保持可读性。
- 换行遵循“**断在运算符后**、对齐到语义层级”的原则。
- 长字符串/URL 可适当超出,但避免影响阅读。
### 2.3 begin/end 与代码块
- 代码块使用统一的块结构(示例按常见 TSL 写法;若项目语法/约定有差异,以项目现有代码为准):
```tsl
if cond then
begin
DoSomething()
end
else
begin
DoOther()
end
```
- 多语句分支使用 `begin/end` 包裹:在 `then/else` 后换行写 `begin``end` 单独成行。
- `else/elseif` 等分支关键字另起一行,与上一块的 `end` 对齐。
- 单语句分支可省略 `begin/end`(保持清晰优先;一旦分支变复杂就回退到块结构):
```tsl
if cond then DoSomething()
else DoOther()
```
### 2.4 运算符与分隔符
- 二元运算符两侧加空格:`a + b`、`x == y`。
- 一元运算符不加空格:`!flag`、`-value`。
- 逗号后加空格:`f(a, b, c)`。
- 不要为了对齐而插入多余空格;让格式由缩进表达结构。
### 2.5 控制流
- 多语句分支必须使用 `begin/end`;单语句分支可省略 `begin/end`,写成单行(如 `if cond then stmt`)。
- 复杂条件拆分为具名布尔变量或小函数。
- 早返回优于深层嵌套:
```tsl
if !ok then return err
// main path
```
## 3. 注释Comments
注释用于解释**为什么**以及必要的背景,而不是重复代码。
### 3.0 注释形式与语言
- 支持的注释形式:
- 行注释:`// ...`(默认优先使用)
- 块注释:`{ ... }`
- 块注释:`/* ... */`
- 注释语言:跟随文件,可中英混写;同一文件内尽量保持一致的表达风格。
- 注释中不要写入明文密钥/Token/密码等敏感信息;示例使用占位符(如 `<TOKEN>`)。
### 3.1 文件级注释
- 文件开头说明用途、主要职责、关键依赖/约束。
- 若文件实现某个对外 API写明入口与预期行为。
### 3.2 函数/接口注释
- “对外可见”的定义:
- `unit``interface` 区域中的声明(对外 API
- `class``public` 区域的方法/property对外 API
- 顶层 `function`:该函数本身(对外入口)
- 对外可见的函数必须写注释,包含:
- 做什么(行为)
- 入参/返回值含义(必要时含单位、范围)
- 关键副作用与异常情况
- 注释使用完整句子,末尾带标点。
- 推荐模板(按需裁剪;语言可中英混写):
```tsl
// Summary: 一句话说明做什么(以及关键约束/边界)。
// Args:
// - foo: 含义(单位/范围/约束)。
// Returns: 返回值语义(以及错误/空值含义,如适用)。
// Side effects: 关键副作用I/O/全局状态/缓存/日志等)。
// Errors: 失败条件与处理方式(返回/抛出/降级)。
```
### 3.3 行内注释
- 用于解释复杂逻辑、非直观边界条件、性能/安全考量。
- 避免“显而易见注释”:
- 尾随注释(写在代码行末)只用于非常短的补充;超过一行时改为写在语句上方,或重构代码提醒意图。
```tsl
count = count + 1 // bad: obvious
```
### 3.4 TODO/FIXME
- 统一格式:`TODO(name): ...` / `FIXME(name): ...`
- 写清原因和期望修复方向,而非“留个坑”。
- `name` 使用 Git 用户名;不强制附 issue/ticket如有可追加在描述中
## 4. 代码实践Best Practices
> 本节偏“实践建议”should用于提升可读性/可测试性;若目标项目有更严格的约束与检查命令,以项目落地的工具链为准(参考 `docs/tsl/toolchain.md`)。如需给自动化/AI 代理配置强约束,可参考 `.agents/tsl/code_quality.md` 与 `.agents/tsl/testing.md`。
### 4.1 变量与常量
- 默认使用不可变/只读:能用 `const` 就用 `const`;可变状态尽量压到最小作用域,并让“更新点”集中且明显。
- 对外 API 优先只读:对外暴露用只读 property只有 `read`,不写 `write`),内部用私有成员保存。
```tsl
type User = class
public
property UserId read user_id_; // readonly
private
user_id_;
end;
```
- 变量声明与第一次使用尽量靠近。
- 避免隐式类型转换TSL 为动态类型,但运行时仍有类型与单位;外部输入(参数/配置/文件/接口)应在边界处显式解析与校验,再进入核心逻辑。
- 避免隐式全局:函数尽量只依赖显式入参;若必须使用顶层全局/静态可变变量,必须在声明处写注释说明:它是什么、用于什么、以及(如不明显)为什么需要是全局/静态。
### 4.2 函数设计
- 签名尽量自解释:对外 API 的参数/返回值建议显式写类型注解;并用注释写清契约(可复用 3.2 的模板)。
- 类型注解不支持 `xxx.xxx` 形式;使用单一类型名。
- 无返回值函数显式标注返回类型为 `void``create`/`destroy` 作为构造/析构函数不写返回类型。
```tsl
function Func(a: string; b: ClassName): void;
```
- 参数默认可读写(引用语义);输入参数如果不应被修改,优先使用 `const` 修饰符让意图与约束更明确。
- 单一职责:函数过长说明拆分点已出现(建议 ≤ 4060 行把“纯计算”与“I/O/环境依赖(文件/网络/数据库/全局状态)”分离,降低耦合、便于测试。
- 参数组织与顺序:
- 输入参数在前;可选配置/选项(如 `*Options`/`*Config`)居中;输出/回调在后。
- 避免堆叠多个布尔开关参数;优先收敛到 `*Options`/`*Config`(按需在 `class``unit` 中定义)。
- 示例:避免多个布尔开关参数(调用点难以理解 `true/false` 的含义),改为 `*Options`/`*Config`
```tsl
// 注:参数类型名按项目实际替换(此处 bool/Any 仅为示例占位)。
// bad: 多个 bool 参数在调用点难读、易传错
function ExportReport(
path: string;
data: Any;
include_header: bool;
compress: bool;
dry_run: bool
): void;
// good: 将可选开关收敛到 Options调用点更自解释、后续扩展更稳定
type ExportOptions = class
public
property IncludeHeader read include_header_ write include_header_;
property Compress read compress_ write compress_;
property DryRun read dry_run_ write dry_run_;
private
include_header_;
compress_;
dry_run_;
end;
function ExportReport(path: string; data: Any; options: ExportOptions): void;
```
- 尽量避免超过 5 个参数;必要时封装为对象(`class`/`unit`)。
### 4.3 错误处理
- 错误必须显式处理:返回失败/错误、抛出异常、或记录并降级best-effort。禁止“看起来成功了但其实失败了”的隐式路径。
- **不设默认策略**:按场景选择返回/抛出/降级,并在对外注释里写清契约(参考 3.2 模板的 `Errors`)。
- **返回失败/错误**:调用方有能力恢复/重试/改参数时(参数不合法、外部输入解析失败、依赖不可用等)。
- **抛出异常**:不应发生的内部错误/不变量被破坏,继续执行风险更大时。
- **记录并降级**:功能可选、失败不影响主流程时(例如缓存读取失败 → 当作 cache miss必须在代码旁注释说明“为什么允许”。
- 不要吞掉异常/错误:`try/except` 之后如果继续执行,必须有明确替代行为(返回/重试/降级)以及理由;否则应将错误继续向上抛出或返回。
- 错误信息与日志(允许在库里打日志,但要克制):
- 错误/日志至少包含:**做什么失败** + **关键上下文(脱敏)**便于定位避免只有“failed”。
- 禁止把 Token/密码/个人数据等敏感信息写入日志、注释或错误信息(参考 `.agents/tsl/auth.md`)。
- 避免重复记录:同一个错误链路尽量只在**边界层**记录一次(库里记录后,上层通常不再重复打一遍同等级日志)。
- 示例:`try/except/end` + 降级best-effort
- 注:示例中的 `Any`/`nil`/`LogWarn`/`ReadCacheFromFile` 为占位,按项目实际类型与函数替换。
```tsl
// 读取可选缓存:失败允许降级为 cache miss必须可观测并说明原因
function ReadOptionalCache(path: string): Any;
begin
try
return ReadCacheFromFile(path)
except
// best-effort: cache 仅用于提速,失败不应影响主流程
LogWarn("ReadOptionalCache failed; fallback to miss. path=" + path)
return nil
end
end;
```
### 4.4 性能与可测试性
- 避免过早优化先写清晰正确的代码再用数据profile/trace/log/基准)定位瓶颈并做最小化改动(参考 `.agents/tsl/performance.md`)。
- 复杂逻辑要可测试:把“纯计算/解析/规则”与“I/O/环境依赖(文件/网络/DB/全局状态”分离I/O 层做薄封装,核心逻辑保持可单测(参考 `.agents/tsl/testing.md`)。
- 避免在热路径里做隐式昂贵操作:循环内重复 I/O、重复解析/格式化、无界缓存、隐式复制等;缓存如必须引入,明确生命周期与上限(大小/TTL/清理点)。
- 示例:薄 I/O + 厚纯逻辑(便于测试与复用):
- 注:示例中的 `Any`/`ReadAllText` 为占位,按项目实际类型与函数替换。
```tsl
// pure: 只做解析/校验,不做 I/O便于单元测试
function ParseConfig(text: string): Any;
// I/O: 只负责读文件与兜底处理,把逻辑交给 ParseConfig
function LoadConfig(path: string): Any;
begin
text = ReadAllText(path)
return ParseConfig(text)
end;
```