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