feat(tsl-codegen): support unified API declarations

Add TSF conversion and shared Markdown recognition for mixed
function, class, and unit declarations.

Extend generation, linting, indexing, examples, and tests around
the unified declaration model.
This commit is contained in:
csh
2026-07-29 15:45:50 +08:00
parent bd25835d2d
commit 9edc8fc868
13 changed files with 8438 additions and 310 deletions
+296 -1
View File
@@ -33,8 +33,9 @@ class FunctionIndexTest(unittest.TestCase):
leaf.write_text(
"# Builtin - 基础 / 数组\n\n"
"## `demo()`\n\n"
"<!-- tags: 数组 列表 -->\n\n"
"声明:function\n\n"
"返回示例值。\n\n"
"<!-- tags: 数组 列表 -->\n\n"
"返回:integer\n",
encoding="utf-8",
)
@@ -50,6 +51,12 @@ class FunctionIndexTest(unittest.TestCase):
result = self.module.main(["--skill-dir", str(self.skill_dir), *args])
return result, stdout.getvalue(), stderr.getvalue()
def write_page(self, relative_path, text):
page = self.codegen_root / relative_path
page.parent.mkdir(parents=True, exist_ok=True)
page.write_text(text, encoding="utf-8")
return page
def test_rebuild_writes_only_tsv(self):
result, _, _ = self.run_main()
@@ -64,6 +71,294 @@ class FunctionIndexTest(unittest.TestCase):
self.assertEqual("数组 列表", row["tags"])
self.assertEqual("返回示例值。", row["summary"])
def test_declaration_does_not_become_a_missing_description_summary(self):
self.write_page(
"project/missing_description.md",
"# Missing\n\n"
"## `Missing()`\n\n"
"声明:function\n\n"
"返回:nil\n",
)
rows = {
row[0]: dict(zip(self.module.HEADER, row))
for row in self.module.build_rows(self.codegen_root)
}
self.assertEqual("", rows["Missing"]["summary"])
def test_legacy_function_anchor_and_first_eight_columns_are_unchanged(self):
rows = self.module.build_rows(self.codegen_root)
row = dict(zip(self.module.HEADER, rows[0]))
self.assertEqual(
[
"name",
"scope",
"module",
"signature",
"page",
"anchor",
"tags",
"summary",
"kind",
"binding",
"visibility",
"owner",
"qualified_name",
],
self.module.HEADER,
)
self.assertEqual(
[
"demo",
"builtin",
"base",
"demo()",
"builtin/base/array.md",
"demo",
"数组 列表",
"返回示例值。",
],
rows[0][:8],
)
self.assertEqual("function", row["kind"])
self.assertEqual("", row["binding"])
self.assertEqual("", row["visibility"])
self.assertEqual("", row["owner"])
self.assertEqual("demo", row["qualified_name"])
def test_mixed_page_resets_owner_for_every_h2(self):
self.write_page(
"project/mixed/api.md",
"# Mixed\n\n"
"## `Widget`\n\n声明:class\n\n组件。\n\n"
"### `Open()`\n\n声明:function\n\n打开。\n\n可见性:public\n\n"
"## `Parse()`\n\n声明:function\n\n解析。\n\n返回:Widget\n\n"
"## `Runtime`\n\n声明:unit\n\n接口。\n\n"
"### `Document`\n\n声明:class\n\n文档。\n\n"
"#### `Save()`\n\n声明:function\n\n保存。\n\n可见性:public\n",
)
rows = {
(row[0], row[3]): dict(zip(self.module.HEADER, row))
for row in self.module.build_rows(self.codegen_root)
}
self.assertEqual("", rows[("Widget", "Widget")]["owner"])
self.assertEqual("Widget", rows[("Open", "Open()")]["owner"])
self.assertEqual("", rows[("Parse", "Parse()")]["owner"])
self.assertEqual("", rows[("Runtime", "Runtime")]["owner"])
self.assertEqual("Runtime", rows[("Document", "Document")]["owner"])
self.assertEqual(
"Runtime.Document", rows[("Save", "Save()")]["owner"]
)
def test_class_page_maps_members_to_unified_index_columns(self):
self.write_page(
"project/widgets/widget.md",
"# Project / Widgets\n\n"
"## `Widget`\n\n"
"声明:class\n\n"
"组件。\n\n"
"<!-- tags: 组件 -->\n\n"
"### `Open(path)`\n\n"
"声明:function\n\n"
"打开组件。\n\n"
"可见性:`public`\n\n"
"### `Create()`\n\n"
"声明:class function\n\n"
"创建组件。\n\n"
"可见性:`protected`\n\n"
"### `Count`\n\n"
"声明:static field\n\n"
"组件数量。\n\n"
"可见性:`protected`\n\n"
"类型:integer\n\n"
"### `Items(index)`\n\n"
"声明:property\n\n"
"按索引读取组件。\n\n"
"可见性:`public`\n\n"
"类型:Widget\n\n"
"访问:read\n\n"
"### `DefaultName`\n\n"
"声明:const\n\n"
"默认名称。\n\n"
"可见性:`public`\n\n"
"值:`'widget'`\n\n"
"### `Maximum`\n\n"
"声明:static const\n\n"
"最大数量。\n\n"
"可见性:`protected`\n\n"
"值:`100`\n",
)
rows = {
row[self.module.HEADER.index("qualified_name")]: dict(
zip(self.module.HEADER, row)
)
for row in self.module.build_rows(self.codegen_root)
}
self.assertEqual(
{
"name": "Widget",
"scope": "project",
"module": "widgets",
"signature": "Widget",
"page": "project/widgets/widget.md",
"anchor": "widget",
"tags": "组件",
"summary": "组件。",
"kind": "class",
"binding": "",
"visibility": "",
"owner": "",
"qualified_name": "Widget",
},
rows["Widget"],
)
self.assertEqual("method", rows["Widget.Open"]["kind"])
self.assertEqual("instance", rows["Widget.Open"]["binding"])
self.assertEqual("public", rows["Widget.Open"]["visibility"])
self.assertEqual("Widget", rows["Widget.Open"]["owner"])
self.assertEqual("openpath", rows["Widget.Open"]["anchor"])
self.assertEqual("class", rows["Widget.Create"]["binding"])
self.assertEqual("create", rows["Widget.Create"]["anchor"])
self.assertEqual("field", rows["Widget.Count"]["kind"])
self.assertEqual("static", rows["Widget.Count"]["binding"])
self.assertEqual("count", rows["Widget.Count"]["anchor"])
self.assertEqual("property", rows["Widget.Items"]["kind"])
self.assertEqual("instance", rows["Widget.Items"]["binding"])
self.assertEqual("Items(index)", rows["Widget.Items"]["signature"])
self.assertEqual("constant", rows["Widget.DefaultName"]["kind"])
self.assertEqual("instance", rows["Widget.DefaultName"]["binding"])
self.assertEqual("constant", rows["Widget.Maximum"]["kind"])
self.assertEqual("static", rows["Widget.Maximum"]["binding"])
self.assertEqual("maximum", rows["Widget.Maximum"]["anchor"])
def test_unit_page_derives_nested_owner_and_public_interface_visibility(self):
self.write_page(
"dotnet/runtime/demo_unit.md",
"# Dotnet / Runtime\n\n"
"## `DemoUnit`\n\n"
"声明:unit\n\n"
"接口。\n\n"
"### `Open()`\n\n"
"声明:function\n\n"
"打开。\n\n"
"返回:Document\n\n"
"### `Current`\n\n"
"声明:var\n\n"
"当前文档。\n\n"
"类型:Document\n\n"
"### `DefaultName`\n\n"
"声明:const\n\n"
"默认名称。\n\n"
"值:`'demo'`\n\n"
"### `Document`\n\n"
"声明:class\n\n"
"文档对象。\n\n"
"#### `Save()`\n\n"
"声明:function\n\n"
"保存。\n\n"
"可见性:`protected`\n",
)
rows = {
row[self.module.HEADER.index("qualified_name")]: dict(
zip(self.module.HEADER, row)
)
for row in self.module.build_rows(self.codegen_root)
}
self.assertEqual("unit", rows["DemoUnit"]["kind"])
self.assertEqual("", rows["DemoUnit"]["owner"])
self.assertEqual("unit", rows["DemoUnit.Open"]["binding"])
self.assertEqual("public", rows["DemoUnit.Open"]["visibility"])
self.assertEqual("DemoUnit", rows["DemoUnit.Open"]["owner"])
self.assertEqual("variable", rows["DemoUnit.Current"]["kind"])
self.assertEqual("constant", rows["DemoUnit.DefaultName"]["kind"])
self.assertEqual("unit", rows["DemoUnit.DefaultName"]["binding"])
self.assertEqual("public", rows["DemoUnit.DefaultName"]["visibility"])
self.assertEqual("unit", rows["DemoUnit.Document"]["binding"])
self.assertEqual("public", rows["DemoUnit.Document"]["visibility"])
self.assertEqual("DemoUnit.Document", rows["DemoUnit.Document.Save"]["owner"])
self.assertEqual("method", rows["DemoUnit.Document.Save"]["kind"])
self.assertEqual("instance", rows["DemoUnit.Document.Save"]["binding"])
self.assertEqual("protected", rows["DemoUnit.Document.Save"]["visibility"])
def test_class_overloads_share_qualified_name_but_have_distinct_anchors(self):
self.write_page(
"project/widgets/overloads.md",
"# Project / Widgets\n\n"
"## `Overloads`\n\n"
"声明:class\n\n"
"重载示例。\n\n"
"### `Open(path)`\n\n"
"声明:function\n\n"
"按路径打开。\n\n"
"可见性:`public`\n\n"
"### `Open(mode, flags)`\n\n"
"声明:function\n\n"
"按模式打开。\n\n"
"可见性:`public`\n",
)
rows = [
dict(zip(self.module.HEADER, row))
for row in self.module.build_rows(self.codegen_root)
if row[0] == "Open"
]
self.assertEqual(2, len(rows))
self.assertEqual({"Overloads.Open"}, {row["qualified_name"] for row in rows})
self.assertEqual(
{"openpath", "openmodeflags"},
{row["anchor"] for row in rows},
)
self.assertEqual(
2,
len({f"{row['page']}#{row['anchor']}" for row in rows}),
)
def test_top_level_overloads_and_cross_kind_names_get_unique_anchors(self):
page = "project/mixed/overloads.md"
self.write_page(
page,
"# Project / Mixed\n\n"
"## `Open(path)`\n\n声明:function\n\n按路径打开。\n\n"
"返回:nil\n\n"
"## `Open(mode)`\n\n声明:function\n\n按模式打开。\n\n"
"返回:nil\n\n"
"## `Open`\n\n声明:class\n\n打开器。\n\n"
"## `Open`\n\n声明:unit\n\n打开接口。\n",
)
rows = [
dict(zip(self.module.HEADER, row))
for row in self.module.build_rows(self.codegen_root)
if row[4] == page
]
by_identity = {
(row["kind"], row["signature"]): row for row in rows
}
self.assertEqual(4, len(rows))
self.assertEqual({""}, {row["owner"] for row in rows})
self.assertEqual({"Open"}, {row["qualified_name"] for row in rows})
self.assertEqual(
"open", by_identity[("function", "Open(path)")]["anchor"]
)
self.assertEqual(
"open-1", by_identity[("function", "Open(mode)")]["anchor"]
)
self.assertEqual("open-2", by_identity[("class", "Open")]["anchor"])
self.assertEqual("open-3", by_identity[("unit", "Open")]["anchor"])
self.assertEqual(
4, len({f"{row['page']}#{row['anchor']}" for row in rows})
)
def test_check_does_not_require_index_pages(self):
self.data_dir.mkdir(parents=True)
rows = self.module.build_rows(self.codegen_root)