🐛 fix(tsl-codegen): make TSF conversion best effort
Recover recognizable function, procedure, class, and unit structure from nonstandard TSF input while preserving strict parsing for compliant files. Keep STANDARD.md limited to published format requirements and cover recovery paths with regression tests.
This commit is contained in:
+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