+86
-101
@@ -24,22 +24,19 @@
|
||||
- [function](#function-2)
|
||||
- [class](#class-2)
|
||||
- [unit](#unit-2)
|
||||
- [录入格式(用户必看)](#录入格式用户必看)
|
||||
- [录入文件格式](#录入文件格式)
|
||||
- [yaml](#yaml)
|
||||
- [json](#json)
|
||||
- [统一 API 索引](#统一-api-索引)
|
||||
|
||||
## 基本原则
|
||||
|
||||
- markdown 是唯一存储源
|
||||
- yaml/json 用于生成 markdown
|
||||
- tsf 源码文档可以转换为 json 或 yaml 录入稿,再由同一生成流程产出 markdown
|
||||
- 发布的 API 文档使用本标准定义的 markdown 存储格式
|
||||
- tsf 源码文档、yaml/json 录入数据与 markdown 的字段映射以本标准为准
|
||||
- 一个录入文件对应一个 markdown 叶子页;录入根固定为
|
||||
`module`、`path`、`declarations`
|
||||
- `declarations` 是非空有序列表,function、class、unit 可以按输入顺序混合
|
||||
- 生成器写入 markdown 前必须使用仓库锁定版本的 Prettier 统一格式
|
||||
- 转换器允许生成描述或类型不完整的草稿;生成器严格校验,不发布不完整录入稿
|
||||
- function 调用签名由维护者或转换器提供;工具不推断 API 语义
|
||||
- `declarations` 是非空有序列表,function、class、unit 可以混合;列表顺序即文档顺序
|
||||
- function 调用签名只记录显式名称和参数名,不包含类型、默认值或语义推断内容
|
||||
- API 标题的反引号内只写名称或调用签名,不写 function、class、property 等声明术语
|
||||
- 每个 API 用独立的 `声明:...` 行记录声明种类
|
||||
- 描述紧跟声明行;tags 是描述的检索补充,位于完整描述之后
|
||||
@@ -56,11 +53,10 @@ function、class、unit 都是同级的顶级声明,可以出现在同一页
|
||||
种类写在自身正文的 `声明:function|class|unit` 行中。
|
||||
|
||||
目录承担领域或模块分组,叶子页承担 API 主题分组,H2 承担顶级声明分组。markdown
|
||||
不要求一个 class 对应一个 md;多个独立 class TSF 的对外声明可以进入同一叶子页,
|
||||
也可以在内容过长或主题不同时拆到同一目录下的多个叶子页。例如
|
||||
不要求一个 class 对应一个 md;多个独立 class 声明可以进入同一叶子页,也可以在
|
||||
内容过长或主题不同时拆到同一目录下的多个叶子页。例如
|
||||
`OpenXmlAttribute` 和 `OpenXmlElement` 可以作为两个 H2 同处
|
||||
`officexml/openxml/elements.md`。每个 TSF 文件对应一个对外顶级声明,一个 markdown
|
||||
叶子页可以收录多个对外顶级声明。
|
||||
`officexml/openxml/elements.md`。一个 markdown 叶子页可以收录多个对外顶级声明。
|
||||
|
||||
顶级声明和成员都按文档顺序存储,不按名称、种类或可见性重新排序。每个顶级声明
|
||||
按以下共同顺序开始:
|
||||
@@ -161,7 +157,7 @@ function 重载和跨声明种类同名允许。
|
||||
|
||||
#### 示例代码
|
||||
|
||||
- 每个示例以“范例NN:说明”开头,编号按出现顺序自动生成并至少保留两位
|
||||
- 每个示例以“范例NN:说明”开头,编号按出现顺序排列并至少保留两位
|
||||
- 代码围栏使用 `tsl`
|
||||
- 一个代码围栏只放一个独立示例
|
||||
- 字符串使用直引号 `'` 或 `"`
|
||||
@@ -247,7 +243,7 @@ return demoFn(src, 0);
|
||||
3. class 描述,必填
|
||||
4. tags,可选;位于完整描述之后
|
||||
5. `父类:BaseClass`,可选;多父类按声明顺序列出
|
||||
6. class 成员,按源码顺序书写
|
||||
6. class 成员,按声明顺序书写
|
||||
|
||||
#### 成员标题与正文
|
||||
|
||||
@@ -411,7 +407,7 @@ unit function 的参数取值/示例使用 H4,interface class function 的参
|
||||
2. `声明:unit`
|
||||
3. unit 描述,必填
|
||||
4. tags,可选;位于完整描述之后
|
||||
5. interface direct member,按源码顺序书写
|
||||
5. interface direct member,按声明顺序书写
|
||||
|
||||
#### 成员标题与正文
|
||||
|
||||
@@ -423,8 +419,8 @@ H3 direct member 标题只写名称或调用签名。标题后的声明行只允
|
||||
- interface class:`声明:class`
|
||||
|
||||
每个 direct member 都按“标题、声明行、描述、tags、成员数据”的共同顺序开始。
|
||||
unit direct member 都来自 interface,因此正文不重复写 public;统一索引把其
|
||||
visibility 规范化为 `public`。unit function 复用顶级 function 格式并要求返回
|
||||
unit direct member 都来自 interface,visibility 固定为 `public`,正文不重复记录。
|
||||
unit function 复用顶级 function 格式并要求返回
|
||||
类型,var 必须写类型,const 必须写值。interface class 的 H4 成员复用顶级
|
||||
class 的成员格式,并显式写 `可见性:public|protected`。
|
||||
|
||||
@@ -490,7 +486,7 @@ class 的成员格式,并显式写 `可见性:public|protected`。
|
||||
|
||||
### 综合示例
|
||||
|
||||
同一叶子页可以按录入顺序混合三种顶级声明。以下 API 仅用于说明存储格式,不代表
|
||||
同一叶子页可以按文档顺序混合三种顶级声明。以下 API 仅用于说明存储格式,不代表
|
||||
真实 TSL API:
|
||||
|
||||
````markdown
|
||||
@@ -571,30 +567,29 @@ return ParseOpenXml('<root/>');
|
||||
## tsf 源码文档格式
|
||||
|
||||
tsf 主要使用 `function`、`unit` 和 `type` 三种顶层组织方式。本标准按这三种方式
|
||||
分别定义源码文档格式。转换器可以一次接收任意混合的 TSF 输入;每个文件贡献一个
|
||||
对外顶级声明,并严格保持命令行输入顺序。
|
||||
分别定义源码文档格式。每个 tsf 源码文档对应一个对外顶级声明。
|
||||
|
||||
### function
|
||||
|
||||
独立顶层 `function` 可以在源码中记录录入数据所需的函数级内容。签名、参数类型、
|
||||
默认参数和返回类型直接读取源码声明。
|
||||
独立顶层 `function` 可以在源码中记录函数级文档。签名、参数类型、默认参数和返回
|
||||
类型由源码声明提供,描述、标签、参数说明、枚举值和示例由文档块提供。
|
||||
|
||||
- 一个 tsf 文件只记录第一个主函数;文件中的后续辅助函数不进入录入数据
|
||||
- 顶层 `procedure` 不按 `function` 格式处理
|
||||
- 多个 tsf 可以与 class/unit TSF 混合生成同一个 json/yaml 录入文件;页面级
|
||||
`module` 和 `path` 在转换时统一提供,不写进单个函数的注释
|
||||
- 一个 function tsf 文件只以第一个顶层 function 作为对外主函数;后续辅助函数不属于
|
||||
该文件的对外文档
|
||||
- 顶层 `procedure` 不在本节规定的 function 文档格式范围内
|
||||
- `module` 和 `path` 是页面级录入字段,不属于单个函数的文档块
|
||||
|
||||
#### 文档块位置
|
||||
|
||||
文档块必须是主函数 `begin` 之后的第一段非空内容,并且位于任何可执行语句、
|
||||
编译指令或其他注释之前
|
||||
|
||||
文档块由连续的 `///` 行组成。允许按照函数体缩进;解析时忽略 `///` 之前的空白,
|
||||
并移除 `///` 及其后的一个可选空格。遇到第一行非 `///` 内容时,文档块结束;函数体
|
||||
后续位置的注释是普通注释
|
||||
文档块由连续的 `///` 行组成,并允许按照函数体缩进。`///` 之前的空白以及其后的
|
||||
一个可选空格不属于文档内容。遇到第一行非 `///` 内容时,文档块结束;函数体后续
|
||||
位置的注释是普通注释
|
||||
|
||||
TSL 解释器将 `///` 作为普通的 `//` 行注释;第三个 `/` 是 codegen 用来识别文档行的
|
||||
标记
|
||||
TSL 解释器将 `///` 作为普通的 `//` 行注释;本标准使用第三个 `/` 区分文档行与普通
|
||||
注释
|
||||
|
||||
#### 文档块结构
|
||||
|
||||
@@ -609,7 +604,7 @@ TSL 解释器将 `///` 作为普通的 `//` 行注释;第三个 `/` 是 codege
|
||||
|
||||
空的 `///` 行可以在多行函数描述或示例中保留空行。指令名固定为小写;未知指令、
|
||||
重复的 `@tags:`/`@returns:`、同一参数重复的 `@param:`/`@values:`,以及不符合上述
|
||||
顺序的指令均视为错误。`@example:` 可以重复,`@output:` 在同一示例组内最多出现
|
||||
顺序的指令均不符合本标准。`@example:` 可以重复,`@output:` 在同一示例组内最多出现
|
||||
一次。
|
||||
|
||||
| 写法 | 必填 | json 映射 | 规则 |
|
||||
@@ -623,36 +618,35 @@ TSL 解释器将 `///` 作为普通的 `//` 行注释;第三个 `/` 是 codege
|
||||
| 示例代码 | 示例组内 | `declarations[].examples[].code` | 内容行额外缩进两个空格;至少包含一个非空代码行 |
|
||||
| `@output:` | 否 | `declarations[].examples[].output` | 原始输出额外缩进两个空格;存在时不得为空 |
|
||||
|
||||
`@param:` 和 `@values:` 中的参数名与函数声明大小写无关地匹配,json 使用函数声明中的
|
||||
参数拼写。
|
||||
`@param:` 和 `@values:` 中的参数名与函数声明大小写无关地匹配;映射后的参数名保留
|
||||
函数声明中的拼写。
|
||||
|
||||
#### 字段来源与映射
|
||||
|
||||
以下字段由主函数声明以及必要的文档指令确定:
|
||||
|
||||
| tsf 声明内容 | json 字段 | 转换规则 |
|
||||
| -------------- | ---------------------------------- | --------------------------------------------------- |
|
||||
| 函数名 | `declarations[].name` | 保留声明名称 |
|
||||
| 函数名和参数名 | `declarations[].signature` | 规范化为只含名称的调用形式,不复制类型或默认值 |
|
||||
| 参数类型 | `declarations[].params[].type` | 保留声明中的类型 |
|
||||
| 参数默认值 | `declarations[].params[].optional` | 存在默认值时写入 `true`,默认表达式不另建 json 字段 |
|
||||
| 返回类型 | `declarations[].returns` | 读取声明和 `@returns:`,按下述规则合并 |
|
||||
| tsf 声明内容 | json 字段 | 映射规则 |
|
||||
| -------------- | ---------------------------------- | ------------------------------------------------- |
|
||||
| 函数名 | `declarations[].name` | 保留声明名称 |
|
||||
| 函数名和参数名 | `declarations[].signature` | 规范化为只含名称的调用形式,不复制类型或默认值 |
|
||||
| 参数类型 | `declarations[].params[].type` | 保留声明中的类型 |
|
||||
| 参数默认值 | `declarations[].params[].optional` | 存在默认值时为 `true`,默认表达式不另建 json 字段 |
|
||||
| 返回类型 | `declarations[].returns` | 取自声明和 `@returns:`,按下述规则合并 |
|
||||
|
||||
要直接得到可生成 markdown 的完整录入数据,函数必须显式声明每个参数的类型,为每个
|
||||
参数提供非空的 `@param:`,并通过函数声明或 `@returns:` 提供返回类型。缺少这些内容
|
||||
时只能得到待手工完善的录入稿。
|
||||
完整录入数据必须包含每个参数的显式类型和非空说明,并通过函数声明或 `@returns:`
|
||||
提供返回类型。
|
||||
可选参数的说明仍应写清默认值含义;`optional: true` 只表达该参数可以省略。
|
||||
|
||||
返回类型按以下规则合并:
|
||||
|
||||
- 只有函数声明时,使用声明中的拼写
|
||||
- 只有 `@returns:` 时,去除首尾空白后使用指令中的拼写
|
||||
- 两处同时存在时,转换器使用与函数声明相同的词法规则拆分类型,忽略 token 之间的
|
||||
空白,并按 TSL 标识符大小写无关的规则比较标识符 token;其他 token 必须一致。
|
||||
校验通过后使用声明中的拼写,校验失败则定位 `@returns:` 行、报错并停止转换
|
||||
- 两处都不存在时,录入稿中的 `returns` 为空,不能直接生成 markdown
|
||||
- 两处同时存在时,使用相同的词法规则拆分类型,忽略 token 之间的空白,并按 TSL
|
||||
标识符大小写无关的规则比较标识符 token;其他 token 必须一致。两处类型必须匹配,
|
||||
映射后使用声明中的拼写
|
||||
- 两处都不存在时,不满足顶级 function 的 `returns` 必填要求
|
||||
|
||||
转换器只校验类型的词法结构,不判断类型别名等语义等价。
|
||||
本标准只定义类型的词法一致性,不定义类型别名等语义等价关系。
|
||||
|
||||
#### 枚举值表
|
||||
|
||||
@@ -673,10 +667,10 @@ TSL 解释器将 `///` 作为普通的 `//` 行注释;第三个 `/` 是 codege
|
||||
- 不接受数组、对象或 json `null`
|
||||
- 值和说明以值后的第一个分隔冒号分开;说明不得为空
|
||||
- 保留枚举项的声明顺序
|
||||
- 同一参数最多有一个非空值表,重复值视为错误;不同 json 类型的值不视为重复,
|
||||
- 同一参数最多有一个非空值表,不允许重复值;不同 json 类型的值不视为重复,
|
||||
例如数字 `1` 与字符串 `"1"` 是两个值
|
||||
- `@values:` 引用的参数必须存在
|
||||
- 转换器不推断枚举值是否与参数类型兼容,该语义由 tsf 作者负责
|
||||
- tsf 作者必须保证枚举值与参数类型在语义上兼容
|
||||
|
||||
#### 示例组
|
||||
|
||||
@@ -685,20 +679,20 @@ TSL 解释器将 `///` 作为普通的 `//` 行注释;第三个 `/` 是 codege
|
||||
`@example:` 开始新的示例组。第一个示例组出现后,不得再写参数、返回类型等其他函数
|
||||
级指令。
|
||||
|
||||
转换时移除代码和输出的两个结构缩进,保留其余空白和换行。示例说明、代码和输出
|
||||
分别映射为 `examples[].desc`、`examples[].code` 和 `examples[].output`。`desc` 与
|
||||
映射到录入数据时,移除代码和输出的两个结构缩进,保留其余空白和换行。示例说明、
|
||||
代码和输出分别映射为 `examples[].desc`、`examples[].code` 和 `examples[].output`。`desc` 与
|
||||
`code` 必填且非空;`output` 可选,但出现时必须包含至少一个非空行。示例顺序保持
|
||||
不变。
|
||||
|
||||
`code` 只记录示例源码,不得包含标准输出标记 `// 输出:`。`output` 只记录原始输出,
|
||||
不写 `//` 注释标记。生成 markdown 时:
|
||||
不写 `//` 注释标记。映射到 markdown 时:
|
||||
|
||||
- 所有示例共用一个 `### 示例` 标题
|
||||
- 每项按顺序生成“范例01:说明”“范例02:说明”
|
||||
- 每项生成一个独立的 `tsl` 代码块
|
||||
- 每项按顺序写为“范例01:说明”“范例02:说明”
|
||||
- 每项使用一个独立的 `tsl` 代码块
|
||||
- 没有 `output` 时,代码块只包含 `code`
|
||||
- 单行 `output` 在代码末尾生成 `// 输出:<值>`
|
||||
- 多行 `output` 先生成 `// 输出:`,再为每个输出行添加 `//` 和一个空格;空输出行生成
|
||||
- 单行 `output` 在代码末尾写为 `// 输出:<值>`
|
||||
- 多行 `output` 先写 `// 输出:`,再为每个输出行添加 `//` 和一个空格;空输出行使用
|
||||
单独的 `//`
|
||||
|
||||
#### 完整 tsf 示例
|
||||
@@ -950,33 +944,32 @@ end;
|
||||
|
||||
### unit
|
||||
|
||||
unit 只接受包含显式 `interface`、`implementation` 并以 `end.` 结束的完整形态。
|
||||
unit 名称必须与文件名大小写无关地一致;简写 unit 明确报错。
|
||||
本标准的 unit 源码文档采用包含显式 `interface`、`implementation` 并以 `end.` 结束的
|
||||
完整形态;简写 unit 不在本标准范围内。unit 名称必须与文件名大小写无关地一致。
|
||||
|
||||
unit 文档块位于 `unit Name;` 之后、`interface` 之前,只允许描述和可选
|
||||
`@tags:`。转换器按源码顺序收录 interface 中的 function、var、const 和 class;
|
||||
`uses` 只表示依赖,不进入 API。interface class 完整复用独立 class 的成员规则,
|
||||
但 interface 中的所有 class 都进入文档。受支持区域中无法绑定到 unit 或 interface
|
||||
成员的 `///` 文档块按原始行号报错。
|
||||
`@tags:`。对外文档按声明顺序包含 interface 中的 function、var、const 和 class;
|
||||
`uses` 只表示依赖,不属于 API。interface class 完整复用独立 class 的成员规则,
|
||||
interface 中的所有 class 均属于对外文档。每个 `///` 文档块必须绑定到 unit 或
|
||||
interface 成员。
|
||||
|
||||
进入 implementation 后停止收集 API;其中的函数、类、变量、常量和 `///` 文档块
|
||||
全部忽略。interface 中的 procedure 和非 class type 不支持,必须在声明行报错,
|
||||
不能静默遗漏。interface function 复用 function 文档指令并要求最终返回类型;
|
||||
var/const 只允许描述和可选 `@tags:`,且一项一条声明。
|
||||
implementation 中的函数、类、变量、常量和 `///` 文档块不属于对外文档。interface
|
||||
中的 procedure 和非 class type 不在本标准范围内。interface function 复用 function
|
||||
文档指令并要求返回类型;var/const 只允许描述和可选 `@tags:`,且一项一条声明。
|
||||
|
||||
## 录入数据结构
|
||||
|
||||
一个录入文件对应一个 markdown 叶子页。录入根只允许 `module`、`path`、
|
||||
`declarations`。`declarations` 必须是非空有序列表,function、class、unit 可以
|
||||
任意混合,生成器严格保持数组顺序。
|
||||
任意混合;数组顺序即 markdown 中的 H2 声明顺序。
|
||||
|
||||
顶层字段:
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
| -------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `module` | 是 | markdown 一级标题内容;不是目录名或文件名。例如 `module: 示例 / 数组` 生成 `# 示例 / 数组` |
|
||||
| `path` | 是 | 目标 markdown 在 scope 目录下的相对路径,包含子目录和文件名,使用 `/` 分隔且不含 `.md` 后缀。例如 `path: base/example` 在默认 `project` scope 下生成 `references/codegen/project/base/example.md` |
|
||||
| `declarations` | 是 | 非空顶级声明列表;每项由 `kind` 判别并要求 `name`,按数组顺序生成 H2 |
|
||||
| 字段 | 必填 | 说明 |
|
||||
| -------------- | ---- | -------------------------------------------------------------------------------------------------------------------- |
|
||||
| `module` | 是 | markdown 一级标题内容;不是目录名或文件名。例如 `module: 示例 / 数组` 对应 `# 示例 / 数组` |
|
||||
| `path` | 是 | markdown 在所属 scope 下的相对路径,使用 `/` 分隔且不含 `.md` 后缀。例如 `path: base/example` 对应 `base/example.md` |
|
||||
| `declarations` | 是 | 非空顶级声明列表;每项由 `kind` 判别并要求 `name`,数组顺序即 H2 顺序 |
|
||||
|
||||
### function
|
||||
|
||||
@@ -986,22 +979,22 @@ var/const 只允许描述和可选 `@tags:`,且一项一条声明。
|
||||
| ----------- | ------ | ------------------------------------------------------ |
|
||||
| `kind` | 是 | 固定为 `function` |
|
||||
| `name` | 是 | function 简单名称;必须与 `signature` 中的名称一致 |
|
||||
| `signature` | 是 | 维护者提供的调用签名 |
|
||||
| `signature` | 是 | 只包含名称和参数名的调用签名 |
|
||||
| `desc` | 是 | 函数描述,可包含多行 |
|
||||
| `tags` | 否 | 检索关键词列表;推荐填写,有助于更准确地识别和检索函数 |
|
||||
| `params` | 有参时 | 参数列表;无参函数省略 |
|
||||
| `returns` | 是 | 返回类型 |
|
||||
| `examples` | 否 | 示例列表;按顺序生成独立的 TSL 代码块 |
|
||||
| `examples` | 否 | 示例列表;按顺序映射为独立的 TSL 代码块 |
|
||||
|
||||
参数字段:
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
| ---------- | ---- | ------------------------------- |
|
||||
| `name` | 是 | 参数名,与签名一致 |
|
||||
| `type` | 是 | 参数类型 |
|
||||
| `desc` | 是 | 参数说明 |
|
||||
| `optional` | 否 | `true` 时自动添加 `可选。` 前缀 |
|
||||
| `values` | 否 | 枚举值列表,生成参数取值说明 |
|
||||
| 字段 | 必填 | 说明 |
|
||||
| ---------- | ---- | ------------------------------------------------------ |
|
||||
| `name` | 是 | 参数名,与签名一致 |
|
||||
| `type` | 是 | 参数类型 |
|
||||
| `desc` | 是 | 参数说明 |
|
||||
| `optional` | 否 | `true` 表示可选参数;markdown 参数说明以 `可选。` 开头 |
|
||||
| `values` | 否 | 枚举值列表,对应参数取值说明 |
|
||||
|
||||
`values` 每项包含:
|
||||
|
||||
@@ -1012,11 +1005,11 @@ var/const 只允许描述和可选 `@tags:`,且一项一条声明。
|
||||
|
||||
`examples` 每项包含:
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
| -------- | ---- | ---------------------------------------------------- |
|
||||
| `desc` | 是 | 示例场景说明;生成“范例NN:说明” |
|
||||
| `code` | 是 | 不含代码围栏和标准输出注释的 TSL 代码 |
|
||||
| `output` | 否 | 原始输出;生成器按单行或多行规则转换为 `//` 输出注释 |
|
||||
| 字段 | 必填 | 说明 |
|
||||
| -------- | ---- | ---------------------------------------------- |
|
||||
| `desc` | 是 | 示例场景说明;对应“范例NN:说明” |
|
||||
| `code` | 是 | 不含代码围栏和标准输出注释的 TSL 代码 |
|
||||
| `output` | 否 | 原始输出;按单行或多行规则映射为 `//` 输出注释 |
|
||||
|
||||
### class
|
||||
|
||||
@@ -1045,8 +1038,8 @@ class 对象字段:
|
||||
`kind: method` 对象不使用 `static` 字段;class function 由 `binding: class` 唯一
|
||||
表达。`value` 的存在性与真假值分开判断,因此数字 `0` 和布尔值 `false` 都是合法
|
||||
常量值。
|
||||
这里的 `method` 是录入结构内部用于归一化实例 function 和 class function 的 kind,
|
||||
不会写进 markdown 标题或 `声明:...` 行。
|
||||
这里的 `method` 是录入结构内部用于统一表达实例 function 和 class function 的 kind,
|
||||
不对应 markdown 标题或 `声明:...` 行中的声明值。
|
||||
|
||||
### unit
|
||||
|
||||
@@ -1062,13 +1055,11 @@ class 对象字段:
|
||||
|
||||
录入数据不保存 unit 的 `uses`、implementation、initialization 或 finalization。
|
||||
|
||||
## 录入格式(用户必看)
|
||||
## 录入文件格式
|
||||
|
||||
### yaml
|
||||
|
||||
yaml 适合包含多行示例的页面。解析 yaml 需要安装 `pyyaml`
|
||||
|
||||
注意:
|
||||
yaml 录入文件应符合以下规则:
|
||||
|
||||
- `examples[].code` 和多行 `examples[].output` 使用 `|` 块标量
|
||||
- 参数名 `...` 必须加引号
|
||||
@@ -1078,9 +1069,7 @@ yaml 适合包含多行示例的页面。解析 yaml 需要安装 `pyyaml`
|
||||
|
||||
### json
|
||||
|
||||
json 使用 Python 标准库解析,无额外依赖
|
||||
|
||||
注意:
|
||||
json 录入文件应符合以下规则:
|
||||
|
||||
- json 不支持注释
|
||||
- 多行 `examples[].code` 和 `examples[].output` 使用 `\n`
|
||||
@@ -1130,7 +1119,3 @@ name scope module signature page anchor tags summary kind binding visibility own
|
||||
例如 class function `Widget.Create` 的 owner 是 `Widget`;unit interface class
|
||||
function `DemoUnit.Document.Save` 的 owner 是 `DemoUnit.Document`。重载共享
|
||||
`qualified_name`,由 `signature` 和唯一的 `page#anchor` 区分。
|
||||
|
||||
lookup 的 `--name` 同时精确匹配简单名称与 `qualified_name`,比较大小写不敏感。
|
||||
简单成员名会返回所有 owner 下的同名 API;完全限定名称用于缩小到指定 class/unit。
|
||||
`--kw` 还会搜索 kind、binding、visibility、owner 和 qualified_name。
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -237,7 +237,7 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
self.read_declaration()["params"],
|
||||
)
|
||||
|
||||
def test_parameter_type_mismatch_reports_param_line_and_preserves_output(self):
|
||||
def test_parameter_type_mismatch_creates_structural_draft(self):
|
||||
source = self.write_tsf(
|
||||
"ParamTypeMismatch",
|
||||
"""
|
||||
@@ -249,32 +249,24 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
end;
|
||||
""",
|
||||
)
|
||||
self.output.write_text("原内容\n", encoding="utf-8")
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:4:", result.stderr)
|
||||
self.assertIn("参数 value 的类型与函数声明不一致", result.stderr)
|
||||
self.assertIn("注释为 string,声明为 integer", result.stderr)
|
||||
self.assertEqual("原内容\n", self.output.read_text(encoding="utf-8"))
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
function = self.read_declaration()
|
||||
self.assertEqual("", function["desc"])
|
||||
self.assertEqual(
|
||||
[{"name": "value", "type": "integer", "desc": ""}],
|
||||
function["params"],
|
||||
)
|
||||
self.assertEqual("integer", function["returns"])
|
||||
|
||||
def test_parameter_type_braces_must_be_complete_and_nonempty(self):
|
||||
def test_malformed_parameter_docs_create_structural_draft(self):
|
||||
cases = {
|
||||
"EmptyParamType": (
|
||||
"/// @param: value {} 输入值",
|
||||
"@param: 参数类型不能为空",
|
||||
),
|
||||
"UnclosedParamType": (
|
||||
"/// @param: value {integer 输入值",
|
||||
"@param: 参数类型缺少右花括号",
|
||||
),
|
||||
"MissingParamDescription": (
|
||||
"/// @param: value {integer}",
|
||||
"@param: 参数说明不能为空",
|
||||
),
|
||||
"EmptyParamType": "/// @param: value {} 输入值",
|
||||
"UnclosedParamType": "/// @param: value {integer 输入值",
|
||||
"MissingParamDescription": "/// @param: value {integer}",
|
||||
}
|
||||
for name, (param_line, expected) in cases.items():
|
||||
for name, param_line in cases.items():
|
||||
with self.subTest(name=name):
|
||||
source = self.write_tsf(
|
||||
name,
|
||||
@@ -290,10 +282,14 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:4:", result.stderr)
|
||||
self.assertIn(expected, result.stderr)
|
||||
self.assertFalse(self.output.exists())
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
function = self.read_declaration()
|
||||
self.assertEqual("", function["desc"])
|
||||
self.assertEqual(
|
||||
[{"name": "value", "type": "", "desc": ""}],
|
||||
function["params"],
|
||||
)
|
||||
self.assertEqual("integer", function["returns"])
|
||||
|
||||
def test_multiple_tsf_files_are_merged_in_input_order(self):
|
||||
first = self.write_tsf(
|
||||
@@ -514,6 +510,45 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual("", function["returns"])
|
||||
|
||||
def test_legacy_document_block_creates_structural_draft(self):
|
||||
source = self.write_tsf(
|
||||
"LegacyDraft",
|
||||
"""
|
||||
function LegacyDraft(required: integer; optional = 1): array;
|
||||
begin
|
||||
{**
|
||||
@param(required)(integer) 必填值
|
||||
@param(optional)(integer) 可选值
|
||||
@return(array) 结果
|
||||
**}
|
||||
return array();
|
||||
end;
|
||||
""",
|
||||
)
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual(
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "LegacyDraft",
|
||||
"signature": "LegacyDraft(required, optional)",
|
||||
"desc": "",
|
||||
"params": [
|
||||
{"name": "required", "type": "integer", "desc": ""},
|
||||
{
|
||||
"name": "optional",
|
||||
"type": "",
|
||||
"optional": True,
|
||||
"desc": "",
|
||||
},
|
||||
],
|
||||
"returns": "array",
|
||||
},
|
||||
self.read_declaration(),
|
||||
)
|
||||
|
||||
def test_json_draft_completes_fixed_fields_for_every_declaration_kind(self):
|
||||
function_source = self.write_tsf(
|
||||
"DraftFunction",
|
||||
@@ -649,7 +684,7 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
self.assertNotIn("positional arguments:", result.stdout)
|
||||
self.assertNotIn("options:", result.stdout)
|
||||
|
||||
def test_return_type_mismatch_reports_directive_line_and_preserves_output(self):
|
||||
def test_return_type_mismatch_creates_structural_draft(self):
|
||||
source = self.write_tsf(
|
||||
"Mismatch",
|
||||
"""
|
||||
@@ -661,17 +696,14 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
end;
|
||||
""",
|
||||
)
|
||||
self.output.write_text("原内容\n", encoding="utf-8")
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:4:", result.stderr)
|
||||
self.assertIn("返回类型与函数声明不一致", result.stderr)
|
||||
self.assertIn("注释为 string,声明为 integer", result.stderr)
|
||||
self.assertEqual("原内容\n", self.output.read_text(encoding="utf-8"))
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
function = self.read_declaration()
|
||||
self.assertEqual("", function["desc"])
|
||||
self.assertEqual("integer", function["returns"])
|
||||
|
||||
def test_document_block_must_be_first_content_after_begin(self):
|
||||
def test_late_document_block_creates_structural_draft(self):
|
||||
source = self.write_tsf(
|
||||
"LateDoc",
|
||||
"""
|
||||
@@ -686,12 +718,30 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:3:", result.stderr)
|
||||
self.assertIn("begin 后第一段内容必须是 /// 文档块", result.stderr)
|
||||
self.assertFalse(self.output.exists())
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
function = self.read_declaration()
|
||||
self.assertEqual("", function["desc"])
|
||||
self.assertEqual("integer", function["returns"])
|
||||
|
||||
def test_duplicate_enum_value_is_rejected(self):
|
||||
def test_missing_body_does_not_borrow_later_function_document(self):
|
||||
source = self.write_tsf(
|
||||
"MissingOwnBody",
|
||||
"function MissingOwnBody(): integer;\n"
|
||||
"function Helper(): integer;\n"
|
||||
"begin\n"
|
||||
" /// 辅助函数文档。\n"
|
||||
" return 1;\n"
|
||||
"end;\n",
|
||||
)
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
function = self.read_declaration()
|
||||
self.assertEqual("MissingOwnBody", function["name"])
|
||||
self.assertEqual("", function["desc"])
|
||||
|
||||
def test_duplicate_enum_value_creates_structural_draft(self):
|
||||
source = self.write_tsf(
|
||||
"DuplicateValue",
|
||||
"""
|
||||
@@ -709,11 +759,16 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:7:", result.stderr)
|
||||
self.assertIn("枚举值重复", result.stderr)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
function = self.read_declaration()
|
||||
self.assertEqual("", function["desc"])
|
||||
self.assertEqual(
|
||||
[{"name": "mode", "type": "integer", "desc": ""}],
|
||||
function["params"],
|
||||
)
|
||||
self.assertEqual("integer", function["returns"])
|
||||
|
||||
def test_directives_after_examples_are_rejected(self):
|
||||
def test_directives_after_examples_create_structural_draft(self):
|
||||
source = self.write_tsf(
|
||||
"BadOrder",
|
||||
"""
|
||||
@@ -730,9 +785,14 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:6:", result.stderr)
|
||||
self.assertIn("示例组之后不能再写函数级指令", result.stderr)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
function = self.read_declaration()
|
||||
self.assertEqual("", function["desc"])
|
||||
self.assertEqual(
|
||||
[{"name": "value", "type": "integer", "desc": ""}],
|
||||
function["params"],
|
||||
)
|
||||
self.assertEqual("integer", function["returns"])
|
||||
|
||||
def test_class_converts_only_first_matching_public_class(self):
|
||||
source = self.write_tsf(
|
||||
@@ -982,7 +1042,7 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
members = self.read_declaration()["members"]
|
||||
self.assertEqual(["Value"], [member["name"] for member in members])
|
||||
|
||||
def test_class_rejects_unsupported_public_declaration_at_exact_line(self):
|
||||
def test_class_skips_unsupported_public_declaration(self):
|
||||
source = self.write_tsf(
|
||||
"UnsupportedClass",
|
||||
"""
|
||||
@@ -992,14 +1052,10 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
end;
|
||||
""",
|
||||
)
|
||||
self.output.write_text("原内容\n", encoding="utf-8")
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:3:", result.stderr)
|
||||
self.assertIn("class 不支持成员声明:type", result.stderr)
|
||||
self.assertEqual("原内容\n", self.output.read_text(encoding="utf-8"))
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual([], self.read_declaration()["members"])
|
||||
|
||||
def test_missing_class_and_unit_member_types_remain_explicit_drafts(self):
|
||||
cls_source = self.write_tsf(
|
||||
@@ -1126,7 +1182,33 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
self.assertNotIn("HiddenValue", serialized)
|
||||
self.assertNotIn("生命周期文档", serialized)
|
||||
|
||||
def test_unit_filename_must_match_name_at_declaration_line(self):
|
||||
def test_incomplete_interface_class_does_not_scan_implementation(self):
|
||||
source = self.write_tsf(
|
||||
"IncompleteInterfaceClass",
|
||||
"unit IncompleteInterfaceClass;\n"
|
||||
"interface\n"
|
||||
"type PublicType = class\n"
|
||||
"public\n"
|
||||
"function Keep(): integer;\n"
|
||||
"implementation\n"
|
||||
"function Hidden(): integer;\n"
|
||||
"begin\n"
|
||||
" return 1;\n"
|
||||
"end;\n"
|
||||
"end.\n",
|
||||
)
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
unit = self.read_declaration()
|
||||
self.assertEqual(["PublicType"], [member["name"] for member in unit["members"]])
|
||||
self.assertEqual(
|
||||
["Keep"],
|
||||
[member["name"] for member in unit["members"][0]["members"]],
|
||||
)
|
||||
|
||||
def test_unit_filename_mismatch_keeps_declared_name(self):
|
||||
source = self.write_tsf(
|
||||
"Wrong",
|
||||
"unit Right;\ninterface\nimplementation\nend.\n",
|
||||
@@ -1134,9 +1216,8 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:1:", result.stderr)
|
||||
self.assertIn("unit 名称必须与文件名一致", result.stderr)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual("Right", self.read_declaration()["name"])
|
||||
|
||||
def test_unit_preserves_multiple_interface_classes_in_source_order(self):
|
||||
source = self.write_tsf(
|
||||
@@ -1162,7 +1243,7 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
members = self.read_declaration()["members"]
|
||||
self.assertEqual(["First", "Second"], [member["name"] for member in members])
|
||||
|
||||
def test_unit_unbound_document_reports_exact_line(self):
|
||||
def test_unit_unbound_document_is_ignored(self):
|
||||
source = self.write_tsf(
|
||||
"UnboundUnit",
|
||||
"""
|
||||
@@ -1178,11 +1259,12 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:3:", result.stderr)
|
||||
self.assertIn("unit interface 文档块无法绑定", result.stderr)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
current = self.read_declaration()["members"][0]
|
||||
self.assertEqual("Current", current["name"])
|
||||
self.assertEqual("", current["desc"])
|
||||
|
||||
def test_unit_unbound_header_document_reports_exact_line(self):
|
||||
def test_unit_unbound_header_document_is_ignored(self):
|
||||
source = self.write_tsf(
|
||||
"UnboundHeaderUnit",
|
||||
"""
|
||||
@@ -1197,11 +1279,10 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:2:", result.stderr)
|
||||
self.assertIn("unit interface 文档块无法绑定", result.stderr)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual("", self.read_declaration()["desc"])
|
||||
|
||||
def test_unit_end_dot_must_be_the_terminal_tokens(self):
|
||||
def test_unit_ignores_tokens_after_terminal_end_dot(self):
|
||||
source = self.write_tsf(
|
||||
"TrailingUnit",
|
||||
"""
|
||||
@@ -1215,9 +1296,8 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:5:", result.stderr)
|
||||
self.assertIn("end. 必须结束整个文件", result.stderr)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual("TrailingUnit", self.read_declaration()["name"])
|
||||
|
||||
def test_class_missing_docs_default_visibility_and_private_filter_create_draft(self):
|
||||
source = self.write_tsf(
|
||||
@@ -1269,35 +1349,37 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
members = self.read_declaration()["members"]
|
||||
self.assertEqual(["value_"], [member["name"] for member in members])
|
||||
|
||||
def test_class_structure_errors_preserve_existing_output(self):
|
||||
def test_class_structure_errors_create_best_effort_drafts(self):
|
||||
cases = {
|
||||
"WrongName": (
|
||||
"type Actual = class\nend;\n",
|
||||
1,
|
||||
"对外 class 名称必须与文件名一致",
|
||||
"Actual",
|
||||
[],
|
||||
),
|
||||
"StaticMethod": (
|
||||
"type StaticMethod = class\npublic\nstatic function Bad();\nend;\n",
|
||||
3,
|
||||
"不存在 static function",
|
||||
"StaticMethod",
|
||||
["Bad"],
|
||||
),
|
||||
"ManyFields": (
|
||||
"type ManyFields = class\npublic\nleft_, right_: integer;\nend;\n",
|
||||
3,
|
||||
"一项一条声明",
|
||||
"ManyFields",
|
||||
["left_", "right_"],
|
||||
),
|
||||
}
|
||||
for name, (text, line, expected) in cases.items():
|
||||
for name, (text, expected_name, expected_members) in cases.items():
|
||||
with self.subTest(name=name):
|
||||
source = self.write_tsf(name, text)
|
||||
self.output.write_text("原内容\n", encoding="utf-8")
|
||||
result = self.run_cli(source)
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:{line}:", result.stderr)
|
||||
self.assertIn(expected, result.stderr)
|
||||
self.assertEqual("原内容\n", self.output.read_text(encoding="utf-8"))
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
declaration = self.read_declaration()
|
||||
self.assertEqual(expected_name, declaration["name"])
|
||||
self.assertEqual(
|
||||
expected_members,
|
||||
[member["name"] for member in declaration["members"]],
|
||||
)
|
||||
|
||||
def test_class_and_unit_constants_reject_multiple_names_at_declaration_line(self):
|
||||
def test_class_and_unit_constants_split_multiple_names_into_drafts(self):
|
||||
cases = {
|
||||
"ManyConstants": (
|
||||
"type ManyConstants = class\npublic\nconst Left, Right = 1;\nend;\n",
|
||||
@@ -1312,19 +1394,19 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
3,
|
||||
),
|
||||
}
|
||||
for name, (text, line) in cases.items():
|
||||
for name, (text, _line) in cases.items():
|
||||
with self.subTest(name=name):
|
||||
source = self.write_tsf(name, text)
|
||||
self.output.write_text("原内容\n", encoding="utf-8")
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:{line}:", result.stderr)
|
||||
self.assertIn("对外常量必须一项一条声明", result.stderr)
|
||||
self.assertEqual("原内容\n", self.output.read_text(encoding="utf-8"))
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual(
|
||||
["Left", "Right"],
|
||||
[member["name"] for member in self.read_declaration()["members"]],
|
||||
)
|
||||
|
||||
def test_class_rejects_procedure_at_declaration_line(self):
|
||||
def test_class_converts_procedure_to_method_draft(self):
|
||||
source = self.write_tsf(
|
||||
"ProcedureClass",
|
||||
"""
|
||||
@@ -1334,16 +1416,15 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
end;
|
||||
""",
|
||||
)
|
||||
self.output.write_text("原内容\n", encoding="utf-8")
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:3:", result.stderr)
|
||||
self.assertIn("class 暂不支持 procedure", result.stderr)
|
||||
self.assertEqual("原内容\n", self.output.read_text(encoding="utf-8"))
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
method = self.read_declaration()["members"][0]
|
||||
self.assertEqual("Open", method["name"])
|
||||
self.assertEqual("method", method["kind"])
|
||||
self.assertEqual("", method["returns"])
|
||||
|
||||
def test_class_unbound_document_reports_its_line(self):
|
||||
def test_class_unbound_document_is_ignored(self):
|
||||
source = self.write_tsf(
|
||||
"UnboundDoc",
|
||||
"""
|
||||
@@ -1359,35 +1440,323 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:4:", result.stderr)
|
||||
self.assertIn("文档块无法绑定", result.stderr)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
field = self.read_declaration()["members"][0]
|
||||
self.assertEqual("value_", field["name"])
|
||||
self.assertEqual("", field["desc"])
|
||||
|
||||
def test_unit_rejects_shorthand_procedure_and_non_class_type(self):
|
||||
def test_unit_converts_shorthand_and_skips_unsupported_types(self):
|
||||
cases = {
|
||||
"ShortUnit": (
|
||||
"unit ShortUnit;\nfunction Open(): integer;\nbegin return 1; end;\nend.\n",
|
||||
1,
|
||||
"显式 interface",
|
||||
["Open"],
|
||||
),
|
||||
"ProcedureUnit": (
|
||||
"unit ProcedureUnit;\ninterface\nprocedure Open();\nimplementation\nend.\n",
|
||||
3,
|
||||
"暂不支持 procedure",
|
||||
["Open"],
|
||||
),
|
||||
"AliasUnit": (
|
||||
"unit AliasUnit;\ninterface\ntype Size = integer;\nimplementation\nend.\n",
|
||||
3,
|
||||
"只支持 class type",
|
||||
[],
|
||||
),
|
||||
}
|
||||
for name, (text, line, expected) in cases.items():
|
||||
for name, (text, expected_members) in cases.items():
|
||||
with self.subTest(name=name):
|
||||
source = self.write_tsf(name, text)
|
||||
result = self.run_cli(source)
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:{line}:", result.stderr)
|
||||
self.assertIn(expected, result.stderr)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual(
|
||||
expected_members,
|
||||
[member["name"] for member in self.read_declaration()["members"]],
|
||||
)
|
||||
|
||||
def test_malformed_class_and_unit_documents_create_structural_drafts(self):
|
||||
cls = self.write_tsf(
|
||||
"BadClassDocs",
|
||||
"""
|
||||
type BadClassDocs = class
|
||||
public
|
||||
/// 打开值
|
||||
/// @param: value {string} 输入值
|
||||
function Open(value: integer): integer;
|
||||
end;
|
||||
""",
|
||||
)
|
||||
unit = self.write_tsf(
|
||||
"BadUnitDocs",
|
||||
"""
|
||||
unit BadUnitDocs;
|
||||
interface
|
||||
/// 打开值
|
||||
/// @returns: string
|
||||
function Open(): integer;
|
||||
implementation
|
||||
end.
|
||||
""",
|
||||
)
|
||||
|
||||
result = self.run_cli(cls, unit)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
class_method = self.read_declarations()[0]["members"][0]
|
||||
unit_function = self.read_declarations()[1]["members"][0]
|
||||
self.assertEqual("", class_method["desc"])
|
||||
self.assertEqual("integer", class_method["params"][0]["type"])
|
||||
self.assertEqual("", unit_function["desc"])
|
||||
self.assertEqual("integer", unit_function["returns"])
|
||||
|
||||
def test_incomplete_class_and_unit_keep_recognizable_members(self):
|
||||
cases = {
|
||||
"IncompleteClass": (
|
||||
"type IncompleteClass = class\n"
|
||||
"public\n"
|
||||
"function Keep(value: integer): integer;\n",
|
||||
"Keep",
|
||||
),
|
||||
"IncompleteUnit": (
|
||||
"unit IncompleteUnit;\n"
|
||||
"function Keep(value: integer): integer;\n",
|
||||
"Keep",
|
||||
),
|
||||
}
|
||||
for name, (text, expected_member) in cases.items():
|
||||
with self.subTest(name=name):
|
||||
source = self.write_tsf(name, text)
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual(
|
||||
[expected_member],
|
||||
[member["name"] for member in self.read_declaration()["members"]],
|
||||
)
|
||||
|
||||
def test_broken_member_does_not_hide_following_function(self):
|
||||
cases = {
|
||||
"RecoverClass": (
|
||||
"type RecoverClass = class\n"
|
||||
"public\n"
|
||||
"property Broken\n"
|
||||
"function Keep(): integer;\n"
|
||||
"end;\n",
|
||||
"class",
|
||||
),
|
||||
"RecoverUnit": (
|
||||
"unit RecoverUnit;\n"
|
||||
"interface\n"
|
||||
"const Broken\n"
|
||||
"function Keep(): integer;\n"
|
||||
"implementation\n"
|
||||
"end.\n",
|
||||
"unit",
|
||||
),
|
||||
}
|
||||
for name, (text, expected_kind) in cases.items():
|
||||
with self.subTest(name=name):
|
||||
source = self.write_tsf(name, text)
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
declaration = self.read_declaration()
|
||||
self.assertEqual(expected_kind, declaration["kind"])
|
||||
self.assertIn(
|
||||
"Keep",
|
||||
[member["name"] for member in declaration["members"]],
|
||||
)
|
||||
|
||||
def test_unnamed_class_and_unit_functions_are_skipped(self):
|
||||
cases = {
|
||||
"UnnamedClassFunction": (
|
||||
"type UnnamedClassFunction = class\n"
|
||||
"public\n"
|
||||
"function\n"
|
||||
"function Keep(): integer;\n"
|
||||
"end;\n",
|
||||
"class",
|
||||
),
|
||||
"UnnamedUnitFunction": (
|
||||
"unit UnnamedUnitFunction;\n"
|
||||
"interface\n"
|
||||
"function\n"
|
||||
"function Keep(): integer;\n"
|
||||
"implementation\n"
|
||||
"end.\n",
|
||||
"unit",
|
||||
),
|
||||
}
|
||||
for name, (text, expected_kind) in cases.items():
|
||||
with self.subTest(name=name):
|
||||
source = self.write_tsf(name, text)
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
declaration = self.read_declaration()
|
||||
self.assertEqual(expected_kind, declaration["kind"])
|
||||
self.assertEqual(
|
||||
["Keep"],
|
||||
[member["name"] for member in declaration["members"]],
|
||||
)
|
||||
|
||||
def test_unnamed_members_do_not_hide_following_functions(self):
|
||||
cases = {
|
||||
"UnnamedProperty": (
|
||||
"type UnnamedProperty = class\n"
|
||||
"public\n"
|
||||
"property\n"
|
||||
"function Keep(): integer;\n"
|
||||
"end;\n",
|
||||
"class",
|
||||
),
|
||||
"UnnamedConstant": (
|
||||
"unit UnnamedConstant;\n"
|
||||
"interface\n"
|
||||
"const\n"
|
||||
"function Keep(): integer;\n"
|
||||
"implementation\n"
|
||||
"end.\n",
|
||||
"unit",
|
||||
),
|
||||
}
|
||||
for name, (text, expected_kind) in cases.items():
|
||||
with self.subTest(name=name):
|
||||
source = self.write_tsf(name, text)
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
declaration = self.read_declaration()
|
||||
self.assertEqual(expected_kind, declaration["kind"])
|
||||
self.assertEqual(
|
||||
["Keep"],
|
||||
[member["name"] for member in declaration["members"]],
|
||||
)
|
||||
|
||||
def test_method_missing_signature_semicolon_does_not_hide_next_member(self):
|
||||
source = self.write_tsf(
|
||||
"BrokenInlineMethod",
|
||||
"type BrokenInlineMethod = class\n"
|
||||
"public\n"
|
||||
"function Broken()\n"
|
||||
"begin\n"
|
||||
" return 1;\n"
|
||||
"end;\n"
|
||||
"function Keep(): integer;\n"
|
||||
"end;\n",
|
||||
)
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual(
|
||||
["Broken", "Keep"],
|
||||
[member["name"] for member in self.read_declaration()["members"]],
|
||||
)
|
||||
|
||||
def test_broken_headers_do_not_hide_first_function(self):
|
||||
cases = {
|
||||
"BrokenBase": (
|
||||
"type BrokenBase = class(Base\n"
|
||||
"function Keep(): integer;\n",
|
||||
"class",
|
||||
),
|
||||
"MissingClassName": (
|
||||
"type\n"
|
||||
"class\n"
|
||||
"function Keep(): integer;\n",
|
||||
"class",
|
||||
),
|
||||
"BrokenUnitHeader": (
|
||||
"unit BrokenUnitHeader\n"
|
||||
"function Keep(): integer;\n",
|
||||
"unit",
|
||||
),
|
||||
"MissingUnitName": (
|
||||
"unit\n"
|
||||
"function Keep(): integer;\n",
|
||||
"unit",
|
||||
),
|
||||
}
|
||||
for name, (text, expected_kind) in cases.items():
|
||||
with self.subTest(name=name):
|
||||
source = self.write_tsf(name, text)
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
declaration = self.read_declaration()
|
||||
self.assertEqual(expected_kind, declaration["kind"])
|
||||
self.assertEqual(
|
||||
["Keep"],
|
||||
[member["name"] for member in declaration["members"]],
|
||||
)
|
||||
|
||||
def test_declaration_truncation_after_name_always_creates_a_draft(self):
|
||||
module = load_script()
|
||||
samples = {
|
||||
"function": (
|
||||
"TruncateFunction.tsf",
|
||||
"function TruncateFunction(required: integer; optional = 1): array;\n"
|
||||
"begin\n"
|
||||
" return array();\n"
|
||||
"end;\n",
|
||||
),
|
||||
"class": (
|
||||
"TruncateClass.tsf",
|
||||
"type TruncateClass = class(Base)\n"
|
||||
"public\n"
|
||||
"function Open(value: integer): string;\n"
|
||||
"property Value: integer read value_ write value_;\n"
|
||||
"const Limit = 2;\n"
|
||||
"value_: integer;\n"
|
||||
"end;\n",
|
||||
),
|
||||
"unit": (
|
||||
"TruncateUnit.tsf",
|
||||
"unit TruncateUnit;\n"
|
||||
"interface\n"
|
||||
"function Open(value: integer): string;\n"
|
||||
"const Limit = 2;\n"
|
||||
"var Current: integer;\n"
|
||||
"implementation\n"
|
||||
"end.\n",
|
||||
),
|
||||
}
|
||||
for expected_kind, (filename, source) in samples.items():
|
||||
name_end = module.tokenize(source)[1].end
|
||||
for cut in range(name_end, len(source) + 1):
|
||||
with self.subTest(kind=expected_kind, cut=cut):
|
||||
declaration = module.complete_draft_fields(
|
||||
module.convert_source(source[:cut], Path(filename))
|
||||
)
|
||||
self.assertEqual(expected_kind, declaration["kind"])
|
||||
self.assertTrue(declaration["name"])
|
||||
|
||||
def test_unit_fallback_keeps_bare_const_and_var_sections(self):
|
||||
source = self.write_tsf(
|
||||
"RecoverSections",
|
||||
"""
|
||||
unit RecoverSections;
|
||||
interface
|
||||
/// 无法绑定
|
||||
// 切断文档绑定
|
||||
const First = 1;
|
||||
Second = 2;
|
||||
var Current: integer;
|
||||
Previous: integer;
|
||||
implementation
|
||||
end.
|
||||
""",
|
||||
)
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual(
|
||||
["First", "Second", "Current", "Previous"],
|
||||
[member["name"] for member in self.read_declaration()["members"]],
|
||||
)
|
||||
|
||||
def test_unit_var_and_const_sections_keep_each_declaration(self):
|
||||
source = self.write_tsf(
|
||||
@@ -1458,7 +1827,7 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
[item["signature"] for item in self.read_declarations()],
|
||||
)
|
||||
|
||||
def test_duplicate_function_signature_preserves_existing_output(self):
|
||||
def test_duplicate_function_signature_is_preserved_in_input_order(self):
|
||||
first = self.write_nested_tsf(
|
||||
"first",
|
||||
"Parse",
|
||||
@@ -1483,15 +1852,15 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
end;
|
||||
""",
|
||||
)
|
||||
self.output.write_text("原内容\n", encoding="utf-8")
|
||||
|
||||
result = self.run_cli(first, second)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("重复 function signature", result.stderr)
|
||||
self.assertEqual("原内容\n", self.output.read_text(encoding="utf-8"))
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual(
|
||||
["Parse(value)", "parse(value)"],
|
||||
[item["signature"] for item in self.read_declarations()],
|
||||
)
|
||||
|
||||
def test_duplicate_class_and_unit_names_are_rejected_case_insensitively(self):
|
||||
def test_duplicate_class_and_unit_names_are_preserved_in_input_order(self):
|
||||
cases = (
|
||||
(
|
||||
self.write_nested_tsf(
|
||||
@@ -1516,16 +1885,14 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
"重复 unit",
|
||||
),
|
||||
)
|
||||
for first, second, expected in cases:
|
||||
with self.subTest(expected=expected):
|
||||
self.output.write_text("原内容\n", encoding="utf-8")
|
||||
|
||||
for first, second, kind in cases:
|
||||
with self.subTest(kind=kind):
|
||||
result = self.run_cli(first, second)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(expected, result.stderr)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual(
|
||||
"原内容\n", self.output.read_text(encoding="utf-8")
|
||||
2,
|
||||
len(self.read_declarations()),
|
||||
)
|
||||
|
||||
def test_same_name_across_kinds_is_allowed(self):
|
||||
@@ -1574,16 +1941,100 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
[item["name"] for item in self.read_declarations()],
|
||||
)
|
||||
|
||||
def test_top_level_procedure_remains_unsupported(self):
|
||||
def test_top_level_procedure_creates_function_draft(self):
|
||||
source = self.write_tsf(
|
||||
"ProcedureDemo",
|
||||
"procedure ProcedureDemo();\nbegin\nend;\n",
|
||||
"procedure ProcedureDemo(value: integer);\nbegin\nend;\n",
|
||||
)
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("目前支持独立顶层 function、class 和完整 unit", result.stderr)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual(
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "ProcedureDemo",
|
||||
"signature": "ProcedureDemo(value)",
|
||||
"desc": "",
|
||||
"params": [
|
||||
{"name": "value", "type": "integer", "desc": ""}
|
||||
],
|
||||
"returns": "",
|
||||
},
|
||||
self.read_declaration(),
|
||||
)
|
||||
|
||||
def test_malformed_top_level_function_keeps_recognizable_signature(self):
|
||||
cases = {
|
||||
"MissingBody": (
|
||||
"function MissingBody(value: integer): array;\n",
|
||||
"MissingBody",
|
||||
[{"name": "value", "type": "integer", "desc": ""}],
|
||||
"array",
|
||||
),
|
||||
"BrokenParams": (
|
||||
"function BrokenParams(required: integer; optional = 1\n",
|
||||
"BrokenParams",
|
||||
[
|
||||
{"name": "required", "type": "integer", "desc": ""},
|
||||
{
|
||||
"name": "optional",
|
||||
"type": "",
|
||||
"optional": True,
|
||||
"desc": "",
|
||||
},
|
||||
],
|
||||
"",
|
||||
),
|
||||
"MissingName": (
|
||||
"function\n",
|
||||
"MissingName",
|
||||
[],
|
||||
"",
|
||||
),
|
||||
}
|
||||
for filename, (text, name, params, returns) in cases.items():
|
||||
with self.subTest(filename=filename):
|
||||
source = self.write_tsf(filename, text)
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
declaration = self.read_declaration()
|
||||
self.assertEqual(name, declaration["name"])
|
||||
parameter_names = ", ".join(
|
||||
parameter["name"] for parameter in params
|
||||
)
|
||||
self.assertEqual(
|
||||
f"{name}({parameter_names})",
|
||||
declaration["signature"],
|
||||
)
|
||||
self.assertEqual(params, declaration["params"])
|
||||
self.assertEqual(returns, declaration["returns"])
|
||||
|
||||
def test_unrecognized_tsf_header_still_creates_function_draft(self):
|
||||
cases = {
|
||||
"LooseCall": (
|
||||
"LegacyName(first, second)\n",
|
||||
"LegacyName",
|
||||
["first", "second"],
|
||||
),
|
||||
"EmptySource": ("", "EmptySource", []),
|
||||
}
|
||||
for filename, (text, name, params) in cases.items():
|
||||
with self.subTest(filename=filename):
|
||||
source = self.write_tsf(filename, text)
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
declaration = self.read_declaration()
|
||||
self.assertEqual("function", declaration["kind"])
|
||||
self.assertEqual(name, declaration["name"])
|
||||
self.assertEqual(
|
||||
params,
|
||||
[parameter["name"] for parameter in declaration["params"]],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user