@@ -58,9 +58,9 @@ python <this-skill-dir>/scripts/lookup.py --section "syntax-05-004" "syntax-02-0
|
||||
|
||||
- 中文口语词自动扩展到 TSL 术语(如"打印"→输出/writeLn,"列表"→数组,
|
||||
"程序慢"→性能分析)。保留提取词的原写法即可。
|
||||
- `tsl`、`tsf`、`tinysoft`、`program`、`debug`、`please` 这几个词不参与逐词匹配,
|
||||
- `tsl`、`tsf`、`tinysoft`、`debug`、`please` 这几个词不参与逐词匹配,
|
||||
但可能整体把查询导向某个专题页。无结果时靠加这类词补救没有用,改为换更具体的
|
||||
语法要素名。
|
||||
语法要素名。`program` 是真实 TSL 关键字,会参与精确匹配。
|
||||
|
||||
## 弱命中视同无匹配
|
||||
|
||||
@@ -93,6 +93,9 @@ Section ID 抄自 `--section` 输出首部的 `Section ID:` 行。多个要素
|
||||
|
||||
写不出某个要素的 Section ID,说明该要素还没有取回:先补齐检索,再给代码。
|
||||
|
||||
不含代码块的语法解释同样要在相关结论后列出依据的 Section ID;多个结论来自不同
|
||||
章节时逐项对应,不用一个 Section ID 为整段未取回的结论背书。
|
||||
|
||||
## 代码块身份
|
||||
|
||||
`--section` 正文里每个代码围栏前有一行 `代码块身份:`。身份决定这段代码能否进入
|
||||
@@ -115,6 +118,8 @@ Section ID 抄自 `--section` 输出首部的 `Section ID:` 行。多个要素
|
||||
|
||||
## 缺口时停止
|
||||
|
||||
**🔴 CHECKPOINT · 🛑 STOP:确认以下缺口后停止生成,不输出含缺口要素的代码。**
|
||||
|
||||
出现下列任一情形,即为事实缺口:
|
||||
|
||||
**检索缺口** —— `--query` 改进查询词后仍无匹配;lookup 返回非零状态;`--section`
|
||||
|
||||
@@ -31,23 +31,42 @@
|
||||
"函数指针释放": ["DeleteInstance", "MakeInstance"],
|
||||
"Linux 动态加载": ["dlopen", "dlsym", ".so"],
|
||||
"远程调用客户端": ["RDo", "RDo2", "客户端远程调用"],
|
||||
"本地弹窗": ["RDo2", "InputQuery", "客户端远程调用"]
|
||||
"本地弹窗": ["RDo2", "InputQuery", "客户端远程调用"],
|
||||
"全局变量": ["global", "跨函数共享变量"],
|
||||
"静态计算": ["static", "表达式缓存"],
|
||||
"只计算一次": ["static", "表达式缓存"],
|
||||
"指定系统函数": ["system", "系统函数限定"],
|
||||
"内存上限": ["_maxMem_", "最大内存"],
|
||||
"每列聚集": ["avgof", "多字段聚集"],
|
||||
"按行广播": ["二维矩阵", "一维数组", "行广播"]
|
||||
},
|
||||
"page_intent_aliases": {
|
||||
"01_quickstart.md": ["最简单能跑", "最简单的脚本", "天软脚本", "tinysoft"],
|
||||
"02_core_model.md": [
|
||||
"脚本和可复用",
|
||||
"可复用函数文件",
|
||||
"声明函数后面写代码"
|
||||
"声明函数后面写代码",
|
||||
"完整 program 入口",
|
||||
"echo 输出多个值"
|
||||
],
|
||||
"03_values_and_literals.md": [
|
||||
"字符串和数组下标",
|
||||
"下标从几开始",
|
||||
"下标起点"
|
||||
],
|
||||
"04_variables_and_constants.md": ["常量怎么声明", "变量能不能直接赋值"],
|
||||
"05_functions_and_calls.md": ["默认参数", "函数怎么带"],
|
||||
"06_expressions_and_operators.md": ["赋值和相等比较"],
|
||||
"04_variables_and_constants.md": [
|
||||
"常量怎么声明",
|
||||
"变量能不能直接赋值",
|
||||
"全局变量",
|
||||
"运行时常量"
|
||||
],
|
||||
"05_functions_and_calls.md": ["默认参数", "函数怎么带", "指定系统函数"],
|
||||
"06_expressions_and_operators.md": [
|
||||
"赋值和相等比较",
|
||||
"运算符优先级",
|
||||
"静态计算",
|
||||
"只计算一次"
|
||||
],
|
||||
"07_control_flow.md": [
|
||||
"跳出去",
|
||||
"循环里满足条件",
|
||||
@@ -59,7 +78,12 @@
|
||||
"08_objects_and_classes.md": ["定义类", "创建对象"],
|
||||
"09_units_and_scope.md": ["多个文件", "复用一组函数"],
|
||||
"10_runtime_context_and_with.md": ["临时切换系统参数"],
|
||||
"11_matrix_and_collections.md": ["某行存在", "二维数组怎么判断"],
|
||||
"11_matrix_and_collections.md": [
|
||||
"某行存在",
|
||||
"二维数组怎么判断",
|
||||
"按行广播",
|
||||
"二维矩阵乘一维数组"
|
||||
],
|
||||
"12_resultset_and_filters.md": ["保留匹配行", "按某一列"],
|
||||
"13_ts_sql.md": [
|
||||
"左连接",
|
||||
@@ -67,7 +91,9 @@
|
||||
"左联接",
|
||||
"数据库",
|
||||
"分组排序",
|
||||
"聚合排序"
|
||||
"聚合排序",
|
||||
"JOIN 第二张表当前行",
|
||||
"每列聚集"
|
||||
],
|
||||
"14_debug_and_profiler.md": [
|
||||
"程序慢",
|
||||
@@ -77,11 +103,13 @@
|
||||
"执行了多久",
|
||||
"执行时间",
|
||||
"耗时",
|
||||
"debug"
|
||||
"debug",
|
||||
"内存上限"
|
||||
],
|
||||
"15_lexical_structure_and_compile_options.md": [
|
||||
"变量名区分大小写",
|
||||
"注释怎么写"
|
||||
"注释怎么写",
|
||||
"嵌套注释"
|
||||
],
|
||||
"16_types_and_conversions.md": ["字符串转整数", "类型转换"],
|
||||
"17_external_calls_and_threads.md": [
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<!-- section-id: syntax-02-001 -->
|
||||
|
||||
回答“目标文件到底是 `.tsl` 脚本还是 `.tsf` 可复用声明文件,以及 `.tsl` 里的哪些内容会顺序执行、哪些内容只是后置声明”。
|
||||
回答“目标文件到底是 `.tsl` 脚本还是 `.tsf` 可复用声明文件,`.tsl` 里的哪些内容会顺序执行、哪些内容只是后置声明,以及何时使用完整 `program ... begin ... end.` 入口形态”。
|
||||
|
||||
本页是文件模型的唯一事实源:后缀判断、语句区 / 声明区顺序、`.tsf` 顶层声明形态和文件名约束都在这里收口。函数体、类体、`unit` 内部的语法外形由各自专题页拥有。
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
- `uses` 可以出现在顶层,但这里只把它当成辅助语句,不把它当成主体声明;函数体和类定义体里的位置限制见 [09_units_and_scope.md](09_units_and_scope.md)。
|
||||
- 裸 `class Name` 不作为类定义写法使用。
|
||||
- 在 `.tsl` 中,不要在声明区之后继续追加脚本语句。
|
||||
- `program Name; ... begin ... end.` 是完整程序入口形态;它可以在主 `begin` 前声明子函数,但不是普通 `.tsl` 脚本的默认起手式。
|
||||
- `unit` 默认先按完整形态理解;它也可以省略 `interface` / `implementation` 写成简写形态,见 [09_units_and_scope.md](09_units_and_scope.md)。
|
||||
- 不要把 `.tsl` 写成只有顶层函数的模块;如果用户要通用可复用函数,优先写 `.tsf`。
|
||||
- 不要把 `.tsf` 写成会直接执行脚本语句的入口;如果用户要顺序执行入口,优先写 `.tsl`。
|
||||
@@ -184,6 +185,64 @@ end.
|
||||
1
|
||||
```
|
||||
|
||||
### 完整 `program` 入口形态
|
||||
|
||||
<!-- section-id: syntax-02-009 -->
|
||||
|
||||
<!-- tags: PROGRAM 入口, 完整程序, 独立脚本入口, CGI 入口, 主 begin 块 -->
|
||||
|
||||
普通 `.tsl` 默认仍使用前面的松散语句区模型。只有用户明确要求完整程序入口、独立脚本 / CGI 兼容形态,或现有代码已经使用 `program` 时,才写成下面的结构:
|
||||
|
||||
代码块身份:可直接照写示例
|
||||
|
||||
```tsl
|
||||
program DemoProgram;
|
||||
|
||||
function DoubleValue(value);
|
||||
begin
|
||||
return value * 2;
|
||||
end;
|
||||
|
||||
begin
|
||||
echo "result:", DoubleValue(3);
|
||||
end.
|
||||
```
|
||||
|
||||
代码块身份:输出片段
|
||||
|
||||
```text
|
||||
result:6
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `program Name;` 位于文件开头
|
||||
- 子函数声明位于主 `begin ... end.` 之前
|
||||
- 完整程序以 `end.` 结束,而普通函数声明仍以 `end;` 结束
|
||||
- 不要把这套结构与“松散语句区在前、声明区在后”的默认 `.tsl` 模型混写
|
||||
|
||||
### `echo` 输出语句
|
||||
|
||||
<!-- section-id: syntax-02-010 -->
|
||||
|
||||
<!-- tags: echo 输出, 打印表达式, 输出多个值, 逗号输出, 控制台输出 -->
|
||||
|
||||
`echo expr1, expr2, ...;` 会按从左到右的顺序输出表达式列表。简单值直接输出;数组、对象等复杂值可能只显示类型摘要。具体输出目标由宿主环境决定,可能是控制台、Web 响应或客户端输出窗口。
|
||||
|
||||
代码块身份:可直接照写示例
|
||||
|
||||
```tsl
|
||||
echo "A=", 1, ",B=", 2;
|
||||
```
|
||||
|
||||
代码块身份:输出片段
|
||||
|
||||
```text
|
||||
A=1,B=2
|
||||
```
|
||||
|
||||
不要把 `echo` 的宿主输出位置或复杂对象的展示格式当成跨环境固定结果。
|
||||
|
||||
### 文件模型反例
|
||||
|
||||
<!-- section-id: syntax-02-006 -->
|
||||
@@ -262,5 +321,6 @@ function:__main__:line 9: invalid statement
|
||||
|
||||
- 把 `.tsl` 当成 `.tsf` 来写,只给一个顶层函数,不写任何会执行的脚本语句。
|
||||
- 把 `.tsf` 当成 `.tsl` 来写,在模块文件里直接堆顺序执行的脚本语句。
|
||||
- 把 `program ... begin ... end.` 与松散 `.tsl` 语句区 / 后置声明区混成一套文件结构。
|
||||
- 把 `uses` 当成主体声明,而不是辅助组织语句。
|
||||
- `.tsf` 文件名与顶层声明不一致:`UserAccount.tsf` 中写 `function GetUser` 或 `type Customer = class` 或 `unit CustomerModule` 会导致加载失败或检索混乱。
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
<!-- quickstart-rule: index-origins -->
|
||||
|
||||
- `array(...)` 既可以写顺序数组,也可以写字符串键表;顺序数组和 `binary(...)` 二进制缓冲区下标从 `0` 开始,字符串下标从 `1` 开始。
|
||||
- 数组元素可以是混合类型;给不存在的下标赋值会扩张数组。显式整数键可写成 `array(0: value0, 1: value1)`。
|
||||
- `s[0]` 在运行时会越界,不要把字符串当成 0 基下标。
|
||||
- 字符串取子串用 `s[start:end]`,并且 `end` 是包含在结果里的。
|
||||
- 字符串替换子串用 `s[start:end] := "..."`。
|
||||
@@ -36,6 +37,8 @@
|
||||
- `U""` 不是宽串;需要在 UTF8、宽串、普通串之间显式转换时,继续用 `utf8ToUnicode(...)`、`utf8ToAnsi(...)`、`ansiToUtf8(...)`、`unicodeToUtf8(...)`、`string(...)`、`wideString(...)`。
|
||||
- `#number` 可以直接把字符码拼进字符串。
|
||||
- `\0` 和 `#0` 都能把 ASCII `0` 放进字符串,并且不会把字符串截断。
|
||||
- 字符串 `like` 的右侧按正则表达式解释,不是 SQL `%` / `_` 通配,也不要把正则里的 `*` 当成独立的 glob 通配符;默认匹配不区分大小写。
|
||||
- 数值 `like` 表示近似相等;`likeEps` 和 `likeEpsRate` 分别控制绝对误差与相对误差阈值,默认值都是 `1e-6`。
|
||||
|
||||
## 可直接照写示例
|
||||
|
||||
@@ -170,6 +173,33 @@ C
|
||||
|
||||
代码块说明:顺序数组 `items` 从 `0` 开始,字符串键表 `row` 用字符串键访问,字符串 `s` 从 `1` 开始。
|
||||
|
||||
数组按写入下标扩张:
|
||||
|
||||
代码块身份:可直接照写示例
|
||||
|
||||
```tsl
|
||||
items := array(1);
|
||||
items[3] := 4;
|
||||
keyed := array(0: 2, 1: 3);
|
||||
writeLn(length(items));
|
||||
writeLn(items[1] = nil);
|
||||
writeLn(items[3]);
|
||||
writeLn(keyed[0]);
|
||||
writeLn(keyed[1]);
|
||||
```
|
||||
|
||||
代码块身份:输出片段
|
||||
|
||||
```text
|
||||
2
|
||||
1
|
||||
4
|
||||
2
|
||||
3
|
||||
```
|
||||
|
||||
说明:`length(items)` 统计已有项数,不等于“最大整数键 + 1”;下标 `1` 没有写入,因此读取为 `nil`。
|
||||
|
||||
### 字符串边界规则
|
||||
|
||||
<!-- section-id: syntax-03-004 -->
|
||||
@@ -416,20 +446,20 @@ A=1 B=2.34 C=AAA
|
||||
- 纯小数可能走科学计数法:`"_" $ 0.0000005 $ ","` 返回 `"_5E-7,"`
|
||||
- 带整数部分的小数保留精度:`"_" $ 1.0000005 $ ","` 返回 `"_1.0000005,"`
|
||||
|
||||
### 字符串 `like` 模式匹配
|
||||
### 字符串与数值 `like`
|
||||
|
||||
<!-- section-id: syntax-03-007 -->
|
||||
|
||||
<!-- tags: 通配符匹配, 正则匹配, 正则表达式, 模糊匹配, 判断是否符合模式 -->
|
||||
<!-- tags: 正则匹配, 正则表达式, 模糊匹配, 判断是否符合模式, 默认不区分大小写, 数值近似相等, like 精度 -->
|
||||
|
||||
`like` 用于判断字符串是否符合指定模式(支持通配符和正则表达式):
|
||||
字符串 `like` 的右侧是正则表达式;默认匹配不区分大小写:
|
||||
|
||||
代码块身份:可直接照写示例
|
||||
|
||||
```tsl
|
||||
result1 := "hello" like "he*";
|
||||
result2 := "HELLO" like "he*";
|
||||
result3 := "test@example.com" like "*@*";
|
||||
result1 := "a" like "A";
|
||||
result2 := "HELLO" like "hello";
|
||||
result3 := "2009-1-1" like "\\d+-\\d+-\\d+";
|
||||
writeLn("result1:", result1);
|
||||
writeLn("result2:", result2);
|
||||
writeLn("result3:", result3);
|
||||
@@ -439,22 +469,49 @@ writeLn("result3:", result3);
|
||||
|
||||
```text
|
||||
result1: 1
|
||||
result2: 0
|
||||
result2: 1
|
||||
result3: 1
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `like` 大小写敏感,`"hello" like "he*"` 返回 `1`(真),`"HELLO" like "he*"` 返回 `0`(假)
|
||||
- `*` 是通配符,匹配任意字符序列
|
||||
- `like` 也支持正则表达式模式(如 `"\\d{4}-\\d{2}-\\d{2}"` 匹配日期格式)
|
||||
- 默认情况下,`"a" like "A"` 和 `"HELLO" like "hello"` 都返回 `1`(真)
|
||||
- `"\\d+-\\d+-\\d+"` 是正则表达式,匹配示例中的日期形态
|
||||
- 正则量词 `*` 只修饰它前面的原子;不要把 `"he*"` 解释成“`he` 后面任意字符”的 glob 模式,需要任意字符序列时写成 `.*`
|
||||
- 匹配控制标志可以改变大小写规则;控制标志的精确 API、参数与目标 scope 使用 `tsl-api-reference` skill 核对
|
||||
|
||||
数值 `like` 用于近似相等判断:
|
||||
|
||||
代码块身份:可直接照写示例
|
||||
|
||||
```tsl
|
||||
old_abs_eps := likeEps;
|
||||
old_rate_eps := likeEpsRate;
|
||||
likeEps := 0.1;
|
||||
likeEpsRate := 0;
|
||||
writeLn(1.05 like 1);
|
||||
likeEps := old_abs_eps;
|
||||
likeEpsRate := old_rate_eps;
|
||||
```
|
||||
|
||||
代码块身份:输出片段
|
||||
|
||||
```text
|
||||
1
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `likeEps` 和 `likeEpsRate` 都可读写,默认值均为 `1e-6`
|
||||
- 两数绝对差小于 `likeEps`,或“绝对差 / 两数绝对值的平均值”小于 `likeEpsRate` 时,数值 `like` 返回真
|
||||
- 临时修改阈值后要恢复旧值,避免影响同一运行时中的后续比较
|
||||
|
||||
`not like` 是取反形式(TSL 2025/8 版本起支持):
|
||||
|
||||
代码块身份:可直接照写示例
|
||||
|
||||
```tsl
|
||||
result := "abc" not like "xyz*";
|
||||
result := "abc" not like "xyz.*";
|
||||
writeLn(result);
|
||||
```
|
||||
|
||||
@@ -547,6 +604,10 @@ items := array(1, 2, 3);
|
||||
- 把普通字符串里的 `\uXXXX` 直接当成宽串单字符。
|
||||
- 把 `U""` 当成宽串;需要宽串时按本页转换链处理。
|
||||
- 把 `#0` / `\0` 当成 C 风格字符串终止符。
|
||||
- 把字符串 `like` 误写成默认区分大小写;默认行为是不区分大小写。
|
||||
- 把 `like` 只理解成字符串模式匹配,遗漏数值近似相等语义。
|
||||
- 修改 `likeEps` / `likeEpsRate` 后不恢复,导致后续代码继承意外的比较阈值。
|
||||
- 以为给较大整数下标赋值后,中间所有位置都会自动生成实际元素;未写入的位置仍为 `nil`。
|
||||
|
||||
代码块身份:反例 / 不可照写
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<!-- section-id: syntax-04-001 -->
|
||||
|
||||
回答“普通变量怎样直接使用、`var` 在什么位置出现、常量必须怎样初始化、哪些名字一旦绑定就不能再赋值”。
|
||||
回答“普通变量怎样直接使用、`var` 在什么位置出现、常量必须怎样初始化、`global` 怎样跨函数共享,以及运行时常量怎样冻结一次调用中的表达式结果”。
|
||||
|
||||
## 核心规则
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
- 右侧数组元素也可以是数组;拆出来的变量会直接得到对应子数组。
|
||||
- 多参数赋值也可以出现在函数调用参数里。
|
||||
- `{$explicit+}` 开启后,后续变量必须先用 `var` 声明;未声明变量会报 `variable not defined`。
|
||||
- `global x, y;` 声明当前任务中的全局变量;每个需要访问它的函数都要再次写 `global` 声明。
|
||||
- `const name := expression;` 是运行时常量:进入所属函数时计算,当前调用内不可再次赋值;不要与编译时常量 `const name = expression;` 混写。
|
||||
|
||||
## 可直接照写示例
|
||||
|
||||
@@ -178,6 +180,71 @@ const kMaxRetries = 3 + 4;
|
||||
value := kMaxRetries;
|
||||
```
|
||||
|
||||
### 全局变量 `global`
|
||||
|
||||
<!-- section-id: syntax-04-009 -->
|
||||
|
||||
<!-- tags: 全局变量, 跨函数共享变量, 不同函数访问同一变量, global 声明 -->
|
||||
|
||||
`global` 让同一运行任务中的不同函数访问同一变量。每个需要读写该变量的函数都要声明它;漏写时,同名标识符会按局部变量处理。
|
||||
|
||||
代码块身份:可直接照写示例
|
||||
|
||||
```tsl
|
||||
global shared_value;
|
||||
shared_value := 37;
|
||||
writeLn(ReadShared());
|
||||
|
||||
function ReadShared();
|
||||
begin
|
||||
global shared_value;
|
||||
return shared_value;
|
||||
end;
|
||||
```
|
||||
|
||||
代码块身份:输出片段
|
||||
|
||||
```text
|
||||
37
|
||||
```
|
||||
|
||||
本节只拥有“当前任务内跨函数共享”和“每个引用函数都要声明”的规则;不要从中推断跨任务持久化、线程隔离或进程级生命周期。
|
||||
|
||||
### 运行时常量 `const name := expression`
|
||||
|
||||
<!-- section-id: syntax-04-010 -->
|
||||
|
||||
<!-- tags: 运行时常量, 表达式常量, 每次调用重新计算, 初始化后不能修改 -->
|
||||
|
||||
运行时常量使用 `:=` 初始化,可以依赖参数、变量或函数调用。它在每次进入所属函数时重新计算一次,随后在本次调用内不可修改。
|
||||
|
||||
代码块身份:可直接照写示例
|
||||
|
||||
```tsl
|
||||
writeLn(FreezeValue(2));
|
||||
writeLn(FreezeValue(5));
|
||||
|
||||
function FreezeValue(value);
|
||||
begin
|
||||
const fixed := value + 1;
|
||||
return fixed;
|
||||
end;
|
||||
```
|
||||
|
||||
代码块身份:输出片段
|
||||
|
||||
```text
|
||||
3
|
||||
6
|
||||
```
|
||||
|
||||
边界:
|
||||
|
||||
- 编译时常量写 `const name = expression;`;运行时常量写 `const name := expression;`
|
||||
- 运行时常量声明放在函数 `begin ... end` 语句体中,不放在函数头后的 `const` 声明段
|
||||
- 运行时常量不作为类成员写法
|
||||
- 初始化后再次赋值会破坏常量约束,不要生成
|
||||
|
||||
### 多参数赋值
|
||||
|
||||
<!-- section-id: syntax-04-006 -->
|
||||
@@ -331,6 +398,9 @@ items := array(1, 2, 3);
|
||||
- 以为 `const` 可以只声明名字,不写初始化表达式。
|
||||
- 以为单变量拆包可以写成 `[a] := array(...)`。
|
||||
- 以为 `{$explicit+}` 开启后仍然可以继续直接写未声明变量。
|
||||
- 只在脚本顶层声明一次 `global`,却忘记在读取它的函数里再次声明。
|
||||
- 把运行时常量 `const name := expression;` 写进函数头后的编译时 `const` 声明段。
|
||||
- 把运行时常量当成可重新赋值的普通变量或类成员。
|
||||
|
||||
代码块身份:反例 / 不可照写
|
||||
|
||||
|
||||
@@ -50,7 +50,8 @@
|
||||
- 一旦某次调用里开始使用命名参数,后面的参数就不能再退回位置参数。
|
||||
- 对二进制函数 / 系统函数直接使用命名参数,会报 `named parameter mode can't support here`;这类函数要先用 TSL 再封一层。
|
||||
- 函数参数支持默认值。
|
||||
- 普通函数的默认值规则不要直接等同到 `unit interface` 声明;跨 `unit` 的默认参数边界只照本页最小反例和 [09_units_and_scope.md](09_units_and_scope.md) 处理。
|
||||
- `unit interface` 中的默认参数可以引用该接口中可访问的 `const`;实现函数头不要重复声明默认值。
|
||||
- 默认参数表达式属于新一代 TSL 能力;表达式里引用变量时,该变量按 `0` 求值。面向旧运行时或版本不明时,默认只生成字面量 / 可访问常量默认值。
|
||||
- 尾部 `...` 形式的可变参数属于文档明确写法。
|
||||
- 在可变参数函数体里,`Params`、`ParamCount`、`RealParamCount` 都可用。
|
||||
- 可变参数组可以通过 `...` 转发给另一个函数调用。
|
||||
@@ -61,6 +62,7 @@
|
||||
- 匿名函数和 TSL 函数值的稳定调用方式仍是 `call(f, ...)` 或 `##f(...)`。
|
||||
- `f(...)` 这种“函数变量直接调用”写法不作为可写事实;无论 `f` 是匿名函数、`findFunction(...)` 还是 `thisFunction(...)` 返回的函数指针,都不要默认写成直调。
|
||||
- `::FuncName(...)` 可以指向全局/系统函数,用来绕过当前作用域里的同名局部函数。
|
||||
- `system.FuncName(...)` 专门强制指定系统函数;它与 `::FuncName(...)` 的全局限定语义不要互相替代。
|
||||
- `external`、原生函数指针包装、`makeInstance` / C 回调和线程调用的事实见 [17_external_calls_and_threads.md](17_external_calls_and_threads.md)。
|
||||
- 不要在 `.tsl` 的函数声明区之后继续追加脚本语句。
|
||||
|
||||
@@ -502,7 +504,7 @@ begin
|
||||
end;
|
||||
```
|
||||
|
||||
默认值也可以写成表达式:
|
||||
新一代 TSL 的默认值也可以写成表达式:
|
||||
|
||||
代码块身份:可直接照写示例
|
||||
|
||||
@@ -523,24 +525,56 @@ end;
|
||||
- `Pack(a: 1)` 返回 `12`
|
||||
- `ExprDefault()` 返回 `3`
|
||||
|
||||
`unit interface` 声明下的默认参数要单独看。普通函数默认参数可用,不等于跨 `unit` 声明边界也同样可靠。
|
||||
表达式默认值的版本边界:
|
||||
|
||||
代码块身份:反例 / 不可照写
|
||||
代码块身份:可直接照写示例
|
||||
|
||||
```text
|
||||
unit UnitConst;
|
||||
interface
|
||||
|
||||
const default_value = 888;
|
||||
function F(a, b = 100, c = default_value);
|
||||
```tsl
|
||||
function RefDefault(a, b = a + 1);
|
||||
begin
|
||||
return b;
|
||||
end;
|
||||
```
|
||||
|
||||
边界说明:
|
||||
|
||||
- `F(1)` 输出 `101`,`F(1, 2)` 输出 `3`
|
||||
- 同一组文件下,`UnitConst.default_value` 可读到 `888`,而 `F(1, 2, 3)` 输出 `6`
|
||||
- 因此不要把“普通函数默认参数可用”直接泛化成“`unit interface` 里引用 `unit const` 的默认参数也同样可靠”
|
||||
- 这类跨 `unit` 的声明边界事实见 [09_units_and_scope.md](09_units_and_scope.md)
|
||||
- 在支持默认参数表达式的新一代 TSL 中,`RefDefault(5)` 返回 `1`,因为默认表达式里的变量 `a` 按 `0` 求值
|
||||
- 这项能力自 2025-08-27 后的 NG 客户端 / 新一代 TSL 服务端提供;目标版本不明时不要生成变量参与的默认表达式
|
||||
|
||||
`unit interface` 可以使用接口中可访问的常量作为默认值:
|
||||
|
||||
代码块身份:配置片段 / 概念骨架
|
||||
|
||||
```text
|
||||
// UnitDefaults.tsf
|
||||
unit UnitDefaults;
|
||||
|
||||
interface
|
||||
|
||||
const default_value = 888;
|
||||
function F(a, b = 100, c = default_value);
|
||||
|
||||
implementation
|
||||
|
||||
function F(a, b, c);
|
||||
begin
|
||||
return a + b + c;
|
||||
end;
|
||||
|
||||
end.
|
||||
|
||||
// main.tsl
|
||||
uses UnitDefaults;
|
||||
echo F(1), ",", F(1, 2);
|
||||
```
|
||||
|
||||
代码块身份:输出片段
|
||||
|
||||
```text
|
||||
989,891
|
||||
```
|
||||
|
||||
说明:默认值只在 `interface` 声明处写一次;实现函数头使用同一组形参,但不重复 `= ...`。
|
||||
|
||||
### 可变参数 `...`
|
||||
|
||||
@@ -686,7 +720,12 @@ end;
|
||||
|
||||
<!-- section-id: syntax-05-011 -->
|
||||
|
||||
<!-- tags: lambda, 闭包, 函数当变量, 函数当参数, 回调, 函数指针, 把函数存起来 -->
|
||||
<!-- tags: lambda, 闭包, 函数当变量, 函数当参数, 回调, 函数指针, 把函数存起来, 匿名函数作用域, 对象成员, 宿主函数, 临时变量, 捕获外层变量 -->
|
||||
|
||||
匿名函数作用域边界:
|
||||
|
||||
- 匿名函数定义在类的方法中时,可以访问当前对象的成员和方法。
|
||||
- 匿名函数不能访问宿主函数的临时变量;不要把它当成会捕获外层临时变量的词法闭包。
|
||||
|
||||
匿名函数变量:
|
||||
|
||||
@@ -851,7 +890,26 @@ end;
|
||||
|
||||
<!-- section-id: syntax-05-013 -->
|
||||
|
||||
`external`、原生函数指针包装、`makeInstance` / C 回调和线程调用,统一见 [17_external_calls_and_threads.md](17_external_calls_and_threads.md)。这一篇只保留“普通函数怎样定义和调用”的主线。
|
||||
<!-- tags: system 前缀, 指定系统函数, 绕过同名用户函数, 系统函数限定 -->
|
||||
|
||||
系统函数可能与用户定义函数同名。需要明确指定系统实现时,使用 `system.FuncName(...)`:
|
||||
|
||||
代码块身份:可直接照写示例
|
||||
|
||||
```tsl
|
||||
value := system.strToInt("123");
|
||||
writeLn(value);
|
||||
```
|
||||
|
||||
代码块身份:输出片段
|
||||
|
||||
```text
|
||||
123
|
||||
```
|
||||
|
||||
本节只拥有 `system.` 这一调用限定语法。示例中的真实函数名、签名和目标 scope 必须由 `tsl-api-reference` skill 核对;不要从本节推断任意系统 API。
|
||||
|
||||
`external`、原生函数指针包装、`makeInstance` / C 回调和线程调用,统一见 [17_external_calls_and_threads.md](17_external_calls_and_threads.md)。
|
||||
|
||||
## 默认生成模板
|
||||
|
||||
@@ -906,7 +964,10 @@ end;
|
||||
- 把 `a = 1` 这种比较表达式误当成命名参数调用。
|
||||
- 以为默认值只能用于无类型参数。
|
||||
- 在 `const` 形参上直接赋值。
|
||||
- 把普通函数的默认值规则原样套到 `unit interface` 里的 `const` 默认参数上。
|
||||
- 在旧运行时或版本不明时生成默认参数表达式。
|
||||
- 以为默认参数表达式里的形参会取本次调用实参;其中变量按 `0` 求值。
|
||||
- 在 `unit interface` 和 `implementation` 的函数头上重复写默认值。
|
||||
- 用 `::FuncName(...)` 和 `system.FuncName(...)` 互相替代,而不区分全局限定与系统函数限定。
|
||||
- 把匿名函数或 `findFunction(...)` 返回值默认写成 `f(...)` 直调。
|
||||
- 把命名参数直接套到二进制函数或系统函数上。
|
||||
- 在一次调用里先进入命名参数模式,后面又退回位置参数。
|
||||
@@ -995,7 +1056,7 @@ const default_value = 888;
|
||||
function F(a, b = 100, c = default_value);
|
||||
```
|
||||
|
||||
不要把上面这种 `unit interface` 声明直接当成已经等价于普通函数默认参数规则。按文档结果,对应的 `F(1)` 输出是 `101`,不是按 `default_value = 888` 补成的结果;具体边界见 [09_units_and_scope.md](09_units_and_scope.md)。
|
||||
上面的片段缺少 `implementation` 和 `end.`,因此不能作为完整 `unit` 文件直接照写;它不是“接口常量不能作为默认值”的反例。完整可写结构见本页默认参数章节。
|
||||
|
||||
代码块身份:反例 / 不可照写
|
||||
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
- 连续标量比较才用 `:>`、`:<`、`:<>`、`:==`、`:>=`、`:<=`。
|
||||
- 数组逐元素链式比较才用 `::>`、`::<`、`::<>`、`::==`、`::>=`、`::<=`。
|
||||
- 混合两类以上运算符时,优先用括号明确分组,不依赖跨语言记忆里的优先级。
|
||||
- TSL 的主要优先级从高到低是:成员/下标/调用,`not`、前置自增减与倒数/逆等一元运算,字符串 `$`,幂,乘除移位,加减(包括一元正负号)与集合/位运算,比较与 `in`/`like`/`is`,`and`/`or`,冒号,赋值,表达式前导 `@`;同级通常从左到右。
|
||||
|
||||
边界规则:
|
||||
|
||||
@@ -566,6 +567,113 @@ writeLn(if 2 > 1 then 2 else 1);
|
||||
|
||||
`if condition then true_value else false_value` 必须带 `else`,否则不是本页可照写的表达式形态。
|
||||
|
||||
### 运算符优先级
|
||||
|
||||
<!-- section-id: syntax-06-015 -->
|
||||
|
||||
<!-- tags: 运算符优先级, 谁先计算, 先乘除后加减, and or 优先级, 表达式加括号 -->
|
||||
|
||||
下表是面向当前正式文档已收录运算符的保守分组,数字越小优先级越高:
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
| 级别 | 主要形态 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 0 | `()`、`[]`、`.`、`?.`、函数调用 | 分组、访问、下标与调用最先结合。 |
|
||||
| 1 | `not`、前置 `++` / `--`、`!`、`.!`、`.!!` | 逻辑非、前置自增减与倒数/逆等一元运算。 |
|
||||
| 2 | `$` | 字符串连接。 |
|
||||
| 3 | `^`、`~`、`:^` | 幂、开方与对应矩阵形态。 |
|
||||
| 4 | `*`、`/`、`\`、`%`、`div`、`mod`、`shl`、`shr`、`rol`、`ror` | 乘除、取余与移位。 |
|
||||
| 5 | `+`、`-`、一元正号 / 负号、集合运算、点前缀位/逻辑运算 | 加减、一元正负号及同组语言运算;因此幂先于一元正负号结合,例如 `-2 ^ 2` 等于 `-(2 ^ 2)`。 |
|
||||
| 6 | 比较、`is`、`in`、`sqlin`、`like`、链式比较 | 关系判断。 |
|
||||
| 7 | `and`、`or` | 低于比较,因此 `a > 1 and b < 2` 按两个比较再逻辑与理解。 |
|
||||
| 8 | `:` | 冒号相关表达式形态。 |
|
||||
| 9 | `:=` 与各类复合赋值 | 赋值接近最低优先级。 |
|
||||
| 10 | `@` | 表达式对象前导最低。 |
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
同级运算通常从左到右求值。为了避免不同语言之间的优先级记忆混淆,混合两类以上运算符时仍推荐显式加括号:
|
||||
|
||||
代码块身份:可直接照写示例
|
||||
|
||||
```tsl
|
||||
value := 1 + 2 * 3;
|
||||
signedPower := -2 ^ 2;
|
||||
flag := (2 > 1) and (1 < 2);
|
||||
writeLn(value);
|
||||
writeLn(signedPower);
|
||||
writeLn(flag);
|
||||
```
|
||||
|
||||
代码块身份:输出片段
|
||||
|
||||
```text
|
||||
7
|
||||
-4
|
||||
1
|
||||
```
|
||||
|
||||
稀有矩阵运算符的详细优先级以其专题页为准;不要用本表外推尚未写入正式文档的符号。
|
||||
|
||||
### 静态计算表达式 `static`
|
||||
|
||||
<!-- section-id: syntax-06-016 -->
|
||||
|
||||
<!-- tags: 静态计算, 只计算一次, 表达式缓存, static name, 按键缓存 -->
|
||||
|
||||
表达式前的 `static` 会缓存第一次计算结果。带 `name` 时,名称表达式是缓存键:同一键复用第一次结果,不同键分别计算。
|
||||
|
||||
代码块身份:可直接照写示例
|
||||
|
||||
```tsl
|
||||
echo StaticValue(), ",", StaticValue();
|
||||
|
||||
function StaticValue();
|
||||
begin
|
||||
return static NextValue();
|
||||
end;
|
||||
|
||||
function NextValue();
|
||||
begin
|
||||
global static_calls;
|
||||
static_calls += 1;
|
||||
return static_calls;
|
||||
end;
|
||||
```
|
||||
|
||||
代码块身份:输出片段
|
||||
|
||||
```text
|
||||
1,1
|
||||
```
|
||||
|
||||
按键分别缓存:
|
||||
|
||||
代码块身份:可直接照写示例
|
||||
|
||||
```tsl
|
||||
echo NamedValue("A"), ",", NamedValue("A"), ",", NamedValue("B");
|
||||
|
||||
function NamedValue(key);
|
||||
begin
|
||||
return static NextValue() name key;
|
||||
end;
|
||||
|
||||
function NextValue();
|
||||
begin
|
||||
global named_static_calls;
|
||||
named_static_calls += 1;
|
||||
return named_static_calls;
|
||||
end;
|
||||
```
|
||||
|
||||
代码块身份:输出片段
|
||||
|
||||
```text
|
||||
1,1,2
|
||||
```
|
||||
|
||||
这不是类成员的 `static` 字段;类静态成员见 [08_objects_and_classes.md](08_objects_and_classes.md)。缓存结果具有运行时状态,不要用它保存每次调用都必须重新计算的值。
|
||||
|
||||
### 表达式对象
|
||||
|
||||
<!-- section-id: syntax-06-007 -->
|
||||
@@ -847,6 +955,8 @@ writeLn(value);
|
||||
- 把 `if` 表达式写成没有 `else` 的半句。
|
||||
- 把本页明确的 `c?.a?.[1]` 外推成所有深链式空安全访问都可靠。
|
||||
- 从其他语言推断 TSL 运算符能力。
|
||||
- 把类成员 `static field;` 与表达式前导 `static Expression [name Key]` 当成同一种语法。
|
||||
- 在复杂混合表达式里依赖其他语言的优先级记忆而省略括号。
|
||||
|
||||
代码块身份:反例 / 不可照写
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
- 块式分支内部的普通语句必须用分号结尾。
|
||||
- 控制流块的 `begin ... end` 后可以加分号也可以不加(语法都允许)。
|
||||
- `for` 支持 `to`、`downto`、可选 `step`,以及 `for i, v in array` 遍历。
|
||||
- 计数 `for` 的初值、终值和步长确定后,循环次数随之固定;循环体内不要给控制变量赋值。
|
||||
- `for i, v in array` 遍历期间,不要修改被遍历数组或其中元素。
|
||||
- `while` 和 `repeat ... until` 都可直接使用;`repeat` 至少会先执行一轮再判断结束条件。
|
||||
- `break` 会跳出当前最近一层循环,`continue` 会跳过当前轮剩余语句。
|
||||
- `case ... of ... else ... end` 可作为语句形态生成;`end` 后可以加分号也可以不加。
|
||||
@@ -154,6 +156,7 @@ for i, value in numbers do
|
||||
|
||||
- 依次输出 `10`、`120`、`230`
|
||||
- 这说明 `for i, value in numbers` 里的 `i` 从 `0` 开始
|
||||
- 遍历期间把 `numbers` 当成只读集合;需要修改时先遍历副本,或在循环结束后统一写回
|
||||
|
||||
代码块身份:输出片段
|
||||
|
||||
@@ -495,6 +498,8 @@ end
|
||||
- 以为 `try ... finally` 会吞掉异常。
|
||||
- 在还没搞清表达式规则前,先把复杂业务函数塞进条件里。
|
||||
- 把控制流问题和函数文件模型问题混在一起排查。
|
||||
- 在计数 `for` 循环体里给控制变量赋值。
|
||||
- 在 `for ... in` 遍历期间修改被遍历数组或其中元素。
|
||||
|
||||
代码块身份:反例 / 不可照写
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
- `setSysParam(key, value)` 和 `getSysParam(key)` 可以直接用字符串键。
|
||||
- `sysParams[key]` 可以直接读写这些运行时参数。
|
||||
- 块环境语句可写成 `with *, sys_param_values do begin ... end` 或 `with **, sys_param_values do begin ... end`。
|
||||
- `with *` 会把提供的系统参数合并进当前运行时上下文;不要依赖它在块结束后自动恢复外层值。
|
||||
- `with *` 会把提供的系统参数合并进当前运行时上下文。普通自定义键可能在块后保留新值;特殊系统环境变量会在块结束后恢复,因此不要把两类键的恢复行为混为一谈。
|
||||
- `with **` 会用提供的系统参数建立隔离块环境;块结束后恢复外层系统参数。
|
||||
- 后缀 `with` 形式写在函数文件调用后面:`#Func() with array(...)`。
|
||||
- `with array(...)` 只在该次调用里临时覆盖对应键,调用结束后会恢复外部原值。
|
||||
@@ -110,7 +110,8 @@ writeLn(getSysParam("b"));
|
||||
|
||||
- 块内输出 `2`、`3`
|
||||
- 块后输出 `2`、`3`
|
||||
- 说明 `with *` 会把传入键合并进当前系统参数上下文;不要把它当成自动恢复外层值的隔离块
|
||||
- 这组实验只证明普通自定义键 `"a"` / `"b"` 会合并并在块后保留新值
|
||||
- 特殊系统环境变量在块结束后会恢复;不要用普通字符串键的结果外推股票、日期等特殊环境
|
||||
|
||||
`with *, SysParamArray do` 使用当前所有系统参数:
|
||||
|
||||
@@ -443,7 +444,7 @@ end;
|
||||
|
||||
- 把系统参数页直接写成金融函数页。
|
||||
- 把 `#Func() with array(...)` 误判成也能直接套在本地函数 `Demo()` 后面。
|
||||
- 把 `with *` 误判成会自动恢复外层系统参数。
|
||||
- 以为 `with *` 对所有键都统一“不恢复”或统一“恢复”;普通自定义键与特殊系统环境变量的边界不同。
|
||||
- 以为 `with array(...)` 改的是全局永久值,不会恢复外层原环境。
|
||||
- 把网格句柄直接当最终值用,而不做 `dupvalue(...)`。
|
||||
- 以为从全局缓存取出的值,本地写入后仍然保持缓存身份。
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
<!-- section-id: syntax-11-004 -->
|
||||
|
||||
<!-- tags: 建数组, 数组下标, 数组索引, array 下标, 字典, 键值对, 二维数组, 嵌套数组, 按名字取值 -->
|
||||
<!-- tags: 建数组, 字典, 键值对, 二维数组, 嵌套数组, 按名字取值 -->
|
||||
|
||||
顺序数组与字符串键表:
|
||||
|
||||
@@ -299,7 +299,7 @@ writeLn("子集 (1,0):", subset[1][0]);
|
||||
|
||||
<!-- section-id: syntax-11-011 -->
|
||||
|
||||
<!-- tags: 长度不一致, 缺位补零, 标量广播, 数组和数字运算 -->
|
||||
<!-- tags: 长度不一致, 缺位补零, 标量广播, 数组和数字运算, 按行广播, 二维矩阵乘一维数组 -->
|
||||
|
||||
基础算符作用于非完全矩阵(行长度不一致或字符串键不对齐的数组)时,对应位置不存在或为 `nil` 时**默认当 0 处理**:
|
||||
|
||||
@@ -356,6 +356,32 @@ writeLn("(1,1):", result[1][1]);
|
||||
- `matrix_value + 10` 每个元素都加 10
|
||||
- 这些是逐元素运算(element-wise),区别于矩阵乘法 `:*`,见 [21_matrix_deep_dive.md](21_matrix_deep_dive.md)
|
||||
|
||||
二维矩阵与一维数组做基础算术时,一维数组按“行”广播;它的长度必须等于矩阵行数:
|
||||
|
||||
代码块身份:可直接照写示例
|
||||
|
||||
```tsl
|
||||
matrix_value := array((1, 2, 3), (4, 5, 6));
|
||||
row_factors := array(10, 100);
|
||||
left_result := matrix_value * row_factors;
|
||||
right_result := row_factors * matrix_value;
|
||||
writeLn(left_result[0][0]);
|
||||
writeLn(left_result[1][2]);
|
||||
writeLn(right_result[0][1]);
|
||||
writeLn(right_result[1][0]);
|
||||
```
|
||||
|
||||
代码块身份:输出片段
|
||||
|
||||
```text
|
||||
10
|
||||
600
|
||||
20
|
||||
400
|
||||
```
|
||||
|
||||
说明:第一行使用 `10`,第二行使用 `100`;左右操作数交换后仍按行广播。不要把这条规则误写成按列广播。
|
||||
|
||||
## 本页不生成的范围
|
||||
|
||||
<!-- section-id: syntax-11-012 -->
|
||||
@@ -383,3 +409,4 @@ writeLn("(1,1):", result[1][1]);
|
||||
- 不要把普通 `array(...)` 自动升级成 `FMArray`;`FMArray` 专属事实见 [22_fmarray.md](22_fmarray.md)。
|
||||
- 不要把点前缀比较 `.>` 和矩阵链式比较 `::>` 混用;`.>` 返回逻辑数组,`::>` 是链式比较。
|
||||
- 不要以为非完全矩阵缺位会报错;默认当 `0` 处理。
|
||||
- 二维矩阵与一维数组运算时,不要把一维数组当成按列因子;它按行广播且长度要匹配行数。
|
||||
|
||||
@@ -19,8 +19,9 @@
|
||||
- 在一维数组上做 TS-SQL 时,优先使用 `thisRow` 和 `thisRowIndex`。
|
||||
- `select` 返回二维结果,`sselect` 返回一维结果,`vselect` 返回单值,`mselect` 返回 `Matrix`。
|
||||
- `where`、`group by`、`order by` 可以直接接在 `from` 后面继续使用;`order by` 支持 `asc`/`desc` 与多列逗号分隔。
|
||||
- 分组后按聚集条件筛选用 `having`(`where` 不能用聚集);`having` 里用 `countof([字段])` 或 `countof(1)`,不要用 `countof(*)`。
|
||||
- 分组后按聚集条件筛选用 `having`(`where` 不能用聚集);计数可用 `countof([字段])`、`countof(1)`、`countof()`,也可用带空格的 `countof( * )`。无空格的 `countof(*)` 会与块注释起始符 `(*` 冲突。
|
||||
- 多表 `join` 时,字段访问应写成 `[表序号].["字段名"]`;`on` 可用 `and` 写多条件;`[表序号].*` 取整表列。
|
||||
- 多表联接中,`thisRow(表序号)` 和 `thisRowIndex(表序号)` 分别取得指定来源表的当前整行与原始下标。
|
||||
- 联接类型:`left join` 保留左表、`right join` 保留右表、`full join` 保留双方、`cross join` 笛卡尔积、逗号联接等价于 `cross join`;不匹配处用 `nil` 填充。
|
||||
- `select` 列表支持 `distinct` 去重、`as 别名`、`as nil`(参与计算但不返回)、`起始列 to 结束列` 字段区间、`selectopt(位选项)`、`drange(区间/M of N)`。
|
||||
- 聚集函数统一形态 `Func(Expr[, Cond[, N[, MovingFirst[, CacheId]]]])`:条件聚集、移动聚集、多字段聚集、`refof(Expr, N)` 引用相对行;`aggof('名', Expr)` 调用自定义聚集回调。
|
||||
@@ -233,6 +234,23 @@ writeLn(join_result[0]["V2"]);
|
||||
100
|
||||
```
|
||||
|
||||
联接上下文里的指定来源当前行:
|
||||
|
||||
代码块身份:可直接照写示例
|
||||
|
||||
```tsl
|
||||
left_rows := array(("id": 1, "v": 10), ("id": 2, "v": 20));
|
||||
right_rows := array(("id": 2, "v": 20), ("id": 3, "v": 30));
|
||||
join_rows := select thisRow(1) as "LeftRow",
|
||||
thisRow(2) as "RightRow",
|
||||
thisRowIndex(1) as "LeftIndex",
|
||||
thisRowIndex(2) as "RightIndex"
|
||||
from left_rows join right_rows on [1].["id"] = [2].["id"]
|
||||
end;
|
||||
```
|
||||
|
||||
结果说明:唯一匹配行中,`LeftRow["v"] = 20`、`RightRow["v"] = 20`、`LeftIndex = 1`、`RightIndex = 0`。
|
||||
|
||||
### `thisGroup`
|
||||
|
||||
<!-- section-id: syntax-13-011 -->
|
||||
@@ -773,6 +791,29 @@ writeLn(ref_prev[1]["Expr1"]);
|
||||
- 移动聚集:`avgof(表达式, 条件, N, MovingFirst)` 取当前行往前 N 条的滑动统计
|
||||
- `refof(表达式, N)` 引用前 N 行的值(`N` 为负则往后);首行无前值时返回 `0`
|
||||
|
||||
`*` 作为聚集输入时必须和左括号留空格,避免 `(*` 被词法层识别为块注释:
|
||||
|
||||
代码块身份:可直接照写示例
|
||||
|
||||
```tsl
|
||||
a := array(("x": 1, "y": 10), ("x": 3, "y": 20));
|
||||
row_count := vselect countof( * ) from a end;
|
||||
column_avg := select avgof( * ) from a end;
|
||||
writeLn(row_count);
|
||||
writeLn(column_avg[0]["Expr1"]);
|
||||
writeLn(column_avg[0]["Expr2"]);
|
||||
```
|
||||
|
||||
代码块身份:输出片段
|
||||
|
||||
```text
|
||||
2
|
||||
2.0
|
||||
15.0
|
||||
```
|
||||
|
||||
说明:`avgof( * )` 对每列分别聚集;这里两列平均值依次为 `2`、`15`。
|
||||
|
||||
### `group by ... having`
|
||||
|
||||
<!-- section-id: syntax-13-024 -->
|
||||
@@ -800,7 +841,7 @@ A
|
||||
说明:
|
||||
|
||||
- `having 聚集条件` 在分组后筛选(上例只保留成员数大于 1 的 `A` 组)
|
||||
- `having` 里的计数用 `countof([字段])` 或 `countof(1)`
|
||||
- `having` 里的计数优先用 `countof([字段])`、`countof(1)` 或 `countof()`;确需星号时写成 `countof( * )`
|
||||
|
||||
代码块身份:反例 / 不可照写
|
||||
|
||||
@@ -808,7 +849,7 @@ A
|
||||
having_rows := select ["cls"] from a group by ["cls"] having countof(*) > 1 end;
|
||||
```
|
||||
|
||||
`countof(*)` 这种带 `*` 的写法不成立,会报 `CountOf ( not found`。计数改用 `countof([字段])` 或 `countof(1)`。
|
||||
无空格的 `countof(*)` 会把 `(*` 词法组合解释成块注释开头,随后报 `CountOf ( not found`。改用 `countof( * )`、`countof()`、`countof([字段])` 或 `countof(1)`。
|
||||
|
||||
### `thisOrder` 与多列 `order by`
|
||||
|
||||
@@ -967,7 +1008,7 @@ query_result := select * from source_rows end;
|
||||
- 在 `left join` 时省略 `on` 子句或不用 `[表序号].["字段"]` 形式。
|
||||
- 在 `insert` 时漏掉 `insertfields` 或字段数与值数不匹配。
|
||||
- 期望 `update`/`delete` 返回新数组;它们直接修改原数组。
|
||||
- 用 `countof(*)` 数行数;`*` 星号形式不被支持,改用 `countof([字段])` 或 `countof(1)`。
|
||||
- 写无空格的 `countof(*)`,使 `(*` 与块注释起始符冲突;改用 `countof( * )`、`countof()`、`countof([字段])` 或 `countof(1)`。
|
||||
|
||||
代码块身份:反例 / 不可照写
|
||||
|
||||
@@ -975,7 +1016,7 @@ query_result := select * from source_rows end;
|
||||
n := vselect countof(*) from source_rows end;
|
||||
```
|
||||
|
||||
`countof(*)` 会报 `CountOf ( not found`。数行数改用 `countof([字段])` 或 `countof(1)`。
|
||||
`countof(*)` 会报 `CountOf ( not found`,原因是 `(*` 与块注释起始符冲突。星号写法加空格为 `countof( * )`,或改用 `countof()` / 明确表达式。
|
||||
|
||||
代码块身份:反例 / 不可照写
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<!-- section-id: syntax-14-001 -->
|
||||
|
||||
回答“`goto`、`debugReturn`、`debugRunEnv`、`mtic` / `mtoc`、`setProfiler`、`__line__` 和 `__stack_frame` 怎样写、会怎样表现”。
|
||||
回答“`goto`、`debugReturn`、`debugRunEnv`、`mtic` / `mtoc`、`setProfiler`、内存伪变量、`__line__` 和 `__stack_frame` 怎样写、会怎样表现”。
|
||||
|
||||
## 核心规则
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
- `setProfiler(7)` 配合 `getProfilerInfo(1)`,可以在不弹窗的情况下拿到性能分析器信息。
|
||||
- `__line__` 会返回所在代码行号。
|
||||
- `__stack_frame` 会返回调用栈帧数组;最小 `toStn(...)` 观察结果里,每一项是 `(line, "function")` 这一类二元组。
|
||||
- `_myMem_` 表示应用当前已使用内存,`_maxMem_` 表示应用允许使用的最大内存;两者是只读数值伪变量。单位和 `_maxMem_` 的具体取值由宿主环境决定。
|
||||
|
||||
## 可直接照写示例
|
||||
|
||||
@@ -217,6 +218,32 @@ writeLn(length(info) > 0);
|
||||
- 说明 `setProfiler(7)` 可以开启性能分析器统计
|
||||
- 说明 `getProfilerInfo(1)` 会直接返回性能分析器信息,而且结果是非空数组
|
||||
|
||||
### 内存伪变量 `_myMem_` / `_maxMem_`
|
||||
|
||||
<!-- section-id: syntax-14-011 -->
|
||||
|
||||
<!-- tags: 已用内存, 内存上限, 最大内存, 内存伪变量, _myMem_, _maxMem_ -->
|
||||
|
||||
代码块身份:可直接照写示例
|
||||
|
||||
```tsl
|
||||
writeLn(ifNumber(_myMem_));
|
||||
writeLn(ifNumber(_maxMem_));
|
||||
```
|
||||
|
||||
代码块身份:输出片段
|
||||
|
||||
```text
|
||||
1
|
||||
1
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `_myMem_` 是应用已使用内存的数值
|
||||
- `_maxMem_` 是宿主允许应用使用的最大内存数值;某些环境可能返回 `0` 表示未给出可比较的上限
|
||||
- 不要假设 `_maxMem_ >= _myMem_`,也不要在没有宿主文档时写死单位
|
||||
|
||||
### `__line__` 与 `__stack_frame`
|
||||
|
||||
<!-- section-id: syntax-14-009 -->
|
||||
@@ -276,3 +303,4 @@ array(
|
||||
- 不要假设 `goto` 可以跨函数、跨脚本体或跳到单独成行的 `label`。
|
||||
- 不要给计时或性能分析器调用补未写入文档参数。
|
||||
- 不要把调试客户端副作用写成普通输出事实。
|
||||
- 不要假定 `_maxMem_` 总是非零、总是大于 `_myMem_`,或擅自指定内存单位。
|
||||
|
||||
+11
-21
@@ -13,7 +13,7 @@
|
||||
<!-- section-id: syntax-15-002 -->
|
||||
|
||||
- 标识符大小写无关;下划线可出现在标识符中。
|
||||
- `//` 是行注释;首行 `#!` 可作为 CGI 风格注释;`{ ... }` 与 `(* ... *)` 是块注释。
|
||||
- `//` 是行注释;首行 `#!` 可作为 CGI 风格注释;`{ ... }` 与 `(* ... *)` 是块注释。两种块注释可以交错嵌套,同类块注释不能嵌套。
|
||||
- 条件编译指令使用 `{$define}`、`{$undef}`、`{$ifdef}`、`{$ifndef}`、`{$else}`、`{$endif}`。
|
||||
- 条件编译只编译命中的分支;未命中的分支不参与脚本编译。
|
||||
- `{$explicit+}` 开启后,后续变量必须先用 `var` 声明;`{$explicit-}` 可以在同一源文件里重新关闭这个要求。
|
||||
@@ -58,7 +58,7 @@ TSL 关键字大小写无关;本表统一按文档推荐写法展示。生成
|
||||
|
||||
<!-- section-id: syntax-15-005 -->
|
||||
|
||||
<!-- tags: 注释怎么写, 大小写敏感吗, 变量命名, 下划线 -->
|
||||
<!-- tags: 注释怎么写, 大小写敏感吗, 变量命名, 下划线, 嵌套注释, 块注释 -->
|
||||
|
||||
大小写无关与下划线标识符:
|
||||
|
||||
@@ -112,6 +112,8 @@ writeLn(40);
|
||||
|
||||
- 依次输出 `1`、`10`、`30`
|
||||
- 说明首行 `#!`、`//`、`{ ... }`、`(* ... *)` 都属于文档明确注释形态
|
||||
- 说明外层 `{ ... }` 可以包含 `(* ... *)`;反向交错也可用
|
||||
- 同类 `{ { ... } }` 或 `(* (* ... *) *)` 不构成嵌套注释,会产生语法错误
|
||||
- 说明 `define` / `undef` / `ifdef` / `ifndef` / `else` / `endif` 这一组条件编译指令可以正常生效
|
||||
|
||||
### 显式变量声明开关
|
||||
@@ -184,27 +186,13 @@ writeLn(1);
|
||||
|
||||
<!-- tags: 编译开关, 编译器选项, 改默认行为 -->
|
||||
|
||||
`{$CompileOption}` 用于设置编译期开关,改变编译器的默认行为:
|
||||
TSL 的编译选项使用 `{$Option+}` / `{$Option-}` 一类指令改变后续源码的编译方式。本页只拥有已经分别验证并有专题规则的选项:
|
||||
|
||||
代码块身份:可直接照写示例
|
||||
- `{$explicit+}` / `{$explicit-}`:切换变量是否必须预先声明,见上一节
|
||||
- `{$varByRef+}` / `{$varByRef-}`:切换未修饰形参的默认传递方式,见下一节
|
||||
- `{$ifdef ...}` 等条件编译指令:控制分支是否参与编译
|
||||
|
||||
```tsl
|
||||
{$CompileOption optimize=1}
|
||||
echo 1 + 1;
|
||||
```
|
||||
|
||||
代码块身份:输出片段
|
||||
|
||||
```text
|
||||
2
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `{$CompileOption optimize=1}` 开启优化
|
||||
- 编译选项从出现位置开始生效,直到源文件结束或被其他选项覆盖
|
||||
- 常见选项包括 `optimize`、`buffermode`、`DebugInfo` 等
|
||||
- 编译选项细节以项目工具链和实际编译命令为准。
|
||||
`optimize`、`buffermode`、`DebugInfo` 等没有在本 skill 中形成可验证语义,不作为正式可生成选项。不要用“脚本仍能输出结果”来证明某个未知编译选项确实生效。
|
||||
|
||||
### 参数默认传递开关
|
||||
|
||||
@@ -263,6 +251,8 @@ end;
|
||||
<!-- section-id: syntax-15-010 -->
|
||||
|
||||
- 不要在 `{$explicit+}` 后继续直接使用未声明变量。
|
||||
- 不要同类嵌套 `{ ... }` 或 `(* ... *)` 块注释;需要嵌套时交错使用两种定界符。
|
||||
- 不要生成未在本页形成可验证规则的 `{$CompileOption optimize=...}`、`buffermode` 或 `DebugInfo`。
|
||||
- 不要把 `{$i ...}` / `{$include ...}` 包含文件写法当成可用能力。
|
||||
- 不要把 `反例 / 不可照写` 代码块复制进正向示例。
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
- 完整 `unit` 形态可以包含 `interface`、`implementation`、`initialization`、`finalization`,并以 `end.` 结束。
|
||||
- `initialization` 在 `unit` 第一次被实际使用时触发,不是只因为顶层写了 `uses` 就立刻执行。
|
||||
- `finalization` 会在脚本结束前触发。
|
||||
- 直接写 `DemoUnit.Member` 时,可以读到 `interface` 和 `implementation` 里的常量、变量。
|
||||
- 跨版本安全边界只保证 `interface` 中声明的常量、变量和函数可由引用者访问;只在 `implementation` 中声明的成员按私有内容处理。
|
||||
- `findFunction("DemoUnit")` 拿到的是 `unit` 对象入口;本页只把它稳定暴露 `interface` 成员写成文档事实。
|
||||
- `DemoUnit.var_name := value` 这种限定赋值不作为可写事实;如果要改 `unit` 状态,应导出函数或方法来改。
|
||||
- `tslfilename()` 的参数规格使用 `tsl-api-reference` skill 按名查询;本页只保留它返回正在执行的 `.tsl` 主脚本完整路径这一行为事实。
|
||||
@@ -111,7 +111,7 @@ FINAL
|
||||
|
||||
<!-- tags: 读模块常量, 访问 unit 成员, 限定名读取 -->
|
||||
|
||||
直接限定读取:
|
||||
跨版本安全的限定读取:
|
||||
|
||||
代码块身份:配置片段 / 概念骨架
|
||||
|
||||
@@ -149,8 +149,6 @@ uses DemoUnit;
|
||||
|
||||
writeLn(DemoUnit.public_const);
|
||||
writeLn(DemoUnit.public_var);
|
||||
writeLn(DemoUnit.impl_const);
|
||||
writeLn(DemoUnit.impl_var);
|
||||
writeLn(PublicFunc());
|
||||
```
|
||||
|
||||
@@ -158,10 +156,10 @@ writeLn(PublicFunc());
|
||||
|
||||
- `DemoUnit.public_const` 输出 `1`。
|
||||
- `DemoUnit.public_var` 输出 `3`。
|
||||
- `DemoUnit.impl_const` 输出 `2`。
|
||||
- `DemoUnit.impl_var` 输出 `4`。
|
||||
- `PublicFunc()` 输出 `10`。
|
||||
- 本页文档边界是:实现段函数仍私有,但实现段常量和变量可以通过 `DemoUnit.Member` 直接读取。
|
||||
- 本页的跨版本文档边界是:外部只依赖 `interface` 引出的成员;实现段里的常量、变量和函数都视为私有。
|
||||
|
||||
部分新一代解释器允许用 `DemoUnit.impl_const` / `DemoUnit.impl_var` 限定读取实现段数据,但这与经典 `unit` 可见性规则冲突,不作为跨环境默认生成能力。若项目已经依赖该行为,必须先按目标解释器实测并记录版本。
|
||||
|
||||
实现段函数的外部调用反例:
|
||||
|
||||
@@ -491,7 +489,7 @@ writeLn(Hello());
|
||||
<!-- section-id: syntax-18-011 -->
|
||||
|
||||
- 把 `DemoUnit.var_name := value` 当成可用的限定赋值。
|
||||
- 以为 `implementation` 里的常量和变量一定都不能从 `DemoUnit.Member` 读到。
|
||||
- 默认从外部读取只在 `implementation` 中声明的成员;跨版本安全代码应通过 `interface` 导出。
|
||||
- 以为 `findFunction("DemoUnit")` 暴露的成员范围和 `DemoUnit.Member` 完全相同。
|
||||
- 把脚本内的 `namespace "..."` 当成和 `tsl.conf` 里的 `Namespace=...` 叠加,而不是覆盖。
|
||||
- 把 `-LIBPATH` 放在脚本文件名前面。
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
<!-- section-id: syntax-22-004 -->
|
||||
|
||||
<!-- tags: 建 FMArray, 高性能数组, 字面量写法 -->
|
||||
<!-- tags: 建 FMArray, 高性能数组, 高性能矩阵, fmarray, 字面量写法 -->
|
||||
|
||||
代码块身份:可直接照写示例
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ STRUCTURAL_METADATA_RE = re.compile(
|
||||
r"<!--\s*(?:section-id|quickstart-rule)\s*:.*?-->",
|
||||
re.DOTALL | re.IGNORECASE,
|
||||
)
|
||||
HTML_COMMENT_RE = re.compile(r"<!--.*?-->", re.DOTALL)
|
||||
IDENTITY_PREFIX = "代码块身份:"
|
||||
BLOCK_DESCRIPTION_PREFIX = "代码块说明:"
|
||||
ALLOWED_IDENTITIES = {
|
||||
@@ -156,7 +157,6 @@ CHINESE_QUERY_PARTICLES = ("的", "是", "吗", "呢", "吧")
|
||||
ASCII_FILTER_STOP_TOKENS = {
|
||||
"debug",
|
||||
"please",
|
||||
"program",
|
||||
"tinysoft",
|
||||
"tsl",
|
||||
"tsf",
|
||||
@@ -480,7 +480,7 @@ def load_sections(references_dir: Path = DEFAULT_REFERENCES_DIR) -> list[Section
|
||||
raise ReferenceStructureError(f"重复 section ID:{base_id}")
|
||||
seen_ids.add(base_id)
|
||||
tags = _section_tags(local_body)
|
||||
searchable_body = STRUCTURAL_METADATA_RE.sub(" ", local_body)
|
||||
searchable_body = HTML_COMMENT_RE.sub(" ", local_body)
|
||||
searchable_text = normalize(
|
||||
"\n".join((page.stem, page_title, *heading_path, *tags, searchable_body))
|
||||
)
|
||||
@@ -1122,9 +1122,9 @@ def _text_contains_exact_query(text: str, query: str) -> bool:
|
||||
|
||||
def _intent_score(section: Section, query: str) -> int:
|
||||
aliases = PAGE_INTENT_ALIASES.get(section.page.name, ())
|
||||
return PAGE_INTENT_SCORE * sum(
|
||||
return PAGE_INTENT_SCORE if any(
|
||||
_query_contains_phrase(query, alias) for alias in aliases
|
||||
)
|
||||
) else 0
|
||||
|
||||
|
||||
def _has_chinese_context(section: Section, query: str) -> bool:
|
||||
@@ -1161,7 +1161,9 @@ def _tag_matched_tokens(tags: tuple[str, ...], query_token_set: set[str]) -> int
|
||||
def _code_text(body: str) -> str:
|
||||
inline = INLINE_CODE_RE.findall(body)
|
||||
fenced = FENCED_CODE_RE.findall(body)
|
||||
return normalize("\n".join((*inline, *fenced)))
|
||||
# 标识符信号只来自 ASCII 代码术语。中文散文会走标题、tag 和正文得分;
|
||||
# 若把围栏里的“下标数组”等输出标签也当标识符,中文查询会被样例值劫持。
|
||||
return normalize("\n".join(ASCII_TOKEN_RE.findall("\n".join((*inline, *fenced)))))
|
||||
|
||||
|
||||
def _score_section(section: Section, query: str, mode: str) -> ScoreBreakdown:
|
||||
@@ -1169,8 +1171,7 @@ def _score_section(section: Section, query: str, mode: str) -> ScoreBreakdown:
|
||||
tokens = query_tokens(query)
|
||||
heading_text = normalize("\n".join(section.heading_path))
|
||||
page_title_text = normalize(section.page_title)
|
||||
body_without_metadata = STRUCTURAL_METADATA_RE.sub(" ", section.local_body)
|
||||
body_text = normalize(SECTION_TAG_RE.sub(" ", body_without_metadata))
|
||||
body_text = normalize(HTML_COMMENT_RE.sub(" ", section.local_body))
|
||||
tag_text = normalize("\n".join(section.tags))
|
||||
term_text = _code_text(section.local_body)
|
||||
expanded_only_tokens = _synonym_tokens(query) - _base_query_tokens(query)
|
||||
@@ -1356,6 +1357,7 @@ def query_sections(
|
||||
)
|
||||
ranked.sort(
|
||||
key=lambda match: (
|
||||
match.weak,
|
||||
-match.score,
|
||||
*(-value for value in match.priority),
|
||||
match.section.page.as_posix(),
|
||||
@@ -1400,10 +1402,9 @@ def _safe_json_string(value: str) -> str:
|
||||
|
||||
|
||||
def _plain_text_summary(body: str, limit: int = 180) -> str:
|
||||
# 标签是检索元数据,不是事实正文;不能泄进候选摘要。
|
||||
without_metadata = STRUCTURAL_METADATA_RE.sub(" ", body)
|
||||
without_tags = SECTION_TAG_RE.sub(" ", without_metadata)
|
||||
without_fences = FENCED_CODE_RE.sub(" ", without_tags)
|
||||
# HTML 注释都是维护元数据,不是事实正文;不能泄进候选摘要。
|
||||
without_comments = HTML_COMMENT_RE.sub(" ", body)
|
||||
without_fences = FENCED_CODE_RE.sub(" ", without_comments)
|
||||
without_links = re.sub(
|
||||
r"!?\[([^\]]*)\]\([^)]+\)", lambda match: match.group(1), without_fences
|
||||
)
|
||||
@@ -1464,7 +1465,7 @@ def render_candidates(result: QueryResult) -> str:
|
||||
|
||||
|
||||
def render_section(section: Section) -> str:
|
||||
body = STRUCTURAL_METADATA_RE.sub("", section.body)
|
||||
body = HTML_COMMENT_RE.sub("", section.body)
|
||||
body = re.sub(r"\n{3,}", "\n\n", body).rstrip()
|
||||
lines = [
|
||||
"# TSL Syntax Section",
|
||||
|
||||
@@ -277,7 +277,19 @@ python tools/tsl-codegen/scripts/generate.py --dir tmp/api-recordings
|
||||
|
||||
生成器读取录入文件中的 `path`,默认写入
|
||||
`skills/tsl-api-reference/references/codegen/project/<path>.md`。不指定
|
||||
`--scope` 时,scope 就是 `project`
|
||||
`--scope` 时,scope 就是 `project`。该路径默认相对于当前工作目录
|
||||
|
||||
从其他目录运行生成器时,使用 `--root` 指定包含 `skills/` 的项目根目录。例如在
|
||||
`tools/tsl-codegen` 目录中运行:
|
||||
|
||||
```bash
|
||||
python scripts/generate.py --file ../../tmp/my-api.json --root ../..
|
||||
```
|
||||
|
||||
此时仍会写入仓库根目录下的
|
||||
`skills/tsl-api-reference/references/codegen/project/<path>.md`,不会在
|
||||
`tools/tsl-codegen` 下创建新的 `skills/` 目录。`--root` 接受绝对路径;相对路径按
|
||||
运行命令时的当前工作目录解析
|
||||
|
||||
写入前会自动使用仓库的 `.prettierrc.json` 格式化 markdown,使新页面与现有
|
||||
builtin 页面保持一致。未安装 Prettier 或格式化失败时,生成器会停止且不写目标文件
|
||||
|
||||
@@ -1,905 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Conservatively enrich TSL API descriptions and search tags.
|
||||
|
||||
Markdown is the fact source. This tool never changes API headings, signatures,
|
||||
parameter tables, return types, examples, or scope placement. It only updates
|
||||
the first prose description line and the optional ``<!-- tags: ... -->`` line.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import html
|
||||
import re
|
||||
import sys
|
||||
import unicodedata
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field, replace
|
||||
from pathlib import Path
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
if str(SCRIPT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
|
||||
from api_markdown import DECLARATION_LINE_RE, iter_api_entries
|
||||
|
||||
|
||||
TAGS_RE = re.compile(r"^<!--\s*tags:\s*(.*?)\s*-->$")
|
||||
ACCESS_RE = re.compile(r"^访问[::]\s*(.*?)\s*$", re.IGNORECASE)
|
||||
H1_RE = re.compile(r"^#(?!#)\s+(.+?)\s*$")
|
||||
TERMINAL_PUNCTUATION_RE = re.compile(r"[。..!!??;;,,::]+$")
|
||||
MULTISPACE_RE = re.compile(r"\s+")
|
||||
FORMULA_RE = re.compile(
|
||||
r"^([^。;;,,]{2,40}?)\s*(?<![<>=!])=(?!=)\s*(.+)$"
|
||||
)
|
||||
COMPARISON_RE = re.compile(r"<=|>=|==|!=|<|>")
|
||||
RELATED_FUNCTION_RE = re.compile(
|
||||
r"^([A-Za-z][A-Za-z0-9_]*)\s*相关函数$"
|
||||
)
|
||||
ACRONYM_RE = re.compile(r"(?<![A-Za-z0-9_])([A-Z][A-Z0-9_]{1,15})(?![A-Za-z0-9_])")
|
||||
ACTION_CLAUSE_RE = re.compile(
|
||||
r"^(?:用于|如果|由于|若(?!干)|支持|查询|输入|按照|按(?!钮)|与系统参数|"
|
||||
r"对.+?(?:进行|使用)|从.+?(?:获取|读取|计算|返回)|"
|
||||
r"由.+?(?:倒推|获取|计算|得到|生成))"
|
||||
)
|
||||
|
||||
PROHIBITED_PROCESS_RE = re.compile(
|
||||
r"Source\s*ID|来源[::]|已验证|验证通过|not[_ ]?found|"
|
||||
r"测试过程|探测结果|官网定义",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
GENERIC_SUMMARIES = {
|
||||
"函数",
|
||||
"方法",
|
||||
"属性",
|
||||
"常量",
|
||||
"接口",
|
||||
"对象",
|
||||
"类",
|
||||
"内置函数",
|
||||
"内部函数",
|
||||
"统计分布相关函数",
|
||||
}
|
||||
GENERIC_TAGS = {
|
||||
"函数",
|
||||
"方法",
|
||||
"属性",
|
||||
"常量",
|
||||
"数据",
|
||||
"tsl",
|
||||
"builtin",
|
||||
"dotnet",
|
||||
"third",
|
||||
"deprecated",
|
||||
"function",
|
||||
"method",
|
||||
"property",
|
||||
}
|
||||
CLASS_DESCRIPTIONS = {
|
||||
"imap": "用于连接 IMAP 服务器、管理邮箱并读取邮件的内置对象",
|
||||
"mailmsg": "用于创建、解析、保存和发送邮件消息的内置对象",
|
||||
"messagepart": "表示邮件正文、附件等消息分段的内置对象",
|
||||
"pop3": "用于连接 POP3 服务器并读取或删除邮件的内置对象",
|
||||
"smtp": "用于连接 SMTP 服务器并发送邮件的内置对象",
|
||||
"tcipher": "提供对称加密和解密能力的内置对象",
|
||||
"tcookie": "表示 HTTP Cookie 名称、值和属性的内置对象",
|
||||
"tcookiecollection": "用于创建、查找和管理 HTTP Cookie 集合的内置对象",
|
||||
"tfilestream": "以文件为存储后端并提供流式读写能力的内置对象",
|
||||
"thandlestream": "封装系统句柄并提供流式读写能力的内置对象",
|
||||
"thashedstringlist": "使用哈希索引加速查找的字符串列表内置对象",
|
||||
"tinifile": "用于读取和写入 INI 配置文件的内置对象",
|
||||
"tmeminifile": "在内存中读取、修改和保存 INI 配置的内置对象",
|
||||
"tmemorystream": "以内存缓冲区保存和操作流数据的内置对象",
|
||||
"tregistryinifile": "以系统注册表为存储后端的 INI 配置内置对象",
|
||||
"trsa": "提供 RSA 非对称加密、解密和签名能力的内置对象",
|
||||
"tsession": "表示服务端会话及其键值数据的内置对象",
|
||||
"tsessionman": "用于创建、查找、维护和删除服务端会话的内置对象",
|
||||
"tstream": "定义顺序读写、定位和复制操作的流基类",
|
||||
"tstringlist": "提供字符串集合存储、查找、排序和名称值管理能力的内置对象",
|
||||
"twebrequest": "封装 HTTP 请求头、请求参数、Cookie 和请求正文的内置对象",
|
||||
"twebresponse": "用于设置 HTTP 状态、响应头、Cookie 和响应正文的内置对象",
|
||||
}
|
||||
GENERIC_DESCRIPTION_OVERRIDES = {
|
||||
"unicodeesc2": "将字符串转换为 \\uxxxx 形式的 Unicode 编码串",
|
||||
"sf_x2inv": "根据累计概率计算卡方分布的逆函数值",
|
||||
"sf_tinv": "根据累计概率计算 t 分布的逆函数值",
|
||||
"sf_sinx": "计算正弦积分",
|
||||
"sf_normalinv2": "根据累计概率计算标准正态分布的逆函数值",
|
||||
"sf_normalinv": "根据累计概率计算正态分布的逆函数值",
|
||||
"sf_normal": "计算正态分布函数值",
|
||||
"sf_gammainv": "根据累计概率计算标准 Gamma 分布的逆函数值",
|
||||
"sf_gamma": "计算伽马函数值",
|
||||
"sf_finv": "根据累计概率计算 F 分布的逆函数值",
|
||||
"sf_ex": "计算指数积分",
|
||||
"sf_error": "计算误差函数(概率函数)值",
|
||||
"sf_cosx": "计算余弦积分",
|
||||
"sf_betainv": "根据累计概率计算贝塔分布的逆函数值",
|
||||
"sf_beta": "计算贝塔函数值",
|
||||
"readfile": "读取本地文件中的数据",
|
||||
"millisecondspan": "计算两个日期时间之间相差的毫秒数",
|
||||
"importfile": "从文件中导入数据",
|
||||
"getmsgdigest": "计算字符串或文件的摘要并返回摘要字符串",
|
||||
"exportfile": "将数据导出到文件",
|
||||
}
|
||||
AWKWARD_DESCRIPTION_OVERRIDES = {
|
||||
"encodegraph": "根据指定的类型、名称、数据和属性数组生成图形并返回",
|
||||
"slndepreciation": (
|
||||
"按直线折旧法计算每期折旧额,计算公式为"
|
||||
"(资产原值 Cost - 资产残值 Salvage)/ 折旧周期数 Life"
|
||||
),
|
||||
"format": "根据 formatstr 指定的格式将参数连接成字符串并返回",
|
||||
"ansiformat": "根据 formatstr 指定的格式将参数连接成字符串并返回",
|
||||
"dupestring": "将字符串 AText 重复 ACount 次并返回结果",
|
||||
"stockpjcj": "返回区间平均成交价,计算公式为区间成交金额 / 区间成交量",
|
||||
}
|
||||
LEADING_VERBS = (
|
||||
"返回",
|
||||
"获取",
|
||||
"读取",
|
||||
"写入",
|
||||
"设置",
|
||||
"创建",
|
||||
"删除",
|
||||
"移除",
|
||||
"清除",
|
||||
"添加",
|
||||
"插入",
|
||||
"查找",
|
||||
"搜索",
|
||||
"判断",
|
||||
"检查",
|
||||
"验证",
|
||||
"计算",
|
||||
"转换",
|
||||
"解析",
|
||||
"格式化",
|
||||
"比较",
|
||||
"执行",
|
||||
"调用",
|
||||
"连接",
|
||||
"关闭",
|
||||
"打开",
|
||||
"发送",
|
||||
"接收",
|
||||
"保存",
|
||||
"加载",
|
||||
"列出",
|
||||
"表示",
|
||||
"提供",
|
||||
"释放",
|
||||
"交换",
|
||||
"排序",
|
||||
"过滤",
|
||||
"统计",
|
||||
"控制",
|
||||
"指示",
|
||||
"提取",
|
||||
"根据",
|
||||
"将",
|
||||
"把",
|
||||
"生成",
|
||||
"构造",
|
||||
"采用",
|
||||
"利用",
|
||||
"输出",
|
||||
"得到",
|
||||
"获得",
|
||||
"取得",
|
||||
"新建",
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SourceRecord:
|
||||
corpus: str
|
||||
page_id: str
|
||||
title: str
|
||||
page: Path
|
||||
description: str = ""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EntryContext:
|
||||
name: str
|
||||
signature: str
|
||||
kind: str
|
||||
scope: str
|
||||
page: str
|
||||
summary: str
|
||||
access: str = ""
|
||||
owner: str = ""
|
||||
tags: tuple[str, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PageContext:
|
||||
title: str
|
||||
path: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class AuditChange:
|
||||
page: str
|
||||
name: str
|
||||
kind: str
|
||||
old_description: str
|
||||
new_description: str
|
||||
old_tags: tuple[str, ...]
|
||||
new_tags: tuple[str, ...]
|
||||
sources: tuple[str, ...]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Audit:
|
||||
entries: int = 0
|
||||
source_matches: int = 0
|
||||
ambiguous_sources: int = 0
|
||||
changed_descriptions: int = 0
|
||||
changed_tags: int = 0
|
||||
changes: list[AuditChange] = field(default_factory=list)
|
||||
|
||||
def merge(self, other: "Audit") -> None:
|
||||
self.entries += other.entries
|
||||
self.source_matches += other.source_matches
|
||||
self.ambiguous_sources += other.ambiguous_sources
|
||||
self.changed_descriptions += other.changed_descriptions
|
||||
self.changed_tags += other.changed_tags
|
||||
self.changes.extend(other.changes)
|
||||
|
||||
|
||||
def normalize_key(value: str) -> str:
|
||||
return unicodedata.normalize("NFKC", value).casefold().strip()
|
||||
|
||||
|
||||
def clean_text(value: str) -> str:
|
||||
value = html.unescape(value).replace("\xa0", " ")
|
||||
value = value.replace("“", '"').replace("”", '"')
|
||||
value = value.replace("‘", "'").replace("’", "'")
|
||||
return MULTISPACE_RE.sub(" ", value).strip()
|
||||
|
||||
|
||||
def strip_terminal_punctuation(value: str) -> str:
|
||||
return TERMINAL_PUNCTUATION_RE.sub("", clean_text(value)).rstrip()
|
||||
|
||||
|
||||
def safe_source_description(value: str) -> str:
|
||||
value = strip_terminal_punctuation(value)
|
||||
if not value or PROHIBITED_PROCESS_RE.search(value):
|
||||
return ""
|
||||
return value
|
||||
|
||||
|
||||
def _extract_source_description(page: Path) -> str:
|
||||
try:
|
||||
soup = BeautifulSoup(page.read_text(encoding="utf-8"), "html.parser")
|
||||
except (OSError, UnicodeError):
|
||||
return ""
|
||||
root = soup.select_one("#help_content") or soup
|
||||
for expected in ("简述", "说明"):
|
||||
for label in root.select(".DescriteTitle"):
|
||||
if clean_text(label.get_text(" ", strip=True)) != expected:
|
||||
continue
|
||||
marker = label.parent if label.parent is not None else label
|
||||
sibling = marker.find_next_sibling()
|
||||
if sibling is None:
|
||||
sibling = label.find_next_sibling()
|
||||
if sibling is not None:
|
||||
return safe_source_description(sibling.get_text(" ", strip=True))
|
||||
return ""
|
||||
|
||||
|
||||
class SourceCatalog:
|
||||
PREFERENCES = {
|
||||
"builtin": ("tsl_base", "net_function", "knowledge_base"),
|
||||
"third": ("net_function", "tsl_base", "knowledge_base"),
|
||||
"deprecated": ("net_function", "tsl_base", "knowledge_base"),
|
||||
"dotnet": ("net_function", "tsl_base", "knowledge_base"),
|
||||
}
|
||||
|
||||
def __init__(self, records: dict[str, list[SourceRecord]] | None = None):
|
||||
self.records = records or {}
|
||||
self._description_cache: dict[Path, str] = {}
|
||||
|
||||
@classmethod
|
||||
def empty(cls) -> "SourceCatalog":
|
||||
return cls({})
|
||||
|
||||
@classmethod
|
||||
def from_docs_root(cls, docs_root: Path) -> "SourceCatalog":
|
||||
records: dict[str, list[SourceRecord]] = defaultdict(list)
|
||||
for corpus in ("net_function", "tsl_base", "knowledge_base"):
|
||||
root = docs_root / corpus
|
||||
manifest = root / "manifest.tsv"
|
||||
if not manifest.is_file():
|
||||
continue
|
||||
with manifest.open(encoding="utf-8", newline="") as handle:
|
||||
for row in csv.DictReader(handle, delimiter="\t"):
|
||||
if row.get("status") != "ok" or not row.get("title"):
|
||||
continue
|
||||
record = SourceRecord(
|
||||
corpus=corpus,
|
||||
page_id=row.get("id", ""),
|
||||
title=row["title"],
|
||||
page=root / row.get("path", ""),
|
||||
)
|
||||
records[normalize_key(record.title)].append(record)
|
||||
return cls(dict(records))
|
||||
|
||||
def _load_description(self, record: SourceRecord) -> SourceRecord:
|
||||
if record.description:
|
||||
return record
|
||||
if record.page not in self._description_cache:
|
||||
self._description_cache[record.page] = _extract_source_description(
|
||||
record.page
|
||||
)
|
||||
return replace(
|
||||
record, description=self._description_cache[record.page]
|
||||
)
|
||||
|
||||
def match(self, entry: EntryContext) -> list[SourceRecord]:
|
||||
matches = self.records.get(normalize_key(entry.name), [])
|
||||
preference = self.PREFERENCES.get(
|
||||
entry.scope,
|
||||
("net_function", "tsl_base", "knowledge_base"),
|
||||
)
|
||||
rank = {name: index for index, name in enumerate(preference)}
|
||||
|
||||
def page_order(record: SourceRecord):
|
||||
try:
|
||||
page_id = (0, int(record.page_id))
|
||||
except ValueError:
|
||||
page_id = (1, record.page_id)
|
||||
return rank.get(record.corpus, 99), page_id
|
||||
|
||||
return sorted(matches, key=page_order)
|
||||
|
||||
def describe(self, records: list[SourceRecord]) -> list[SourceRecord]:
|
||||
return [self._load_description(record) for record in records]
|
||||
|
||||
|
||||
def _starts_with_verb(value: str) -> bool:
|
||||
return value.startswith(LEADING_VERBS) or bool(
|
||||
ACTION_CLAUSE_RE.match(value)
|
||||
)
|
||||
|
||||
|
||||
def _choose_source_description(
|
||||
current: str, sources: list[SourceRecord]
|
||||
) -> str:
|
||||
candidates = [
|
||||
safe_source_description(record.description) for record in sources
|
||||
]
|
||||
candidates = [value for value in candidates if value]
|
||||
if not candidates:
|
||||
return current
|
||||
if len(sources) == 1 and current in GENERIC_SUMMARIES:
|
||||
candidate = candidates[0]
|
||||
if candidate not in GENERIC_SUMMARIES and len(candidate) > len(current):
|
||||
return candidate
|
||||
if PROHIBITED_PROCESS_RE.search(current):
|
||||
return candidates[0]
|
||||
return current
|
||||
|
||||
|
||||
def _property_description(value: str, access: str) -> str:
|
||||
if _starts_with_verb(value):
|
||||
return value
|
||||
if (
|
||||
len(value) > 30
|
||||
or any(mark in value for mark in "。;,")
|
||||
or value.startswith(("功能同", "只读", "用于"))
|
||||
):
|
||||
return value
|
||||
normalized = normalize_key(access).replace(" ", "")
|
||||
if value.startswith("是否"):
|
||||
return (
|
||||
f"控制{value}"
|
||||
if "write" in normalized
|
||||
else f"指示{value}"
|
||||
)
|
||||
if "read" in normalized and "write" in normalized:
|
||||
return f"获取或设置{value}"
|
||||
if "write" in normalized:
|
||||
return f"设置{value}"
|
||||
return f"获取{value}"
|
||||
|
||||
|
||||
def _name_intent(name: str) -> str:
|
||||
key = normalize_key(name)
|
||||
rules = (
|
||||
(("is", "if", "has", "can", "check", "valid"), "判断"),
|
||||
(("get", "find", "query", "search"), "获取"),
|
||||
(("read", "load"), "读取"),
|
||||
(("write", "save", "export"), "写入"),
|
||||
(("set",), "设置"),
|
||||
(("create", "make", "new"), "创建"),
|
||||
(("delete", "remove", "drop"), "删除"),
|
||||
(("clear", "reset"), "清除"),
|
||||
(("add", "append", "insert"), "添加"),
|
||||
(("parse", "decode"), "解析"),
|
||||
(("encode", "convert", "to"), "转换"),
|
||||
(("format",), "格式化"),
|
||||
(("calc", "compute"), "计算"),
|
||||
(("open",), "打开"),
|
||||
(("close",), "关闭"),
|
||||
(("send", "post"), "发送"),
|
||||
(("connect", "login"), "连接"),
|
||||
(("list",), "列出"),
|
||||
)
|
||||
for prefixes, intent in rules:
|
||||
if key.startswith(prefixes):
|
||||
return intent
|
||||
return ""
|
||||
|
||||
|
||||
def improve_description(
|
||||
entry: EntryContext, sources: list[SourceRecord]
|
||||
) -> str:
|
||||
current = strip_terminal_punctuation(entry.summary)
|
||||
current = _choose_source_description(current, sources)
|
||||
current = strip_terminal_punctuation(current)
|
||||
if not current:
|
||||
return entry.summary
|
||||
|
||||
if entry.kind == "class":
|
||||
replacement = CLASS_DESCRIPTIONS.get(normalize_key(entry.name))
|
||||
if replacement and re.fullmatch(
|
||||
rf"{re.escape(entry.name)}\s*内置对象", current, re.IGNORECASE
|
||||
):
|
||||
return replacement
|
||||
|
||||
if entry.kind == "property":
|
||||
return _property_description(current, entry.access)
|
||||
|
||||
key = normalize_key(entry.name)
|
||||
generic_override = GENERIC_DESCRIPTION_OVERRIDES.get(key)
|
||||
if generic_override and current.endswith("相关函数"):
|
||||
return generic_override
|
||||
awkward_override = AWKWARD_DESCRIPTION_OVERRIDES.get(key)
|
||||
if awkward_override and current.startswith(
|
||||
("返回根据", "返回将", "返回:", "返回:")
|
||||
):
|
||||
return awkward_override
|
||||
|
||||
current = re.sub(
|
||||
r"^(?:该|本)函数(?:主要(?:是)?)?\s*", "", current
|
||||
)
|
||||
current = re.sub(r"^功能[::]\s*", "", current)
|
||||
if current.startswith("是否"):
|
||||
current = f"判断{current}"
|
||||
|
||||
for old, new in (
|
||||
("读出", "读取"),
|
||||
("取出", "获取"),
|
||||
("得到", "获取"),
|
||||
("获得", "获取"),
|
||||
("取得", "获取"),
|
||||
("新建", "创建"),
|
||||
):
|
||||
if current.startswith(old):
|
||||
current = new + current[len(old):]
|
||||
break
|
||||
if current.startswith("读") and not current.startswith("读取"):
|
||||
current = "读取" + current[1:]
|
||||
if current.startswith("写") and not current.startswith("写入"):
|
||||
current = "写入" + current[1:]
|
||||
current = current.replace("读出", "读取")
|
||||
current = current.replace("装载内容", "加载内容")
|
||||
|
||||
related_function = RELATED_FUNCTION_RE.fullmatch(current)
|
||||
if related_function:
|
||||
return f"返回 {related_function.group(1)} 对应的数据"
|
||||
|
||||
formula = FORMULA_RE.match(current)
|
||||
page_key = normalize_key(entry.page)
|
||||
if (
|
||||
formula
|
||||
and ("/financial/" in page_key or "/financial_report/" in page_key)
|
||||
and not _starts_with_verb(current)
|
||||
):
|
||||
left, right = formula.groups()
|
||||
return f"计算{left.strip()},公式为{right.strip()}"
|
||||
|
||||
if _starts_with_verb(current):
|
||||
return current
|
||||
|
||||
if "/financial/" in page_key or "/financial_report/" in page_key:
|
||||
if (
|
||||
re.match(r"^\d+(?:\s|[..、)])", current)
|
||||
or COMPARISON_RE.search(current)
|
||||
):
|
||||
return current
|
||||
return f"返回{current}"
|
||||
|
||||
intent = _name_intent(entry.name)
|
||||
if intent and len(current) <= 28 and not any(
|
||||
verb in current[:16] for verb in LEADING_VERBS
|
||||
):
|
||||
return f"{intent}{current}"
|
||||
return current
|
||||
|
||||
|
||||
DOMAIN_RULES = (
|
||||
(r"数组|列表|(?:^|[^a-z])(?:array|fmarray|list)(?:$|[^a-z])", ("数组", "列表", "array", "list")),
|
||||
(r"矩阵|(?:^|[^a-z])matrix(?:$|[^a-z])|\bmt_", ("矩阵", "matrix")),
|
||||
(r"字符串|文本|(?:^|[^a-z])(?:string|char|text)(?:$|[^a-z])", ("字符串", "文本", "string", "text")),
|
||||
(r"日期|时间|(?:^|[^a-z])(?:datetime|date|time)(?:$|[^a-z])", ("日期时间", "日期", "时间", "datetime")),
|
||||
(r"文件|目录|路径|(?:^|[^a-z])(?:file|folder|directory|path)(?:$|[^a-z])", ("文件", "目录", "路径", "file")),
|
||||
(r"网络|(?:^|[^a-z])(?:http|https|cgi|cookie|url)(?:$|[^a-z])", ("网络", "HTTP", "请求", "network")),
|
||||
(r"缓存|(?:^|[^a-z])cache(?:$|[^a-z])", ("缓存", "缓存管理", "cache")),
|
||||
(r"数据库|(?:^|[^a-z])(?:sql|dbf|ini)(?:$|[^a-z])", ("数据库", "SQL", "database")),
|
||||
(r"进程|线程|(?:^|[^a-z])(?:process|thread|pipe)(?:$|[^a-z])", ("进程", "线程", "process")),
|
||||
(r"颜色|(?:^|[^a-z])(?:color|rgb|cmyk)(?:$|[^a-z])", ("颜色", "RGB", "color")),
|
||||
(r"图形|图表|(?:^|[^a-z])(?:graph|chart)(?:$|[^a-z])", ("图形", "图表", "graph")),
|
||||
(r"统计|概率|分布|(?:^|[^a-z])(?:statistics|cdf|pdf)(?:$|[^a-z])", ("统计", "概率", "statistics")),
|
||||
(r"优化|线性规划|(?:^|[^a-z])optimization(?:$|[^a-z])", ("优化", "求解", "optimization")),
|
||||
(r"数值|数学|(?:^|[^a-z])(?:numeric|math)(?:$|[^a-z])", ("数学", "数值", "numeric")),
|
||||
(r"类型转换|(?:^|[^a-z])(?:convert|conversion)(?:$|[^a-z])", ("类型转换", "转换", "conversion")),
|
||||
(r"对象|(?:^|[^a-z])(?:object|class)(?:$|[^a-z])", ("对象", "实例", "object")),
|
||||
(r"(?:^|[^a-z])(?:com|ole)(?:$|[^a-z])|activex", ("COM", "OLE", "自动化")),
|
||||
(r"(?:^|[^a-z])ftp(?:$|[^a-z])", ("FTP", "文件传输")),
|
||||
(r"邮件|(?:^|[^a-z])(?:smtp|pop3|imap|mail)(?:$|[^a-z])", ("邮件", "SMTP", "POP3", "IMAP")),
|
||||
(r"财务|金融|证券|股票|基金|债券|期货|期权", ("金融",)),
|
||||
(r"报表|报告期|report", ("报表", "报告期")),
|
||||
)
|
||||
|
||||
SPECIAL_TAG_RULES = (
|
||||
(r"去重|删除重复|dropduplicate|dedup", ("去重", "删除重复", "deduplicate")),
|
||||
(r"哈希索引|哈希表", ("哈希索引", "哈希表", "hash")),
|
||||
(
|
||||
r"摘要|(?:^|[^a-z0-9_])(?:crc32|md5|sha1|sha224|sha256|sha384|sha512|sm3|digest|hash)(?:$|[^a-z0-9_])",
|
||||
("摘要", "哈希", "散列", "digest", "hash"),
|
||||
),
|
||||
(r"排序|sort", ("排序", "sort")),
|
||||
(r"过滤|筛选|filter", ("过滤", "筛选", "filter")),
|
||||
(r"查找|搜索|find|search|lookup", ("查找", "搜索", "lookup")),
|
||||
)
|
||||
|
||||
INTENT_TAGS = {
|
||||
"返回": ("返回", "获取"),
|
||||
"获取": ("获取", "查询"),
|
||||
"读取": ("读取", "获取"),
|
||||
"写入": ("写入", "保存"),
|
||||
"设置": ("设置", "修改"),
|
||||
"创建": ("创建", "生成"),
|
||||
"删除": ("删除", "移除"),
|
||||
"清除": ("清除", "重置"),
|
||||
"添加": ("添加", "追加"),
|
||||
"查找": ("查找", "搜索"),
|
||||
"判断": ("判断", "检查"),
|
||||
"计算": ("计算", "求解"),
|
||||
"转换": ("转换", "编码"),
|
||||
"解析": ("解析", "parse"),
|
||||
"格式化": ("格式化", "format"),
|
||||
"打开": ("打开", "open"),
|
||||
"关闭": ("关闭", "close"),
|
||||
"发送": ("发送", "提交"),
|
||||
"连接": ("连接", "登录"),
|
||||
"列出": ("列出", "列表"),
|
||||
"输出": ("输出", "打印"),
|
||||
}
|
||||
|
||||
|
||||
def _taxonomy_tags(title: str) -> list[str]:
|
||||
result = []
|
||||
for raw in re.split(r"\s*/\s*|\s+-\s+", title):
|
||||
value = raw.strip(" #")
|
||||
value = re.sub(r"\([^)]*\)|([^)]*)", "", value).strip()
|
||||
for suffix in ("相关函数", "及其实现", "相关"):
|
||||
if value.endswith(suffix):
|
||||
value = value[: -len(suffix)].rstrip()
|
||||
if not value or normalize_key(value) in GENERIC_TAGS:
|
||||
continue
|
||||
if len(value) > 18:
|
||||
continue
|
||||
chinese = "".join(re.findall(r"[\u3400-\u9fff]+", value))
|
||||
latin = re.sub(r"[\u3400-\u9fff]+", " ", value).strip()
|
||||
if latin and normalize_key(latin) not in GENERIC_TAGS:
|
||||
result.extend(
|
||||
re.findall(r"[A-Za-z][A-Za-z0-9_+.-]*", latin)
|
||||
)
|
||||
if chinese:
|
||||
result.append(chinese)
|
||||
return result
|
||||
|
||||
|
||||
def _description_intent(summary: str) -> str:
|
||||
value = strip_terminal_punctuation(summary)
|
||||
replacements = {
|
||||
"得到": "获取",
|
||||
"获得": "获取",
|
||||
"取得": "获取",
|
||||
"新建": "创建",
|
||||
}
|
||||
for source, target in replacements.items():
|
||||
if value.startswith(source):
|
||||
return target
|
||||
for intent in INTENT_TAGS:
|
||||
if value.startswith(intent):
|
||||
return intent
|
||||
for intent in ("读取", "写入", "保存", "计算", "判断", "转换", "解析"):
|
||||
if intent in value[:16]:
|
||||
return "写入" if intent == "保存" else intent
|
||||
return ""
|
||||
|
||||
|
||||
def derive_tags(entry: EntryContext, page: PageContext) -> list[str]:
|
||||
tags: list[str] = []
|
||||
seen: set[str] = set()
|
||||
|
||||
def add(value: str) -> None:
|
||||
value = clean_text(value).strip()
|
||||
key = normalize_key(value)
|
||||
if (
|
||||
not value
|
||||
or key in seen
|
||||
or key in GENERIC_TAGS
|
||||
or len(tags) >= 12
|
||||
):
|
||||
return
|
||||
seen.add(key)
|
||||
tags.append(value)
|
||||
|
||||
for tag in entry.tags:
|
||||
add(tag)
|
||||
|
||||
for tag in _taxonomy_tags(page.title):
|
||||
add(tag)
|
||||
|
||||
haystack = " ".join(
|
||||
(entry.name, entry.signature, entry.summary, page.title, page.path)
|
||||
)
|
||||
domain_haystack = " ".join((page.title, page.path))
|
||||
for pattern, aliases in SPECIAL_TAG_RULES:
|
||||
if re.search(pattern, haystack, re.IGNORECASE):
|
||||
for alias in aliases:
|
||||
add(alias)
|
||||
|
||||
summary = strip_terminal_punctuation(entry.summary)
|
||||
intent = _description_intent(summary)
|
||||
if not intent and not _starts_with_verb(summary):
|
||||
is_market_price = (
|
||||
normalize_key(entry.name) in {"open", "close"}
|
||||
and "盘价" in summary
|
||||
)
|
||||
if not is_market_price:
|
||||
intent = _name_intent(entry.name)
|
||||
for alias in INTENT_TAGS.get(intent, ()):
|
||||
add(alias)
|
||||
for pattern, aliases in DOMAIN_RULES:
|
||||
if re.search(pattern, domain_haystack, re.IGNORECASE):
|
||||
for alias in aliases:
|
||||
add(alias)
|
||||
for acronym in ACRONYM_RE.findall(entry.summary):
|
||||
add(acronym)
|
||||
return tags
|
||||
|
||||
|
||||
def _scope_for_page(page: str) -> str:
|
||||
parts = Path(page).parts
|
||||
return parts[0] if len(parts) > 1 else Path(page).stem
|
||||
|
||||
|
||||
def _page_title(lines: list[str], page: str) -> PageContext:
|
||||
for line in lines:
|
||||
match = H1_RE.match(line)
|
||||
if match:
|
||||
return PageContext(match.group(1), page)
|
||||
return PageContext(Path(page).stem, page)
|
||||
|
||||
|
||||
def _entry_metadata(lines: list[str], start: int, end: int):
|
||||
declaration_seen = False
|
||||
description_index = None
|
||||
tag_index = None
|
||||
access = ""
|
||||
for index in range(start + 1, end):
|
||||
text = lines[index].strip()
|
||||
if not text:
|
||||
continue
|
||||
if not declaration_seen:
|
||||
if DECLARATION_LINE_RE.fullmatch(text):
|
||||
declaration_seen = True
|
||||
continue
|
||||
tag_match = TAGS_RE.fullmatch(text)
|
||||
if tag_match:
|
||||
tag_index = index
|
||||
continue
|
||||
access_match = ACCESS_RE.fullmatch(text)
|
||||
if access_match:
|
||||
access = access_match.group(1)
|
||||
continue
|
||||
if description_index is None:
|
||||
if text.startswith(("|", "#", "返回:", "类型:", "可见性:", "值:")):
|
||||
continue
|
||||
description_index = index
|
||||
tags = ()
|
||||
if tag_index is not None:
|
||||
tags = tuple(TAGS_RE.fullmatch(lines[tag_index].strip()).group(1).split())
|
||||
return description_index, tag_index, access, tags
|
||||
|
||||
|
||||
def enrich_markdown(
|
||||
text: str, page: str, source_catalog: SourceCatalog
|
||||
) -> tuple[str, Audit]:
|
||||
had_final_newline = text.endswith("\n")
|
||||
lines = text.splitlines()
|
||||
page_context = _page_title(lines, page)
|
||||
entries = [entry for entry in iter_api_entries(lines) if entry.heading.valid]
|
||||
audit = Audit(entries=len(entries))
|
||||
root_owner = ""
|
||||
owners: dict[int, str] = {}
|
||||
for entry in entries:
|
||||
if entry.heading.level == 2:
|
||||
root_owner = entry.heading.name if entry.heading.kind in {"class", "unit"} else ""
|
||||
owners[entry.start] = root_owner if entry.heading.level > 2 else ""
|
||||
|
||||
for api in reversed(entries):
|
||||
description_index, tag_index, access, existing_tags = _entry_metadata(
|
||||
lines, api.start, api.end
|
||||
)
|
||||
if description_index is None:
|
||||
continue
|
||||
old_description = lines[description_index].strip()
|
||||
entry = EntryContext(
|
||||
name=api.heading.name,
|
||||
signature=api.heading.signature,
|
||||
kind=api.heading.kind,
|
||||
scope=_scope_for_page(page),
|
||||
page=page,
|
||||
summary=old_description,
|
||||
access=access,
|
||||
owner=owners.get(api.start, ""),
|
||||
tags=existing_tags,
|
||||
)
|
||||
sources = source_catalog.match(entry)
|
||||
if sources:
|
||||
audit.source_matches += 1
|
||||
if len(sources) > 1:
|
||||
audit.ambiguous_sources += 1
|
||||
evidence = sources
|
||||
normalized_summary = strip_terminal_punctuation(old_description)
|
||||
if (
|
||||
normalized_summary in GENERIC_SUMMARIES
|
||||
or PROHIBITED_PROCESS_RE.search(normalized_summary)
|
||||
):
|
||||
evidence = source_catalog.describe(sources)
|
||||
new_description = improve_description(entry, evidence)
|
||||
entry_for_tags = replace(entry, summary=new_description)
|
||||
new_tags = tuple(derive_tags(entry_for_tags, page_context))
|
||||
|
||||
if new_description != old_description:
|
||||
lines[description_index] = new_description
|
||||
audit.changed_descriptions += 1
|
||||
if new_tags != existing_tags:
|
||||
rendered = f"<!-- tags: {' '.join(new_tags)} -->"
|
||||
if tag_index is not None:
|
||||
lines[tag_index] = rendered
|
||||
else:
|
||||
insert_at = description_index + 1
|
||||
if insert_at < len(lines) and lines[insert_at].strip() == "":
|
||||
insert_at += 1
|
||||
lines[insert_at:insert_at] = [rendered, ""]
|
||||
else:
|
||||
lines[insert_at:insert_at] = ["", rendered, ""]
|
||||
audit.changed_tags += 1
|
||||
|
||||
if new_description != old_description or new_tags != existing_tags:
|
||||
audit.changes.append(
|
||||
AuditChange(
|
||||
page=page,
|
||||
name=api.heading.name,
|
||||
kind=api.heading.kind,
|
||||
old_description=old_description,
|
||||
new_description=new_description,
|
||||
old_tags=existing_tags,
|
||||
new_tags=new_tags,
|
||||
sources=tuple(
|
||||
f"{record.corpus}:{record.page_id}" for record in sources
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
result = "\n".join(lines)
|
||||
if had_final_newline:
|
||||
result += "\n"
|
||||
return result, audit
|
||||
|
||||
|
||||
def _write_report(path: Path, audit: Audit) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open("w", encoding="utf-8", newline="") as handle:
|
||||
writer = csv.writer(handle, delimiter="\t", lineterminator="\n")
|
||||
writer.writerow(
|
||||
(
|
||||
"page",
|
||||
"name",
|
||||
"kind",
|
||||
"old_description",
|
||||
"new_description",
|
||||
"old_tags",
|
||||
"new_tags",
|
||||
"sources",
|
||||
)
|
||||
)
|
||||
for item in audit.changes:
|
||||
writer.writerow(
|
||||
(
|
||||
item.page,
|
||||
item.name,
|
||||
item.kind,
|
||||
item.old_description,
|
||||
item.new_description,
|
||||
" ".join(item.old_tags),
|
||||
" ".join(item.new_tags),
|
||||
" ".join(item.sources),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _selected(page: str, scopes: set[str], prefixes: tuple[str, ...]) -> bool:
|
||||
if scopes and _scope_for_page(page) not in scopes:
|
||||
return False
|
||||
return not prefixes or page.startswith(prefixes)
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
mode = parser.add_mutually_exclusive_group(required=True)
|
||||
mode.add_argument("--audit", action="store_true", help="report changes only")
|
||||
mode.add_argument("--apply", action="store_true", help="write proposed changes")
|
||||
parser.add_argument("--root", type=Path, required=True, help="codegen Markdown root")
|
||||
parser.add_argument(
|
||||
"--docs-root", type=Path, required=True, help="cached tmp/docs root"
|
||||
)
|
||||
parser.add_argument("--report", type=Path, help="optional TSV audit report")
|
||||
parser.add_argument("--scope", action="append", default=[])
|
||||
parser.add_argument("--prefix", action="append", default=[])
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
catalog = SourceCatalog.from_docs_root(args.docs_root)
|
||||
aggregate = Audit()
|
||||
changed_files = 0
|
||||
for md in sorted(args.root.rglob("*.md")):
|
||||
page = md.relative_to(args.root).as_posix()
|
||||
if not _selected(page, set(args.scope), tuple(args.prefix)):
|
||||
continue
|
||||
before_stat = md.stat()
|
||||
original = md.read_text(encoding="utf-8")
|
||||
enriched, audit = enrich_markdown(original, page, catalog)
|
||||
aggregate.merge(audit)
|
||||
if enriched == original:
|
||||
continue
|
||||
changed_files += 1
|
||||
if args.apply:
|
||||
after_stat = md.stat()
|
||||
if (
|
||||
after_stat.st_mtime_ns != before_stat.st_mtime_ns
|
||||
or after_stat.st_size != before_stat.st_size
|
||||
):
|
||||
raise RuntimeError(f"target changed while processing: {md}")
|
||||
md.write_text(enriched, encoding="utf-8", newline="\n")
|
||||
|
||||
if args.report:
|
||||
_write_report(args.report, aggregate)
|
||||
print(
|
||||
"entries={entries} source_matches={source_matches} "
|
||||
"ambiguous_sources={ambiguous_sources} changed_files={changed_files} "
|
||||
"changed_descriptions={changed_descriptions} changed_tags={changed_tags}".format(
|
||||
entries=aggregate.entries,
|
||||
source_matches=aggregate.source_matches,
|
||||
ambiguous_sources=aggregate.ambiguous_sources,
|
||||
changed_files=changed_files,
|
||||
changed_descriptions=aggregate.changed_descriptions,
|
||||
changed_tags=aggregate.changed_tags,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -21,7 +21,7 @@ Function declaration fields:
|
||||
tags optional list of Chinese keywords -> `<!-- tags: ... -->`
|
||||
params required when the signature takes args; omit for nullary
|
||||
returns required return type
|
||||
example optional fenced tsl block, pasted verbatim
|
||||
examples optional list of structured examples with desc/code/output fields
|
||||
Each param: name/type/desc required; optional (bool) -> `可选。` prefix;
|
||||
values (list of {value, desc}) -> a `name 取值` enum section.
|
||||
|
||||
@@ -29,6 +29,7 @@ Usage (run from repo root):
|
||||
python tools/tsl-codegen/scripts/generate.py --file entry.yml
|
||||
python tools/tsl-codegen/scripts/generate.py --file entry.json \
|
||||
--scope my-project
|
||||
python scripts/generate.py --file entry.json --root ../..
|
||||
python tools/tsl-codegen/scripts/generate.py --dir recordings
|
||||
"""
|
||||
|
||||
@@ -279,7 +280,6 @@ def validate_function(fn, where, *, returns_required, extra_fields=()):
|
||||
"tags",
|
||||
"params",
|
||||
"returns",
|
||||
"example",
|
||||
"examples",
|
||||
*extra_fields,
|
||||
}
|
||||
@@ -292,12 +292,6 @@ def validate_function(fn, where, *, returns_required, extra_fields=()):
|
||||
non_empty_string(fn.get("returns"), f"{where}:缺少 returns")
|
||||
elif "returns" in fn:
|
||||
optional_draft_string(fn["returns"], f"{where}: returns")
|
||||
require(
|
||||
not ("example" in fn and "examples" in fn),
|
||||
f"{where}:example 和 examples 不能同时存在",
|
||||
)
|
||||
if "example" in fn:
|
||||
non_empty_string(fn["example"], f"{where}: example")
|
||||
if "examples" in fn:
|
||||
validate_examples(fn["examples"], where)
|
||||
return name
|
||||
@@ -333,7 +327,6 @@ def validate_class_member(member, where):
|
||||
"params",
|
||||
"returns",
|
||||
"modifiers",
|
||||
"example",
|
||||
"examples",
|
||||
},
|
||||
where,
|
||||
@@ -600,14 +593,6 @@ def render_examples(fn, heading_level):
|
||||
while lines and not lines[-1]:
|
||||
lines.pop()
|
||||
return lines
|
||||
if fn.get("example"):
|
||||
return [
|
||||
f"{'#' * heading_level} 示例",
|
||||
"",
|
||||
"```tsl",
|
||||
*fn["example"].rstrip("\n").split("\n"),
|
||||
"```",
|
||||
]
|
||||
return []
|
||||
|
||||
|
||||
@@ -822,7 +807,7 @@ def format_markdown(text):
|
||||
return result.stdout
|
||||
|
||||
|
||||
def output_path(data, scope):
|
||||
def output_path(data, scope, root=None):
|
||||
"""Build the leaf-page destination from the recording file's relative path."""
|
||||
relative = data.get("path")
|
||||
require(relative, "录入数据缺少 path")
|
||||
@@ -854,7 +839,8 @@ def output_path(data, scope):
|
||||
)
|
||||
relative_path = Path(*recorded_path.parts)
|
||||
return (
|
||||
Path("skills/tsl-api-reference/references/codegen")
|
||||
(Path(root) if root is not None else Path())
|
||||
/ "skills/tsl-api-reference/references/codegen"
|
||||
/ scope
|
||||
/ relative_path.with_suffix(".md")
|
||||
)
|
||||
@@ -902,14 +888,14 @@ def gather_directory_inputs(directory, fmt):
|
||||
return inputs
|
||||
|
||||
|
||||
def prepare_input(in_path, input_format, scope):
|
||||
def prepare_input(in_path, input_format, scope, root=None):
|
||||
data = load_entries(in_path, input_format)
|
||||
try:
|
||||
rendered = render_page(data)
|
||||
except GenerationError as exc:
|
||||
raise GenerationError(f"录入数据校验失败:{exc}") from exc
|
||||
try:
|
||||
out_path = output_path(data, scope)
|
||||
out_path = output_path(data, scope, root)
|
||||
except GenerationError as exc:
|
||||
raise GenerationError(f"输出路径无效:{exc}") from exc
|
||||
try:
|
||||
@@ -941,11 +927,14 @@ def find_output_collisions(prepared):
|
||||
def main(argv=None):
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
if hasattr(sys.stderr, "reconfigure"):
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
parser = ChineseArgumentParser(
|
||||
description="从 YAML/JSON 录入文件生成 TSL API 文档",
|
||||
usage=(
|
||||
"%(prog)s [--help] [--scope SCOPE] [--format {json,yaml}] "
|
||||
"(--file INPUT_FILE | --dir INPUT_DIR | INPUT_FILE)"
|
||||
"%(prog)s [--help] [--root ROOT_DIR] [--scope SCOPE] "
|
||||
"[--format {json,yaml}] "
|
||||
"(--file INPUT_FILE | --dir INPUT_DIR)"
|
||||
),
|
||||
add_help=False,
|
||||
allow_abbrev=False,
|
||||
@@ -955,12 +944,6 @@ def main(argv=None):
|
||||
action="help",
|
||||
help="显示本帮助并退出(不提供 -h 短选项)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"legacy_input",
|
||||
nargs="?",
|
||||
metavar="INPUT_FILE",
|
||||
help="已废弃,请使用 --file;暂时兼容 YAML/JSON 录入文件路径",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--file",
|
||||
dest="input_file",
|
||||
@@ -973,6 +956,11 @@ def main(argv=None):
|
||||
metavar="INPUT_DIR",
|
||||
help="批量生成目录中的直属 YAML/JSON 录入文件",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--root",
|
||||
metavar="ROOT_DIR",
|
||||
help="包含 skills 目录的项目根目录(默认:当前工作目录)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--scope",
|
||||
type=scope_name,
|
||||
@@ -986,16 +974,16 @@ def main(argv=None):
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
input_modes = (args.legacy_input, args.input_file, args.input_dir)
|
||||
input_modes = (args.input_file, args.input_dir)
|
||||
if sum(value is not None for value in input_modes) != 1:
|
||||
parser.error("必须且只能指定一种输入方式:INPUT_FILE、--file 或 --dir")
|
||||
parser.error("必须且只能指定一种输入方式:--file 或 --dir")
|
||||
is_batch = args.input_dir is not None
|
||||
try:
|
||||
if is_batch:
|
||||
input_paths = gather_directory_inputs(Path(args.input_dir), args.format)
|
||||
input_format = None
|
||||
else:
|
||||
in_path = Path(args.input_file or args.legacy_input)
|
||||
in_path = Path(args.input_file)
|
||||
if not in_path.is_file():
|
||||
die(f"输入文件不存在或不是普通文件:{in_path}")
|
||||
input_paths = [in_path]
|
||||
@@ -1008,7 +996,9 @@ def main(argv=None):
|
||||
input_errors = []
|
||||
for in_path in input_paths:
|
||||
try:
|
||||
prepared.append(prepare_input(in_path, input_format, args.scope))
|
||||
prepared.append(
|
||||
prepare_input(in_path, input_format, args.scope, args.root)
|
||||
)
|
||||
except GenerationError as exc:
|
||||
input_errors.append((in_path, exc))
|
||||
|
||||
|
||||
@@ -1,778 +0,0 @@
|
||||
import importlib.util
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
SCRIPT = Path(__file__).parents[1] / "scripts" / "enrich_metadata.py"
|
||||
|
||||
|
||||
def load_script():
|
||||
spec = importlib.util.spec_from_file_location("tsl_enrich_metadata", SCRIPT)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
class SourceCatalogTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.module = load_script()
|
||||
self.temp_dir = tempfile.TemporaryDirectory()
|
||||
self.docs = Path(self.temp_dir.name)
|
||||
|
||||
def tearDown(self):
|
||||
self.temp_dir.cleanup()
|
||||
|
||||
def write_corpus(self, corpus, rows, descriptions):
|
||||
root = self.docs / corpus
|
||||
pages = root / "pages"
|
||||
pages.mkdir(parents=True)
|
||||
manifest = [
|
||||
"id\ttitle\turl\tpath\tsource_bytes\tutf8_bytes\tsha256\tstatus"
|
||||
]
|
||||
for page_id, title in rows:
|
||||
manifest.append(
|
||||
f"{page_id}\t{title}\thttp://example/{page_id}\t"
|
||||
f"pages/{page_id}.html\t1\t1\tsha\tok"
|
||||
)
|
||||
description = descriptions[page_id]
|
||||
(pages / f"{page_id}.html").write_text(
|
||||
"<div id='help_content'><h3>"
|
||||
+ title
|
||||
+ "</h3><div><div class='DescriteTitle'>简述</div></div>"
|
||||
+ f"<div>{description}</div></div>",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(root / "manifest.tsv").write_text(
|
||||
"\n".join(manifest) + "\n", encoding="utf-8"
|
||||
)
|
||||
|
||||
def test_exact_matching_preserves_underscores_and_prefers_scope_corpus(self):
|
||||
self.write_corpus(
|
||||
"tsl_base",
|
||||
[("1", "alpha_test")],
|
||||
{"1": "基础解释器说明"},
|
||||
)
|
||||
self.write_corpus(
|
||||
"net_function",
|
||||
[("2", "alpha_test"), ("3", "alphaTest")],
|
||||
{"2": "NET 说明", "3": "无下划线的另一个函数"},
|
||||
)
|
||||
catalog = self.module.SourceCatalog.from_docs_root(self.docs)
|
||||
|
||||
builtin = self.module.EntryContext(
|
||||
name="alpha_test",
|
||||
signature="alpha_test()",
|
||||
kind="function",
|
||||
scope="builtin",
|
||||
page="builtin/demo.md",
|
||||
summary="原说明",
|
||||
)
|
||||
dotnet = self.module.EntryContext(
|
||||
name="alpha_test",
|
||||
signature="alpha_test()",
|
||||
kind="function",
|
||||
scope="dotnet",
|
||||
page="dotnet/demo.md",
|
||||
summary="原说明",
|
||||
)
|
||||
|
||||
self.assertEqual("tsl_base", catalog.match(builtin)[0].corpus)
|
||||
self.assertEqual("net_function", catalog.match(dotnet)[0].corpus)
|
||||
self.assertNotIn(
|
||||
"alphaTest", {record.title for record in catalog.match(dotnet)}
|
||||
)
|
||||
|
||||
def test_duplicate_titles_remain_visible_for_audit(self):
|
||||
self.write_corpus(
|
||||
"net_function",
|
||||
[("1", "same_name"), ("2", "same_name")],
|
||||
{"1": "说明一", "2": "说明二"},
|
||||
)
|
||||
catalog = self.module.SourceCatalog.from_docs_root(self.docs)
|
||||
entry = self.module.EntryContext(
|
||||
name="same_name",
|
||||
signature="same_name()",
|
||||
kind="function",
|
||||
scope="dotnet",
|
||||
page="dotnet/demo.md",
|
||||
summary="原说明",
|
||||
)
|
||||
|
||||
self.assertEqual(["1", "2"], [item.page_id for item in catalog.match(entry)])
|
||||
|
||||
|
||||
class DescriptionQualityTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.module = load_script()
|
||||
|
||||
def entry(self, **overrides):
|
||||
values = {
|
||||
"name": "profitRatio",
|
||||
"signature": "profitRatio(report_date)",
|
||||
"kind": "function",
|
||||
"scope": "dotnet",
|
||||
"page": "dotnet/financial/profitability.md",
|
||||
"summary": "总资产收益率(%)。",
|
||||
}
|
||||
values.update(overrides)
|
||||
return self.module.EntryContext(**values)
|
||||
|
||||
def test_removes_only_terminal_sentence_punctuation(self):
|
||||
entry = self.entry(summary="总资产收益率(%)。")
|
||||
|
||||
self.assertEqual(
|
||||
"返回总资产收益率(%)",
|
||||
self.module.improve_description(entry, []),
|
||||
)
|
||||
|
||||
def test_preserves_formula_and_identifiers(self):
|
||||
entry = self.entry(
|
||||
summary="总资产报酬率(%)=利润总额/平均资产总额*100。"
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
"计算总资产报酬率(%),公式为利润总额/平均资产总额*100",
|
||||
self.module.improve_description(entry, []),
|
||||
)
|
||||
|
||||
def test_rejects_source_process_prose(self):
|
||||
entry = self.entry(summary="返回指定值")
|
||||
source = self.module.SourceRecord(
|
||||
corpus="net_function",
|
||||
page_id="1",
|
||||
title="profitRatio",
|
||||
page=Path("1.html"),
|
||||
description="Windows 已验证通过,Linux 返回 not found",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
"返回指定值", self.module.improve_description(entry, [source])
|
||||
)
|
||||
|
||||
def test_current_environment_can_be_real_api_semantics(self):
|
||||
self.assertEqual(
|
||||
"获取当前环境时间系统参数",
|
||||
self.module.safe_source_description("获取当前环境时间系统参数。"),
|
||||
)
|
||||
|
||||
def test_property_access_is_stated_explicitly(self):
|
||||
entry = self.entry(
|
||||
name="Host",
|
||||
signature="Host",
|
||||
kind="property",
|
||||
scope="builtin",
|
||||
page="builtin/object/ftp.md",
|
||||
summary="远程服务器地址。",
|
||||
access="read/write",
|
||||
owner="FTP",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
"获取或设置远程服务器地址",
|
||||
self.module.improve_description(entry, []),
|
||||
)
|
||||
|
||||
def test_boolean_property_description_is_idempotent(self):
|
||||
entry = self.entry(
|
||||
name="UseTLS",
|
||||
signature="UseTLS",
|
||||
kind="property",
|
||||
scope="builtin",
|
||||
page="builtin/object/imap.md",
|
||||
summary="是否采用 SSL 连接",
|
||||
access="read/write",
|
||||
owner="IMAP",
|
||||
)
|
||||
|
||||
first = self.module.improve_description(entry, [])
|
||||
repeated = self.module.improve_description(
|
||||
self.module.EntryContext(
|
||||
**{**entry.__dict__, "summary": first}
|
||||
),
|
||||
[],
|
||||
)
|
||||
|
||||
self.assertEqual("控制是否采用 SSL 连接", first)
|
||||
self.assertEqual(first, repeated)
|
||||
|
||||
def test_low_confidence_sentence_is_kept_except_terminal_punctuation(self):
|
||||
entry = self.entry(
|
||||
name="opaqueApi",
|
||||
page="dotnet.md",
|
||||
summary="根据调用上下文处理结果,具体规则取决于输入。",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
"根据调用上下文处理结果,具体规则取决于输入",
|
||||
self.module.improve_description(entry, []),
|
||||
)
|
||||
|
||||
def test_short_description_is_not_replaced_by_ambiguous_source_text(self):
|
||||
entry = self.entry(
|
||||
name="clRed",
|
||||
page="builtin/color.md",
|
||||
summary="红色",
|
||||
)
|
||||
sources = [
|
||||
self.module.SourceRecord(
|
||||
corpus="net_function",
|
||||
page_id="1",
|
||||
title="clRed",
|
||||
page=Path("1.html"),
|
||||
description="定义",
|
||||
)
|
||||
]
|
||||
|
||||
self.assertEqual("红色", self.module.improve_description(entry, sources))
|
||||
|
||||
def test_existing_action_word_is_normalized_without_duplicate_verb(self):
|
||||
getter = self.entry(
|
||||
name="getCValue",
|
||||
page="builtin/color.md",
|
||||
summary="得到颜色的 CMYK 模式 C 值。",
|
||||
)
|
||||
writer = self.entry(
|
||||
name="write",
|
||||
page="builtin/cgi.md",
|
||||
summary="输出字符串。",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
"获取颜色的 CMYK 模式 C 值",
|
||||
self.module.improve_description(getter, []),
|
||||
)
|
||||
self.assertEqual("输出字符串", self.module.improve_description(writer, []))
|
||||
|
||||
def test_equation_inside_algorithm_sentence_is_not_rewritten_as_formula(self):
|
||||
entry = self.entry(
|
||||
name="se_Gauss",
|
||||
page="builtin/optimization.md",
|
||||
summary="用高斯消去法求解线性方程组 AX = B",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
"用高斯消去法求解线性方程组 AX = B",
|
||||
self.module.improve_description(entry, []),
|
||||
)
|
||||
|
||||
def test_builtin_class_gets_a_purpose_description(self):
|
||||
entry = self.entry(
|
||||
name="TStringList",
|
||||
signature="TStringList",
|
||||
kind="class",
|
||||
scope="builtin",
|
||||
page="builtin/object/tstringlist.md",
|
||||
summary="TStringList 内置对象",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
"提供字符串集合存储、查找、排序和名称值管理能力的内置对象",
|
||||
self.module.improve_description(entry, []),
|
||||
)
|
||||
|
||||
def test_long_property_explanation_is_not_wrapped_in_access_boilerplate(self):
|
||||
entry = self.entry(
|
||||
name="CommaTextW",
|
||||
signature="CommaTextW",
|
||||
kind="property",
|
||||
scope="builtin",
|
||||
page="builtin/object/tstringlist.md",
|
||||
summary=(
|
||||
"功能同 CommaText,区别是在读取时返回宽字节字符串,"
|
||||
"而 CommaText 返回多字节字符串"
|
||||
),
|
||||
access="read/write",
|
||||
owner="TStringList",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
entry.summary, self.module.improve_description(entry, [])
|
||||
)
|
||||
|
||||
def test_short_read_and_load_phrases_are_normalized(self):
|
||||
read_entry = self.entry(
|
||||
name="read",
|
||||
page="builtin/object/tstream.md",
|
||||
summary="读出内容",
|
||||
)
|
||||
load_entry = self.entry(
|
||||
name="loadFromFile",
|
||||
page="builtin/object/tstringlist.md",
|
||||
summary="从指定的文件中装载内容",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
"读取内容", self.module.improve_description(read_entry, [])
|
||||
)
|
||||
self.assertEqual(
|
||||
"从指定的文件中加载内容",
|
||||
self.module.improve_description(load_entry, []),
|
||||
)
|
||||
|
||||
def test_embedded_read_out_is_normalized_without_duplicate_prefix(self):
|
||||
entry = self.entry(
|
||||
name="readExcelSheets",
|
||||
page="dotnet.md",
|
||||
summary="从Excel文件中读出Sheets列表。",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
"从Excel文件中读取Sheets列表",
|
||||
self.module.improve_description(entry, []),
|
||||
)
|
||||
|
||||
def test_existing_decomposition_verb_is_not_prefixed(self):
|
||||
entry = self.entry(
|
||||
name="decodeGraphGroup",
|
||||
page="builtin/graph.md",
|
||||
summary="分解图形组合并写入输出参数",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
entry.summary, self.module.improve_description(entry, [])
|
||||
)
|
||||
|
||||
def test_comparison_expression_is_not_rewritten_as_formula(self):
|
||||
entry = self.entry(
|
||||
name="stockStepAmount",
|
||||
page="dotnet/financial/stock-capital-flow.md",
|
||||
summary="分档区间 V1<=Value<V2 的成交金额",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
entry.summary, self.module.improve_description(entry, [])
|
||||
)
|
||||
|
||||
def test_financial_action_sentence_is_not_prefixed_with_return(self):
|
||||
for summary in (
|
||||
"提取股票的财务比率,与系统证券相关",
|
||||
"根据报告期获取财务指标",
|
||||
"将日线数据转换为周线数据",
|
||||
"统计指定报告期的基金数量",
|
||||
"生成指定区间的分析结果",
|
||||
):
|
||||
with self.subTest(summary=summary):
|
||||
entry = self.entry(
|
||||
name="annualRatio",
|
||||
page="dotnet/financial/financial-analysis.md",
|
||||
summary=summary,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
entry.summary, self.module.improve_description(entry, [])
|
||||
)
|
||||
|
||||
def test_financial_action_clauses_are_not_prefixed_with_return(self):
|
||||
for summary in (
|
||||
"用于生成投资组合结果名称",
|
||||
"如果存在对应记录则返回证券代码",
|
||||
"对矩阵进行正交处理",
|
||||
"从净值表中获取基金列表",
|
||||
"由交易价格倒推出隐含波动率",
|
||||
"按照周期读取数据",
|
||||
"与系统参数证券和日期相关",
|
||||
):
|
||||
with self.subTest(summary=summary):
|
||||
entry = self.entry(
|
||||
name="financialHelper",
|
||||
page="dotnet/financial/financial-analysis.md",
|
||||
summary=summary,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
entry.summary, self.module.improve_description(entry, [])
|
||||
)
|
||||
|
||||
def test_financial_boolean_description_uses_judgment_wording(self):
|
||||
entry = self.entry(
|
||||
name="isBankBond",
|
||||
page="dotnet/financial/bond-basic-info.md",
|
||||
summary="是否银行间债券。如果是,返回1,否则返回0",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
"判断是否银行间债券。如果是,返回1,否则返回0",
|
||||
self.module.improve_description(entry, []),
|
||||
)
|
||||
|
||||
def test_function_boilerplate_is_removed_from_action_description(self):
|
||||
entry = self.entry(
|
||||
name="cb_ytm",
|
||||
page="dotnet/financial/convertible-bond.md",
|
||||
summary="该函数采用牛顿迭代法获取可转债的到期收益率",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
"采用牛顿迭代法获取可转债的到期收益率",
|
||||
self.module.improve_description(entry, []),
|
||||
)
|
||||
|
||||
def test_financial_numbered_explanation_is_not_prefixed_with_return(self):
|
||||
entry = self.entry(
|
||||
name="financialMode",
|
||||
page="dotnet/financial/financial-analysis.md",
|
||||
summary="1 表示合并报表,2 表示母公司报表",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
entry.summary, self.module.improve_description(entry, [])
|
||||
)
|
||||
|
||||
def test_related_data_placeholder_is_made_explicit(self):
|
||||
entry = self.entry(
|
||||
name="cb_downPeriodConversionPeriod",
|
||||
page="dotnet/financial/convertible-bond.md",
|
||||
summary="CB_DownPeriodConversionPeriod 相关函数",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
"返回 CB_DownPeriodConversionPeriod 对应的数据",
|
||||
self.module.improve_description(entry, []),
|
||||
)
|
||||
|
||||
def test_reviewed_generic_descriptions_use_precise_purpose_text(self):
|
||||
cases = (
|
||||
(
|
||||
"sf_Normal",
|
||||
"统计分布相关函数。",
|
||||
"计算正态分布函数值",
|
||||
),
|
||||
(
|
||||
"readFile",
|
||||
"文件访问函数相关函数。",
|
||||
"读取本地文件中的数据",
|
||||
),
|
||||
(
|
||||
"unicodeEsc2",
|
||||
"多语言支持函数相关函数。",
|
||||
"将字符串转换为 \\uxxxx 形式的 Unicode 编码串",
|
||||
),
|
||||
)
|
||||
for name, summary, expected in cases:
|
||||
with self.subTest(name=name):
|
||||
entry = self.entry(name=name, page="dotnet.md", summary=summary)
|
||||
self.assertEqual(
|
||||
expected, self.module.improve_description(entry, [])
|
||||
)
|
||||
|
||||
def test_reviewed_awkward_descriptions_are_rewritten_directly(self):
|
||||
cases = (
|
||||
(
|
||||
"encodeGraph",
|
||||
"返回根据指定的类型、名称、数据以及属性数组生成的图形。",
|
||||
"根据指定的类型、名称、数据和属性数组生成图形并返回",
|
||||
),
|
||||
(
|
||||
"dupeString",
|
||||
"返回将指定的字符串AText重复ACount次后的字符串",
|
||||
"将字符串 AText 重复 ACount 次并返回结果",
|
||||
),
|
||||
(
|
||||
"stockpjcj",
|
||||
"返回:区间平均成交,区间成交金额/区间成交量",
|
||||
"返回区间平均成交价,计算公式为区间成交金额 / 区间成交量",
|
||||
),
|
||||
)
|
||||
for name, summary, expected in cases:
|
||||
with self.subTest(name=name):
|
||||
entry = self.entry(
|
||||
name=name, page="builtin/string.md", summary=summary
|
||||
)
|
||||
self.assertEqual(
|
||||
expected, self.module.improve_description(entry, [])
|
||||
)
|
||||
|
||||
|
||||
class TagDerivationTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.module = load_script()
|
||||
|
||||
def test_array_deduplication_gets_controlled_chinese_and_english_aliases(self):
|
||||
entry = self.module.EntryContext(
|
||||
name="arrDropDuplicate",
|
||||
signature="arrDropDuplicate(data)",
|
||||
kind="function",
|
||||
scope="builtin",
|
||||
page="builtin/array.md",
|
||||
summary="删除数组中的重复元素",
|
||||
)
|
||||
page = self.module.PageContext(
|
||||
title="Builtin / 数组", path="builtin/array.md"
|
||||
)
|
||||
|
||||
tags = self.module.derive_tags(entry, page)
|
||||
|
||||
self.assertTrue(
|
||||
{"数组", "列表", "去重", "删除重复", "array", "deduplicate"}
|
||||
<= set(tags)
|
||||
)
|
||||
self.assertLessEqual(len(tags), 12)
|
||||
self.assertNotIn("函数", tags)
|
||||
self.assertNotIn("builtin", tags)
|
||||
|
||||
def test_file_digest_gets_digest_and_hash_aliases(self):
|
||||
entry = self.module.EntryContext(
|
||||
name="getMsgDigest",
|
||||
signature="getMsgDigest(s, mode)",
|
||||
kind="function",
|
||||
scope="builtin",
|
||||
page="builtin/string.md",
|
||||
summary="计算字符串摘要,支持 CRC32、MD5、SHA 和 SM3",
|
||||
)
|
||||
page = self.module.PageContext(
|
||||
title="Builtin / 字符串", path="builtin/string.md"
|
||||
)
|
||||
|
||||
tags = self.module.derive_tags(entry, page)
|
||||
|
||||
self.assertTrue(
|
||||
{"字符串", "摘要", "哈希", "散列", "digest", "hash"}
|
||||
<= set(tags)
|
||||
)
|
||||
|
||||
def test_existing_tags_are_preserved_first_and_output_is_idempotent(self):
|
||||
entry = self.module.EntryContext(
|
||||
name="readFile",
|
||||
signature="readFile(alias, file_name)",
|
||||
kind="function",
|
||||
scope="builtin",
|
||||
page="builtin/file.md",
|
||||
summary="读取文件内容",
|
||||
tags=("自定义", "读取"),
|
||||
)
|
||||
page = self.module.PageContext(
|
||||
title="Builtin / 文件", path="builtin/file.md"
|
||||
)
|
||||
|
||||
first = self.module.derive_tags(entry, page)
|
||||
second = self.module.derive_tags(
|
||||
self.module.EntryContext(**{**entry.__dict__, "tags": tuple(first)}),
|
||||
page,
|
||||
)
|
||||
|
||||
self.assertEqual(["自定义", "读取"], first[:2])
|
||||
self.assertEqual(first, second)
|
||||
|
||||
def test_substrings_do_not_create_digest_or_com_tags(self):
|
||||
reshape = self.module.EntryContext(
|
||||
name="reshape",
|
||||
signature="reshape(data, shape)",
|
||||
kind="function",
|
||||
scope="builtin",
|
||||
page="builtin/array.md",
|
||||
summary="重构数组形状",
|
||||
)
|
||||
uncompress = self.module.EntryContext(
|
||||
name="uniuncompress",
|
||||
signature="uniuncompress(data, type)",
|
||||
kind="function",
|
||||
scope="builtin",
|
||||
page="builtin/string.md",
|
||||
summary="统一解压缩数据",
|
||||
)
|
||||
|
||||
reshape_tags = self.module.derive_tags(
|
||||
reshape,
|
||||
self.module.PageContext("Builtin / 数组", reshape.page),
|
||||
)
|
||||
uncompress_tags = self.module.derive_tags(
|
||||
uncompress,
|
||||
self.module.PageContext("Builtin / 字符串", uncompress.page),
|
||||
)
|
||||
|
||||
self.assertNotIn("摘要", reshape_tags)
|
||||
self.assertNotIn("hash", reshape_tags)
|
||||
self.assertNotIn("COM", uncompress_tags)
|
||||
self.assertNotIn("OLE", uncompress_tags)
|
||||
|
||||
def test_runtime_and_character_names_do_not_create_time_or_graph_tags(self):
|
||||
random_entry = self.module.EntryContext(
|
||||
name="randomfrom",
|
||||
signature="randomfrom(values)",
|
||||
kind="function",
|
||||
scope="builtin",
|
||||
page="builtin/runtime.md",
|
||||
summary="返回随机数组元素",
|
||||
)
|
||||
char_entry = self.module.EntryContext(
|
||||
name="charToByteLen",
|
||||
signature="charToByteLen(s, max_len)",
|
||||
kind="function",
|
||||
scope="builtin",
|
||||
page="builtin/string.md",
|
||||
summary="计算字符串占用的字节数",
|
||||
)
|
||||
|
||||
random_tags = self.module.derive_tags(
|
||||
random_entry,
|
||||
self.module.PageContext("Builtin / 运行时", random_entry.page),
|
||||
)
|
||||
char_tags = self.module.derive_tags(
|
||||
char_entry,
|
||||
self.module.PageContext("Builtin / 字符串", char_entry.page),
|
||||
)
|
||||
|
||||
self.assertNotIn("日期时间", random_tags)
|
||||
self.assertNotIn("图形", char_tags)
|
||||
|
||||
def test_parameter_name_does_not_create_unrelated_time_domain(self):
|
||||
entry = self.module.EntryContext(
|
||||
name="futureValue",
|
||||
signature=(
|
||||
"futureValue(rate, n_periods, payment, present_value, payment_time)"
|
||||
),
|
||||
kind="function",
|
||||
scope="builtin",
|
||||
page="builtin/numeric.md",
|
||||
summary="返回一项投资的未来值",
|
||||
)
|
||||
tags = self.module.derive_tags(
|
||||
entry,
|
||||
self.module.PageContext("Builtin / 数值计算", entry.page),
|
||||
)
|
||||
|
||||
self.assertNotIn("日期时间", tags)
|
||||
|
||||
def test_hash_index_is_not_tagged_as_cryptographic_digest(self):
|
||||
entry = self.module.EntryContext(
|
||||
name="THashedStringList",
|
||||
signature="THashedStringList",
|
||||
kind="class",
|
||||
scope="builtin",
|
||||
page="builtin/object/thashedstringlist.md",
|
||||
summary="使用哈希索引加速查找的字符串列表内置对象",
|
||||
)
|
||||
tags = self.module.derive_tags(
|
||||
entry,
|
||||
self.module.PageContext(
|
||||
"Object / THashedStringList 字符串列表", entry.page
|
||||
),
|
||||
)
|
||||
|
||||
self.assertIn("哈希索引", tags)
|
||||
self.assertIn("hash", tags)
|
||||
self.assertNotIn("摘要", tags)
|
||||
self.assertNotIn("digest", tags)
|
||||
|
||||
def test_business_statistics_verb_does_not_add_probability_domain(self):
|
||||
entry = self.module.EntryContext(
|
||||
name="fundCount",
|
||||
signature="fundCount(report_date)",
|
||||
kind="function",
|
||||
scope="dotnet",
|
||||
page="dotnet/financial/fund-holdings.md",
|
||||
summary="统计指定报告期的基金数量",
|
||||
)
|
||||
tags = self.module.derive_tags(
|
||||
entry,
|
||||
self.module.PageContext("金融 / 基金 / 持仓", entry.page),
|
||||
)
|
||||
|
||||
self.assertNotIn("概率", tags)
|
||||
self.assertNotIn("statistics", tags)
|
||||
self.assertNotIn("统计指定报告期的基金数量", tags)
|
||||
self.assertNotIn("证券", tags)
|
||||
|
||||
def test_taxonomy_tags_remove_generic_suffixes(self):
|
||||
entry = self.module.EntryContext(
|
||||
name="weightedMedian",
|
||||
signature="weightedMedian(data, weights)",
|
||||
kind="function",
|
||||
scope="dotnet",
|
||||
page="dotnet/financial/weighted-statistics.md",
|
||||
summary="计算加权中位数",
|
||||
)
|
||||
tags = self.module.derive_tags(
|
||||
entry,
|
||||
self.module.PageContext(
|
||||
"基础算法常见加权统计量及其实现 / 时间相关函数",
|
||||
entry.page,
|
||||
),
|
||||
)
|
||||
|
||||
self.assertIn("基础算法常见加权统计量", tags)
|
||||
self.assertIn("时间", tags)
|
||||
self.assertNotIn("基础算法常见加权统计量及其实现", tags)
|
||||
self.assertNotIn("时间相关函数", tags)
|
||||
|
||||
def test_taxonomy_tags_drop_parenthetical_explanations_and_punctuation(self):
|
||||
entry = self.module.EntryContext(
|
||||
name="close",
|
||||
signature="close()",
|
||||
kind="function",
|
||||
scope="dotnet",
|
||||
page="dotnet/datawarehouse/after_market.md",
|
||||
summary="收盘价,与系统参数股票、时间、周期和复权相关",
|
||||
)
|
||||
tags = self.module.derive_tags(
|
||||
entry,
|
||||
self.module.PageContext(
|
||||
"数据仓库 / 盘后相关(在当日可用于盘中)", entry.page
|
||||
),
|
||||
)
|
||||
|
||||
self.assertIn("数据仓库", tags)
|
||||
self.assertIn("盘后", tags)
|
||||
self.assertNotIn("(", tags)
|
||||
self.assertNotIn(")", tags)
|
||||
self.assertNotIn("盘后相关在当日可用于盘中", tags)
|
||||
self.assertNotIn("关闭", tags)
|
||||
self.assertNotIn("close", tags)
|
||||
|
||||
def test_name_intent_does_not_override_an_explicit_action_clause(self):
|
||||
entry = self.module.EntryContext(
|
||||
name="openEndFundFilterByFundIndex",
|
||||
signature="openEndFundFilterByFundIndex(fund_inds)",
|
||||
kind="function",
|
||||
scope="dotnet",
|
||||
page="dotnet/financial/fund-pool.md",
|
||||
summary="根据基金指数获取成分基金,并对基金进行筛选",
|
||||
)
|
||||
tags = self.module.derive_tags(
|
||||
entry,
|
||||
self.module.PageContext("金融 / 基金 / 基金池", entry.page),
|
||||
)
|
||||
|
||||
self.assertNotIn("打开", tags)
|
||||
self.assertNotIn("open", tags)
|
||||
|
||||
|
||||
class MarkdownEnrichmentTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.module = load_script()
|
||||
|
||||
def test_inserts_tags_after_description_without_touching_code(self):
|
||||
text = (
|
||||
"# Builtin / 数组\n\n"
|
||||
"## `arrDropDuplicate(data)`\n\n"
|
||||
"声明:function\n\n"
|
||||
"删除数组中的重复元素。\n\n"
|
||||
"| 参数 | 类型 | 说明 |\n"
|
||||
"| --- | --- | --- |\n"
|
||||
"| `data` | array | 输入数组 |\n\n"
|
||||
"返回:array\n\n"
|
||||
"### 示例\n\n"
|
||||
"```tsl\nreturn arrDropDuplicate(array(1, 1));\n```\n"
|
||||
)
|
||||
|
||||
result, audit = self.module.enrich_markdown(
|
||||
text, "builtin/array.md", self.module.SourceCatalog.empty()
|
||||
)
|
||||
repeated, repeated_audit = self.module.enrich_markdown(
|
||||
result, "builtin/array.md", self.module.SourceCatalog.empty()
|
||||
)
|
||||
|
||||
self.assertIn("删除数组中的重复元素\n\n<!-- tags:", result)
|
||||
self.assertIn("return arrDropDuplicate(array(1, 1));", result)
|
||||
self.assertEqual(result, repeated)
|
||||
self.assertEqual(1, audit.changed_descriptions)
|
||||
self.assertEqual(1, audit.changed_tags)
|
||||
self.assertEqual(0, repeated_audit.changed_descriptions)
|
||||
self.assertEqual(0, repeated_audit.changed_tags)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -164,19 +164,21 @@ class DocGenCliTest(unittest.TestCase):
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertTrue(self.generated("base/my_functions").is_file())
|
||||
|
||||
def test_legacy_positional_input_remains_supported(self):
|
||||
def test_positional_input_is_rejected(self):
|
||||
result = self.run_raw_cli(self.input)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertTrue(self.generated("base/my_functions").is_file())
|
||||
self.assertEqual(2, result.returncode)
|
||||
self.assertIn(f"无法识别的参数: {self.input}", result.stderr)
|
||||
self.assertFalse(self.generated("base/my_functions").exists())
|
||||
|
||||
def test_help_marks_legacy_input_as_deprecated(self):
|
||||
def test_help_only_documents_named_input_options(self):
|
||||
result = self.run_raw_cli("--help")
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertIn("--file INPUT_FILE", result.stdout)
|
||||
self.assertIn("--dir INPUT_DIR", result.stdout)
|
||||
self.assertIn("已废弃,请使用 --file", result.stdout)
|
||||
self.assertNotIn("positional arguments:", result.stdout)
|
||||
self.assertNotIn("已废弃", result.stdout)
|
||||
|
||||
def test_invalid_format_error_is_fully_chinese(self):
|
||||
result = self.run_raw_cli("--file", self.input, "--format", "xml")
|
||||
@@ -192,7 +194,6 @@ class DocGenCliTest(unittest.TestCase):
|
||||
cases = (
|
||||
(),
|
||||
("--file", self.input, "--dir", self.root),
|
||||
(self.input, "--file", self.input),
|
||||
)
|
||||
for args in cases:
|
||||
with self.subTest(args=args):
|
||||
@@ -200,7 +201,7 @@ class DocGenCliTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(2, result.returncode)
|
||||
self.assertIn(
|
||||
"必须且只能指定一种输入方式:INPUT_FILE、--file 或 --dir",
|
||||
"必须且只能指定一种输入方式:--file 或 --dir",
|
||||
result.stderr,
|
||||
)
|
||||
|
||||
@@ -399,6 +400,49 @@ class DocGenCliTest(unittest.TestCase):
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertTrue(output.is_file())
|
||||
|
||||
def test_root_writes_under_explicit_project_root(self):
|
||||
output_root = self.root / "explicit-root"
|
||||
|
||||
result = self.run_cli("--root", str(output_root))
|
||||
|
||||
output = (
|
||||
output_root
|
||||
/ "skills"
|
||||
/ "tsl-api-reference"
|
||||
/ "references"
|
||||
/ "codegen"
|
||||
/ "project"
|
||||
/ "base"
|
||||
/ "my_functions.md"
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertTrue(output.is_file())
|
||||
self.assertFalse(self.generated("base/my_functions").exists())
|
||||
|
||||
def test_relative_root_is_resolved_from_current_working_directory(self):
|
||||
result = self.run_cli("--root", "explicit-root")
|
||||
|
||||
output = (
|
||||
self.root
|
||||
/ "explicit-root"
|
||||
/ "skills"
|
||||
/ "tsl-api-reference"
|
||||
/ "references"
|
||||
/ "codegen"
|
||||
/ "project"
|
||||
/ "base"
|
||||
/ "my_functions.md"
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertTrue(output.is_file())
|
||||
|
||||
def test_help_documents_root_option(self):
|
||||
result = self.run_raw_cli("--help")
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertIn("--root ROOT_DIR", result.stdout)
|
||||
self.assertIn("包含 skills 目录的项目根目录", result.stdout)
|
||||
|
||||
def test_output_option_is_rejected(self):
|
||||
result = self.run_cli("--output", str(self.root / "out.md"))
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
@@ -498,12 +542,19 @@ class DocGenCliTest(unittest.TestCase):
|
||||
output.write_text("原内容\n", encoding="utf-8")
|
||||
bin_dir = self.root / "bin"
|
||||
bin_dir.mkdir()
|
||||
fake_npx = bin_dir / "npx"
|
||||
fake_npx.write_text(
|
||||
"#!/bin/sh\necho formatter-failed >&2\nexit 9\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
fake_npx.chmod(0o755)
|
||||
if os.name == "nt":
|
||||
fake_npx = bin_dir / "npx.cmd"
|
||||
fake_npx.write_text(
|
||||
"@echo off\r\necho formatter-failed 1>&2\r\nexit /b 9\r\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
else:
|
||||
fake_npx = bin_dir / "npx"
|
||||
fake_npx.write_text(
|
||||
"#!/bin/sh\necho formatter-failed >&2\nexit 9\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
fake_npx.chmod(0o755)
|
||||
env = os.environ.copy()
|
||||
env["PATH"] = str(bin_dir)
|
||||
|
||||
@@ -884,6 +935,46 @@ class DocGenCliTest(unittest.TestCase):
|
||||
self.assertIn("范例02:多行输出", text)
|
||||
self.assertIn("// 输出:\n// first\n// second", text)
|
||||
|
||||
def test_singular_example_is_rejected_without_overwrite(self):
|
||||
self.write_input(
|
||||
{
|
||||
"module": "项目 / 旧示例格式",
|
||||
"path": "base/singular_example",
|
||||
"declarations": [
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "demo",
|
||||
"signature": "demo()",
|
||||
"desc": "示例函数。",
|
||||
"returns": "nil",
|
||||
"example": "return demo();",
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
self.assert_rejected_without_overwrite(
|
||||
"base/singular_example", "存在未知字段:example"
|
||||
)
|
||||
|
||||
def test_singular_method_example_is_rejected_without_overwrite(self):
|
||||
self.write_class_member_input(
|
||||
{
|
||||
"kind": "method",
|
||||
"name": "Run",
|
||||
"visibility": "public",
|
||||
"binding": "instance",
|
||||
"signature": "Run()",
|
||||
"desc": "运行。",
|
||||
"example": "return self.Run();",
|
||||
},
|
||||
"base/singular_method_example",
|
||||
)
|
||||
|
||||
self.assert_rejected_without_overwrite(
|
||||
"base/singular_method_example", "存在未知字段:example"
|
||||
)
|
||||
|
||||
def test_top_level_enum_sections_are_plain_text_not_emphasis_headings(self):
|
||||
self.write_input(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user