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.
835 lines
26 KiB
Python
835 lines
26 KiB
Python
import importlib.util
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
|
|
SCRIPT = Path(__file__).parents[1] / "scripts" / "lint.py"
|
|
API_MARKDOWN = Path(__file__).parents[1] / "scripts" / "api_markdown.py"
|
|
VALID_PAGE = (
|
|
"# 项目 / 示例\n\n"
|
|
"## `demo()`\n\n"
|
|
"声明:function\n\n"
|
|
"示例函数\n\n"
|
|
"返回:nil\n"
|
|
)
|
|
|
|
|
|
def load_api_markdown():
|
|
spec = importlib.util.spec_from_file_location("tsl_api_markdown", API_MARKDOWN)
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[spec.name] = module
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
class DocLintCliTest(unittest.TestCase):
|
|
def setUp(self):
|
|
self.temp_dir = tempfile.TemporaryDirectory()
|
|
self.root = Path(self.temp_dir.name)
|
|
self.page = self.root / "base" / "demo.md"
|
|
self.page.parent.mkdir()
|
|
self.page.write_text(VALID_PAGE, encoding="utf-8")
|
|
|
|
def tearDown(self):
|
|
self.temp_dir.cleanup()
|
|
|
|
def run_cli(self, *args):
|
|
return subprocess.run(
|
|
[sys.executable, str(SCRIPT), *map(str, args)],
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
)
|
|
|
|
def test_accepts_one_markdown_file(self):
|
|
result = self.run_cli("--file", self.page)
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertIn("1 files", result.stderr)
|
|
|
|
def test_accepts_one_directory(self):
|
|
result = self.run_cli("--dir", self.root)
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertIn("1 files", result.stderr)
|
|
|
|
def test_accepts_mixed_typed_h2_declarations(self):
|
|
self.page.write_text(
|
|
"# 示例 / 混合\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"
|
|
"### 示例\n\n"
|
|
"```tsl\nreturn Parse();\n```\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",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = self.run_cli("--file", self.page, "--strict")
|
|
|
|
self.assertEqual(0, result.returncode, result.stdout + result.stderr)
|
|
|
|
def test_declaration_must_use_exact_canonical_line(self):
|
|
self.page.write_text(
|
|
"# 示例 / 函数\n\n"
|
|
"## `Open()`\n\n"
|
|
"声明:Function\n\n"
|
|
"打开。\n\n"
|
|
"返回:nil\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = self.run_cli("--file", self.page)
|
|
|
|
self.assertEqual(1, result.returncode)
|
|
self.assertIn("[heading]", result.stdout)
|
|
self.assertIn("声明:function", result.stdout)
|
|
|
|
def test_business_type_metadata_is_not_a_declaration_marker(self):
|
|
module = load_api_markdown()
|
|
lines = [
|
|
"## `Account`",
|
|
"",
|
|
"声明:function",
|
|
"",
|
|
"远程服务器账户。",
|
|
"",
|
|
"类型:读写",
|
|
"",
|
|
"返回:nil",
|
|
]
|
|
|
|
entries = list(module.iter_api_entries(lines))
|
|
|
|
self.assertEqual(1, len(entries))
|
|
self.assertTrue(entries[0].heading.valid)
|
|
self.assertEqual("function", entries[0].heading.root_kind)
|
|
|
|
self.page.write_text(
|
|
"# 示例 / 遗留属性\n\n" + "\n".join(lines) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
result = self.run_cli("--file", self.page, "--strict")
|
|
self.assertEqual(0, result.returncode, result.stdout + result.stderr)
|
|
|
|
def test_declaration_must_be_first_non_empty_line_after_heading(self):
|
|
self.page.write_text(
|
|
"# 示例 / 函数\n\n"
|
|
"## `Open()`\n\n"
|
|
"打开。\n\n"
|
|
"声明:function\n\n"
|
|
"返回:nil\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = self.run_cli("--file", self.page)
|
|
|
|
self.assertEqual(1, result.returncode)
|
|
self.assertIn("[heading]", result.stdout)
|
|
self.assertIn("第一条非空正文", result.stdout)
|
|
|
|
def test_tags_must_follow_description(self):
|
|
self.page.write_text(
|
|
"# 示例 / 函数\n\n"
|
|
"## `Open()`\n\n"
|
|
"声明:function\n\n"
|
|
"<!-- tags: 示例 -->\n\n"
|
|
"打开。\n\n"
|
|
"返回:nil\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = self.run_cli("--file", self.page)
|
|
|
|
self.assertEqual(1, result.returncode)
|
|
self.assertIn("[description]", result.stdout)
|
|
|
|
def test_property_type_is_optional_but_access_is_required(self):
|
|
page_without_type = (
|
|
"# 示例 / 类\n\n"
|
|
"## `Widget`\n\n"
|
|
"声明:class\n\n"
|
|
"表示组件。\n\n"
|
|
"### `Title`\n\n"
|
|
"声明:property\n\n"
|
|
"组件标题。\n\n"
|
|
"可见性:`public`\n\n"
|
|
"访问:read\n"
|
|
)
|
|
self.page.write_text(page_without_type, encoding="utf-8")
|
|
|
|
valid = self.run_cli("--file", self.page, "--strict")
|
|
|
|
self.assertEqual(0, valid.returncode, valid.stdout + valid.stderr)
|
|
|
|
self.page.write_text(
|
|
page_without_type.replace("\n访问:read\n", "\n"),
|
|
encoding="utf-8",
|
|
)
|
|
invalid = self.run_cli("--file", self.page)
|
|
|
|
self.assertEqual(1, invalid.returncode)
|
|
self.assertIn("[access]", invalid.stdout)
|
|
|
|
def test_accepts_class_page_with_optional_method_return(self):
|
|
self.page.write_text(
|
|
"# 示例 / 类\n\n"
|
|
"## `Widget`\n\n"
|
|
"声明:class\n\n"
|
|
"表示组件。\n\n"
|
|
"### `Close()`\n\n"
|
|
"声明:function\n\n"
|
|
"关闭组件。\n\n"
|
|
"可见性:`public`\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = self.run_cli("--file", self.page, "--strict")
|
|
|
|
self.assertEqual(0, result.returncode, result.stdout + result.stderr)
|
|
|
|
def test_class_member_requires_visibility(self):
|
|
self.page.write_text(
|
|
"# 示例 / 类\n\n"
|
|
"## `Widget`\n\n"
|
|
"声明:class\n\n"
|
|
"表示组件。\n\n"
|
|
"### `Count`\n\n"
|
|
"声明:field\n\n"
|
|
"组件数量。\n\n"
|
|
"类型:integer\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = self.run_cli("--file", self.page)
|
|
|
|
self.assertEqual(1, result.returncode)
|
|
self.assertIn("[visibility]", result.stdout)
|
|
|
|
def test_parameterized_property_requires_the_fixed_parameter_table(self):
|
|
self.page.write_text(
|
|
"# 示例 / 类\n\n"
|
|
"## `Widget`\n\n"
|
|
"声明:class\n\n"
|
|
"表示组件。\n\n"
|
|
"### `Items(index)`\n\n"
|
|
"声明:property\n\n"
|
|
"按索引读取组件。\n\n"
|
|
"可见性:`public`\n\n"
|
|
"类型:Widget\n\n"
|
|
"访问:read\n\n"
|
|
"| 名称 | 类型 | 说明 |\n"
|
|
"| --- | --- | --- |\n"
|
|
"| `index` | integer | 索引 |\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = self.run_cli("--file", self.page)
|
|
|
|
self.assertEqual(1, result.returncode)
|
|
self.assertIn("[param-header]", result.stdout)
|
|
|
|
def test_nullary_property_rejects_a_parameter_table(self):
|
|
self.page.write_text(
|
|
"# 示例 / 类\n\n"
|
|
"## `Widget`\n\n"
|
|
"声明:class\n\n"
|
|
"表示组件。\n\n"
|
|
"### `Title`\n\n"
|
|
"声明:property\n\n"
|
|
"组件标题。\n\n"
|
|
"可见性:`public`\n\n"
|
|
"类型:string\n\n"
|
|
"访问:read\n\n"
|
|
"| 参数 | 类型 | 说明 |\n"
|
|
"| --- | --- | --- |\n"
|
|
"| `unused` | integer | 不应存在 |\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = self.run_cli("--file", self.page)
|
|
|
|
self.assertEqual(1, result.returncode)
|
|
self.assertIn("[param-table]", result.stdout)
|
|
|
|
def test_class_method_allows_h4_value_and_example_subheadings(self):
|
|
self.page.write_text(
|
|
"# 示例 / 类\n\n"
|
|
"## `Widget`\n\n"
|
|
"声明:class\n\n"
|
|
"表示组件。\n\n"
|
|
"### `Open(mode)`\n\n"
|
|
"声明:function\n\n"
|
|
"打开组件。\n\n"
|
|
"可见性:`public`\n\n"
|
|
"| 参数 | 类型 | 说明 |\n"
|
|
"| --- | --- | --- |\n"
|
|
"| `mode` | integer | 打开模式 |\n\n"
|
|
"#### `mode` 取值\n\n"
|
|
"- `0` — 默认模式\n\n"
|
|
"#### 示例\n\n"
|
|
"```tsl\nreturn Open(0);\n```\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = self.run_cli("--file", self.page, "--strict")
|
|
|
|
self.assertEqual(0, result.returncode, result.stdout + result.stderr)
|
|
|
|
def test_property_rejects_example_subheading(self):
|
|
self.page.write_text(
|
|
"# 示例 / 类\n\n"
|
|
"## `Widget`\n\n"
|
|
"声明:class\n\n"
|
|
"表示组件。\n\n"
|
|
"### `Title`\n\n"
|
|
"声明:property\n\n"
|
|
"组件标题。\n\n"
|
|
"可见性:`public`\n\n"
|
|
"类型:string\n\n"
|
|
"访问:read\n\n"
|
|
"#### 示例\n\n"
|
|
"```tsl\nreturn Widget.Title;\n```\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = self.run_cli("--file", self.page)
|
|
|
|
self.assertEqual(1, result.returncode)
|
|
self.assertIn("[subheading]", result.stdout)
|
|
self.assertIn("property", result.stdout)
|
|
|
|
def test_accepts_unit_page_with_nested_class_member(self):
|
|
self.page.write_text(
|
|
"# 示例 / Unit\n\n"
|
|
"## `DemoUnit`\n\n"
|
|
"声明:unit\n\n"
|
|
"提供接口。\n\n"
|
|
"### `DefaultSize`\n\n"
|
|
"声明:const\n\n"
|
|
"默认大小。\n\n"
|
|
"值:`100`\n\n"
|
|
"### `Open()`\n\n"
|
|
"声明:function\n\n"
|
|
"打开。\n\n"
|
|
"返回:Document\n\n"
|
|
"### `Document`\n\n"
|
|
"声明:class\n\n"
|
|
"文档对象。\n\n"
|
|
"#### `Save()`\n\n"
|
|
"声明:function\n\n"
|
|
"保存。\n\n"
|
|
"可见性:`protected`\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = self.run_cli("--file", self.page, "--strict")
|
|
|
|
self.assertEqual(0, result.returncode, result.stdout + result.stderr)
|
|
|
|
def test_unit_class_method_allows_h5_value_and_example_subheadings(self):
|
|
self.page.write_text(
|
|
"# 示例 / Unit\n\n"
|
|
"## `DemoUnit`\n\n"
|
|
"声明:unit\n\n"
|
|
"提供接口。\n\n"
|
|
"### `Document`\n\n"
|
|
"声明:class\n\n"
|
|
"文档对象。\n\n"
|
|
"#### `Save(mode)`\n\n"
|
|
"声明:function\n\n"
|
|
"保存。\n\n"
|
|
"可见性:`public`\n\n"
|
|
"| 参数 | 类型 | 说明 |\n"
|
|
"| --- | --- | --- |\n"
|
|
"| `mode` | integer | 保存模式 |\n\n"
|
|
"##### `mode` 取值\n\n"
|
|
"- `0` — 默认模式\n\n"
|
|
"##### 示例\n\n"
|
|
"```tsl\nreturn Save(0);\n```\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = self.run_cli("--file", self.page, "--strict")
|
|
|
|
self.assertEqual(0, result.returncode, result.stdout + result.stderr)
|
|
|
|
def test_unit_function_requires_return(self):
|
|
self.page.write_text(
|
|
"# 示例 / Unit\n\n"
|
|
"## `DemoUnit`\n\n"
|
|
"声明:unit\n\n"
|
|
"提供接口。\n\n"
|
|
"### `Open()`\n\n"
|
|
"声明:function\n\n"
|
|
"打开。\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = self.run_cli("--file", self.page)
|
|
|
|
self.assertEqual(1, result.returncode)
|
|
self.assertIn("[return]", result.stdout)
|
|
|
|
def test_unit_function_rejects_an_empty_return_type(self):
|
|
self.page.write_text(
|
|
"# 示例 / Unit\n\n"
|
|
"## `DemoUnit`\n\n"
|
|
"声明:unit\n\n"
|
|
"提供接口。\n\n"
|
|
"### `Open()`\n\n"
|
|
"声明:function\n\n"
|
|
"打开。\n\n"
|
|
"返回:\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = self.run_cli("--file", self.page)
|
|
|
|
self.assertEqual(1, result.returncode)
|
|
self.assertIn("[return]", result.stdout)
|
|
|
|
def test_empty_type_and_value_lines_are_rejected(self):
|
|
cases = {
|
|
"property": (
|
|
"# 示例 / 类\n\n## `Widget`\n\n声明:class\n\n组件。\n\n"
|
|
"### `Title`\n\n声明:property\n\n标题。\n\n可见性:`public`\n\n"
|
|
"类型:\n\n访问:read\n",
|
|
"[type]",
|
|
),
|
|
"field": (
|
|
"# 示例 / 类\n\n## `Widget`\n\n声明:class\n\n组件。\n\n"
|
|
"### `Count`\n\n声明:field\n\n数量。\n\n可见性:`public`\n\n类型:\n",
|
|
"[type]",
|
|
),
|
|
"variable": (
|
|
"# 示例 / Unit\n\n## `DemoUnit`\n\n声明:unit\n\n接口。\n\n"
|
|
"### `Current`\n\n声明:var\n\n当前值。\n\n类型:\n",
|
|
"[type]",
|
|
),
|
|
"constant": (
|
|
"# 示例 / Unit\n\n## `DemoUnit`\n\n声明:unit\n\n接口。\n\n"
|
|
"### `Default`\n\n声明:const\n\n默认值。\n\n值:\n",
|
|
"[value]",
|
|
),
|
|
}
|
|
for name, (text, expected) in cases.items():
|
|
with self.subTest(name=name):
|
|
self.page.write_text(text, encoding="utf-8")
|
|
|
|
result = self.run_cli("--file", self.page)
|
|
|
|
self.assertEqual(1, result.returncode)
|
|
self.assertIn(expected, result.stdout)
|
|
|
|
def test_class_parent_metadata_does_not_count_as_description(self):
|
|
self.page.write_text(
|
|
"# 示例 / 类\n\n"
|
|
"## `Widget`\n\n"
|
|
"声明:class\n\n"
|
|
"父类:`BaseWidget`\n\n"
|
|
"",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = self.run_cli("--file", self.page)
|
|
|
|
self.assertEqual(1, result.returncode)
|
|
self.assertIn("[description]", result.stdout)
|
|
|
|
def test_class_parameter_rows_must_match_signature(self):
|
|
self.page.write_text(
|
|
"# 示例 / 类\n\n"
|
|
"## `Widget`\n\n"
|
|
"声明:class\n\n"
|
|
"表示组件。\n\n"
|
|
"### `Open(mode)`\n\n"
|
|
"声明:function\n\n"
|
|
"打开组件。\n\n"
|
|
"可见性:`public`\n\n"
|
|
"| 参数 | 类型 | 说明 |\n"
|
|
"| --- | --- | --- |\n"
|
|
"| `other` | integer | 错误参数 |\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = self.run_cli("--file", self.page)
|
|
|
|
self.assertEqual(1, result.returncode)
|
|
self.assertIn("[param-names]", result.stdout)
|
|
|
|
def test_class_parameter_rows_require_type_and_description(self):
|
|
self.page.write_text(
|
|
"# 示例 / 类\n\n"
|
|
"## `Widget`\n\n"
|
|
"声明:class\n\n"
|
|
"表示组件。\n\n"
|
|
"### `Open(mode)`\n\n"
|
|
"声明:function\n\n"
|
|
"打开组件。\n\n"
|
|
"可见性:`public`\n\n"
|
|
"| 参数 | 类型 | 说明 |\n"
|
|
"| --- | --- | --- |\n"
|
|
"| `mode` | | |\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = self.run_cli("--file", self.page)
|
|
|
|
self.assertEqual(1, result.returncode)
|
|
self.assertIn("[param-row]", result.stdout)
|
|
|
|
def test_unit_class_member_requires_visibility(self):
|
|
self.page.write_text(
|
|
"# 示例 / Unit\n\n"
|
|
"## `DemoUnit`\n\n"
|
|
"声明:unit\n\n"
|
|
"提供接口。\n\n"
|
|
"### `Document`\n\n"
|
|
"声明:class\n\n"
|
|
"文档对象。\n\n"
|
|
"#### `Name`\n\n"
|
|
"声明:field\n\n"
|
|
"文档名称。\n\n"
|
|
"类型:string\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = self.run_cli("--file", self.page)
|
|
|
|
self.assertEqual(1, result.returncode)
|
|
self.assertIn("[visibility]", result.stdout)
|
|
|
|
def test_class_and_unit_member_data_lines_are_required(self):
|
|
cases = {
|
|
"property": (
|
|
"# 示例 / 类\n\n## `Widget`\n\n声明:class\n\n组件。\n\n"
|
|
"### `Title`\n\n声明:property\n\n标题。\n\n可见性:`public`\n",
|
|
"[access]",
|
|
),
|
|
"field": (
|
|
"# 示例 / 类\n\n## `Widget`\n\n声明:class\n\n组件。\n\n"
|
|
"### `Count`\n\n声明:field\n\n数量。\n\n可见性:`public`\n",
|
|
"[type]",
|
|
),
|
|
"variable": (
|
|
"# 示例 / Unit\n\n## `DemoUnit`\n\n声明:unit\n\n接口。\n\n"
|
|
"### `Current`\n\n声明:var\n\n当前值。\n",
|
|
"[type]",
|
|
),
|
|
"constant": (
|
|
"# 示例 / Unit\n\n## `DemoUnit`\n\n声明:unit\n\n接口。\n\n"
|
|
"### `Default`\n\n声明:const\n\n默认值。\n",
|
|
"[value]",
|
|
),
|
|
}
|
|
for name, (text, expected) in cases.items():
|
|
with self.subTest(name=name):
|
|
self.page.write_text(text, encoding="utf-8")
|
|
|
|
result = self.run_cli("--file", self.page)
|
|
|
|
self.assertEqual(1, result.returncode)
|
|
self.assertIn(expected, result.stdout)
|
|
|
|
def test_class_legacy_typed_api_heading_is_rejected(self):
|
|
self.page.write_text(
|
|
"# 示例 / 类\n\n"
|
|
"## `Widget`\n\n"
|
|
"声明:class\n\n"
|
|
"表示组件。\n\n"
|
|
"### function `Open()`\n\n"
|
|
"打开组件。\n\n"
|
|
"可见性:`public`\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = self.run_cli("--file", self.page)
|
|
|
|
self.assertEqual(1, result.returncode)
|
|
self.assertIn("[heading]", result.stdout)
|
|
self.assertIn("标题只写名称或调用签名", result.stdout)
|
|
|
|
def test_unit_legacy_typed_api_heading_is_rejected(self):
|
|
self.page.write_text(
|
|
"# 示例 / Unit\n\n"
|
|
"## `DemoUnit`\n\n"
|
|
"声明:unit\n\n"
|
|
"提供接口。\n\n"
|
|
"### function `Open()`\n\n"
|
|
"打开。\n\n"
|
|
"返回:Document\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = self.run_cli("--file", self.page)
|
|
|
|
self.assertEqual(1, result.returncode)
|
|
self.assertIn("[heading]", result.stdout)
|
|
self.assertIn("标题只写名称或调用签名", result.stdout)
|
|
|
|
def test_class_member_at_h4_is_rejected(self):
|
|
self.page.write_text(
|
|
"# 示例 / 类\n\n"
|
|
"## `Widget`\n\n"
|
|
"声明:class\n\n"
|
|
"表示组件。\n\n"
|
|
"#### `Open()`\n\n"
|
|
"声明:function\n\n"
|
|
"打开组件。\n\n"
|
|
"可见性:`public`\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = self.run_cli("--file", self.page)
|
|
|
|
self.assertEqual(1, result.returncode)
|
|
self.assertIn("[heading]", result.stdout)
|
|
self.assertIn("H3", result.stdout)
|
|
|
|
def test_unit_class_member_at_h5_is_rejected(self):
|
|
self.page.write_text(
|
|
"# 示例 / Unit\n\n"
|
|
"## `DemoUnit`\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",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = self.run_cli("--file", self.page)
|
|
|
|
self.assertEqual(1, result.returncode)
|
|
self.assertIn("[heading]", result.stdout)
|
|
self.assertIn("H4", result.stdout)
|
|
|
|
def test_rejects_static_function_and_private_api(self):
|
|
self.page.write_text(
|
|
"# 示例 / 类\n\n"
|
|
"## `Widget`\n\n"
|
|
"声明:class\n\n"
|
|
"表示组件。\n\n"
|
|
"### `Create()`\n\n"
|
|
"声明:static function\n\n"
|
|
"创建。\n\n"
|
|
"可见性:`private`\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = self.run_cli("--file", self.page)
|
|
|
|
self.assertEqual(1, result.returncode)
|
|
self.assertIn("static function", result.stdout)
|
|
self.assertIn("private", result.stdout)
|
|
|
|
def test_mixed_h2_declarations_reset_nested_context(self):
|
|
module = load_api_markdown()
|
|
lines = [
|
|
"# Demo",
|
|
"",
|
|
"## `Widget`",
|
|
"",
|
|
"声明:class",
|
|
"",
|
|
"组件。",
|
|
"",
|
|
"### `Open()`",
|
|
"",
|
|
"声明:function",
|
|
"",
|
|
"打开。",
|
|
"",
|
|
"可见性:public",
|
|
"",
|
|
"## `Parse(text)`",
|
|
"",
|
|
"声明:function",
|
|
"",
|
|
"解析。",
|
|
"",
|
|
"返回:Widget",
|
|
"",
|
|
"## `DemoUnit`",
|
|
"",
|
|
"声明:unit",
|
|
"",
|
|
"接口。",
|
|
"",
|
|
"### `Document`",
|
|
"",
|
|
"声明:class",
|
|
"",
|
|
"文档。",
|
|
"",
|
|
"#### `Save()`",
|
|
"",
|
|
"声明:function",
|
|
"",
|
|
"保存。",
|
|
"",
|
|
"可见性:public",
|
|
]
|
|
|
|
headings = [entry.heading for entry in module.iter_api_entries(lines)]
|
|
|
|
self.assertEqual(
|
|
[
|
|
(2, "class", "class", "Widget"),
|
|
(3, "method", "class", "Open"),
|
|
(2, "function", "function", "Parse"),
|
|
(2, "unit", "unit", "DemoUnit"),
|
|
(3, "class", "unit", "Document"),
|
|
(4, "method", "unit", "Save"),
|
|
],
|
|
[
|
|
(item.level, item.kind, item.root_kind, item.name)
|
|
for item in headings
|
|
],
|
|
)
|
|
|
|
def test_missing_duplicate_and_unknown_declarations_are_errors(self):
|
|
module = load_api_markdown()
|
|
cases = {
|
|
"missing": (
|
|
["## `Open()`", "", "打开。", "", "返回:nil"],
|
|
"第一条非空正文",
|
|
),
|
|
"duplicate": (
|
|
[
|
|
"## `Widget`",
|
|
"",
|
|
"声明:class",
|
|
"",
|
|
"组件。",
|
|
"",
|
|
"声明:unit",
|
|
],
|
|
"必须且只能包含一行",
|
|
),
|
|
"unknown": (
|
|
["## `Runtime`", "", "声明:module", "", "接口。"],
|
|
"必须是 function、class 或 unit",
|
|
),
|
|
}
|
|
for name, (lines, expected) in cases.items():
|
|
with self.subTest(name=name):
|
|
headings = [
|
|
entry.heading for entry in module.iter_api_entries(lines)
|
|
]
|
|
|
|
self.assertEqual(1, len(headings))
|
|
self.assertFalse(headings[0].valid)
|
|
self.assertIn(expected, headings[0].error)
|
|
|
|
def test_plain_h2_ends_api_body_and_resets_nested_context(self):
|
|
module = load_api_markdown()
|
|
lines = [
|
|
"# Demo",
|
|
"",
|
|
"## `Parse()`",
|
|
"",
|
|
"声明:function",
|
|
"",
|
|
"解析。",
|
|
"",
|
|
"返回:nil",
|
|
"",
|
|
"## 其他说明",
|
|
"",
|
|
"### function `NotAMember()`",
|
|
]
|
|
|
|
entries = list(module.iter_api_entries(lines))
|
|
|
|
self.assertEqual(["Parse"], [entry.heading.name for entry in entries])
|
|
self.assertEqual(10, entries[0].end)
|
|
|
|
def test_parameter_value_and_example_headings_are_not_api_entries(self):
|
|
module = load_api_markdown()
|
|
lines = [
|
|
"# Demo",
|
|
"",
|
|
"## `Widget`",
|
|
"",
|
|
"声明:class",
|
|
"",
|
|
"表示组件。",
|
|
"",
|
|
"### `Open(mode)`",
|
|
"",
|
|
"声明:function",
|
|
"",
|
|
"打开。",
|
|
"",
|
|
"可见性:`public`",
|
|
"",
|
|
"#### `mode` 取值",
|
|
"",
|
|
"#### 示例",
|
|
"",
|
|
"## `Parse(mode)`",
|
|
"",
|
|
"声明:function",
|
|
"",
|
|
"解析。",
|
|
"",
|
|
"| 参数 | 类型 | 说明 |",
|
|
"| --- | --- | --- |",
|
|
"| `mode` | integer | 模式 |",
|
|
"",
|
|
"**mode 取值**",
|
|
"",
|
|
"### 示例",
|
|
"",
|
|
"## `DemoUnit`",
|
|
"",
|
|
"声明:unit",
|
|
"",
|
|
"接口。",
|
|
"",
|
|
"### `Load(mode)`",
|
|
"",
|
|
"声明:function",
|
|
"",
|
|
"加载。",
|
|
"",
|
|
"#### `mode` 取值",
|
|
"",
|
|
"#### 示例",
|
|
]
|
|
|
|
entries = list(module.iter_api_entries(lines))
|
|
|
|
self.assertEqual(
|
|
["Widget", "Open", "Parse", "DemoUnit", "Load"],
|
|
[entry.heading.name for entry in entries],
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|