📦 deps(tsl): sync tsl-playbook from aa8a3e73

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