Recover recognizable function, procedure, class, and unit structure from nonstandard TSF input while preserving strict parsing for compliant files. Keep STANDARD.md limited to published format requirements and cover recovery paths with regression tests.
2042 lines
66 KiB
Python
2042 lines
66 KiB
Python
import importlib.util
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import textwrap
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
SCRIPT = Path(__file__).parents[1] / "scripts" / "convert_tsf.py"
|
|
|
|
|
|
def load_script():
|
|
spec = importlib.util.spec_from_file_location(
|
|
"tsl_codegen_convert_tsf", SCRIPT
|
|
)
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[spec.name] = module
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
class ConvertTsfCliTest(unittest.TestCase):
|
|
def setUp(self):
|
|
self.temp_dir = tempfile.TemporaryDirectory()
|
|
self.root = Path(self.temp_dir.name)
|
|
self.output = self.root / "entry.json"
|
|
|
|
def tearDown(self):
|
|
self.temp_dir.cleanup()
|
|
|
|
def write_tsf(self, name, source):
|
|
path = self.root / f"{name}.tsf"
|
|
path.write_text(textwrap.dedent(source).lstrip(), encoding="utf-8")
|
|
return path
|
|
|
|
def write_nested_tsf(self, directory, name, source):
|
|
path = self.root / directory / f"{name}.tsf"
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(textwrap.dedent(source).lstrip(), encoding="utf-8")
|
|
return path
|
|
|
|
def run_cli(
|
|
self,
|
|
*inputs,
|
|
fmt="json",
|
|
output=None,
|
|
module="示例 / 函数",
|
|
path="base/example",
|
|
):
|
|
output = output or self.output
|
|
return subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(SCRIPT),
|
|
*map(str, inputs),
|
|
"--format",
|
|
fmt,
|
|
"--module",
|
|
module,
|
|
"--path",
|
|
path,
|
|
"--output",
|
|
str(output),
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
)
|
|
|
|
def read_declarations(self):
|
|
return json.loads(self.output.read_text(encoding="utf-8"))[
|
|
"declarations"
|
|
]
|
|
|
|
def read_declaration(self):
|
|
declarations = self.read_declarations()
|
|
self.assertEqual(1, len(declarations))
|
|
return declarations[0]
|
|
|
|
def test_json_converts_complete_function_document(self):
|
|
source = self.write_tsf(
|
|
"Normalize",
|
|
"""
|
|
function Normalize(mode: integer = 0): integer;
|
|
begin
|
|
/// 按指定模式处理并返回模式值。
|
|
/// 第二行说明。
|
|
/// @tags: 示例 枚举
|
|
/// @param: mode {INTEGER} 处理模式,默认 0
|
|
/// @values: mode
|
|
/// 0: 原样返回
|
|
/// 1: 去重
|
|
/// "auto": 自动判断
|
|
/// @returns: INTEGER
|
|
/// @example: 使用默认模式
|
|
/// return Normalize();
|
|
/// @output:
|
|
/// 0
|
|
/// @example: 指定模式
|
|
/// return Normalize(1);
|
|
return mode;
|
|
end;
|
|
""",
|
|
)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
self.assertEqual("", result.stdout)
|
|
self.assertIn(f"已写入 {self.output}", result.stderr)
|
|
self.assertEqual(
|
|
{
|
|
"module": "示例 / 函数",
|
|
"path": "base/example",
|
|
"declarations": [
|
|
{
|
|
"kind": "function",
|
|
"name": "Normalize",
|
|
"signature": "Normalize(mode)",
|
|
"desc": "按指定模式处理并返回模式值。\n第二行说明。",
|
|
"tags": ["示例", "枚举"],
|
|
"params": [
|
|
{
|
|
"name": "mode",
|
|
"type": "integer",
|
|
"optional": True,
|
|
"desc": "处理模式,默认 0",
|
|
"values": [
|
|
{"value": 0, "desc": "原样返回"},
|
|
{"value": 1, "desc": "去重"},
|
|
{"value": "auto", "desc": "自动判断"},
|
|
],
|
|
}
|
|
],
|
|
"returns": "integer",
|
|
"examples": [
|
|
{
|
|
"desc": "使用默认模式",
|
|
"code": "return Normalize();",
|
|
"output": "0",
|
|
},
|
|
{
|
|
"desc": "指定模式",
|
|
"code": "return Normalize(1);",
|
|
},
|
|
],
|
|
}
|
|
],
|
|
},
|
|
json.loads(self.output.read_text(encoding="utf-8")),
|
|
)
|
|
|
|
def test_documented_parameter_types_fill_untyped_signature(self):
|
|
source = self.write_tsf(
|
|
"DocumentedTypes",
|
|
"""
|
|
function DocumentedTypes(x, y): integer;
|
|
begin
|
|
/// 补充无类型签名的参数类型
|
|
/// @param: x {integer} 第一个值
|
|
/// @param: y {array of integer} 第二个值
|
|
return x;
|
|
end;
|
|
""",
|
|
)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
self.assertEqual(
|
|
[
|
|
{"name": "x", "type": "integer", "desc": "第一个值"},
|
|
{
|
|
"name": "y",
|
|
"type": "array of integer",
|
|
"desc": "第二个值",
|
|
},
|
|
],
|
|
self.read_declaration()["params"],
|
|
)
|
|
|
|
def test_declared_parameter_types_use_colon_and_semicolon(self):
|
|
source = self.write_tsf(
|
|
"DeclaredTypes",
|
|
"""
|
|
function DeclaredTypes(x: integer; y: array of integer): integer;
|
|
begin
|
|
/// 读取签名中的参数类型
|
|
/// @param: x 第一个值
|
|
/// @param: y 第二个值
|
|
return x;
|
|
end;
|
|
""",
|
|
)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
self.assertEqual(
|
|
[
|
|
{"name": "x", "type": "integer", "desc": "第一个值"},
|
|
{
|
|
"name": "y",
|
|
"type": "array of integer",
|
|
"desc": "第二个值",
|
|
},
|
|
],
|
|
self.read_declaration()["params"],
|
|
)
|
|
|
|
def test_matching_documented_parameter_type_keeps_declared_spelling(self):
|
|
source = self.write_tsf(
|
|
"MatchingParamType",
|
|
"""
|
|
function MatchingParamType(value: Array Of Integer): integer;
|
|
begin
|
|
/// 校验重复记录的参数类型
|
|
/// @param: value {array of integer} 输入值
|
|
return 1;
|
|
end;
|
|
""",
|
|
)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
self.assertEqual(
|
|
[
|
|
{
|
|
"name": "value",
|
|
"type": "Array Of Integer",
|
|
"desc": "输入值",
|
|
}
|
|
],
|
|
self.read_declaration()["params"],
|
|
)
|
|
|
|
def test_parameter_type_mismatch_creates_structural_draft(self):
|
|
source = self.write_tsf(
|
|
"ParamTypeMismatch",
|
|
"""
|
|
function ParamTypeMismatch(value: integer): integer;
|
|
begin
|
|
/// 校验参数类型
|
|
/// @param: value {string} 输入值
|
|
return value;
|
|
end;
|
|
""",
|
|
)
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
function = self.read_declaration()
|
|
self.assertEqual("", function["desc"])
|
|
self.assertEqual(
|
|
[{"name": "value", "type": "integer", "desc": ""}],
|
|
function["params"],
|
|
)
|
|
self.assertEqual("integer", function["returns"])
|
|
|
|
def test_malformed_parameter_docs_create_structural_draft(self):
|
|
cases = {
|
|
"EmptyParamType": "/// @param: value {} 输入值",
|
|
"UnclosedParamType": "/// @param: value {integer 输入值",
|
|
"MissingParamDescription": "/// @param: value {integer}",
|
|
}
|
|
for name, param_line in cases.items():
|
|
with self.subTest(name=name):
|
|
source = self.write_tsf(
|
|
name,
|
|
f"""
|
|
function {name}(value): integer;
|
|
begin
|
|
/// 校验参数类型格式
|
|
{param_line}
|
|
return value;
|
|
end;
|
|
""",
|
|
)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
function = self.read_declaration()
|
|
self.assertEqual("", function["desc"])
|
|
self.assertEqual(
|
|
[{"name": "value", "type": "", "desc": ""}],
|
|
function["params"],
|
|
)
|
|
self.assertEqual("integer", function["returns"])
|
|
|
|
def test_multiple_tsf_files_are_merged_in_input_order(self):
|
|
first = self.write_tsf(
|
|
"First",
|
|
"""
|
|
function First(): integer;
|
|
begin
|
|
/// 第一个函数。
|
|
return 1;
|
|
end;
|
|
""",
|
|
)
|
|
second = self.write_tsf(
|
|
"Second",
|
|
"""
|
|
function Second(): string;
|
|
begin
|
|
/// 第二个函数。
|
|
return "second";
|
|
end;
|
|
""",
|
|
)
|
|
|
|
result = self.run_cli(first, second)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
data = json.loads(self.output.read_text(encoding="utf-8"))
|
|
self.assertEqual(
|
|
["First()", "Second()"],
|
|
[
|
|
function["signature"]
|
|
for function in data["declarations"]
|
|
],
|
|
)
|
|
|
|
def test_mixed_function_class_unit_inputs_preserve_cli_order(self):
|
|
cls = self.write_tsf(
|
|
"Widget",
|
|
"""
|
|
type Widget = class
|
|
/// 组件。
|
|
end;
|
|
""",
|
|
)
|
|
function = self.write_tsf(
|
|
"OpenWidget",
|
|
"""
|
|
function OpenWidget(): Widget;
|
|
begin
|
|
/// 打开组件。
|
|
return nil;
|
|
end;
|
|
""",
|
|
)
|
|
unit = self.write_tsf(
|
|
"WidgetRuntime",
|
|
"""
|
|
unit WidgetRuntime;
|
|
/// 运行时接口。
|
|
interface
|
|
implementation
|
|
end.
|
|
""",
|
|
)
|
|
|
|
result = self.run_cli(cls, function, unit)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
declarations = json.loads(
|
|
self.output.read_text(encoding="utf-8")
|
|
)["declarations"]
|
|
self.assertEqual(
|
|
[
|
|
("class", "Widget"),
|
|
("function", "OpenWidget"),
|
|
("unit", "WidgetRuntime"),
|
|
],
|
|
[(item["kind"], item["name"]) for item in declarations],
|
|
)
|
|
|
|
def test_yaml_serialization_uses_same_data_structure(self):
|
|
try:
|
|
import yaml
|
|
except ImportError:
|
|
self.skipTest("未安装 pyyaml")
|
|
source = self.write_tsf(
|
|
"Describe",
|
|
"""
|
|
function Describe(value: string);
|
|
begin
|
|
/// 返回输入内容。
|
|
/// @param: value 输入内容
|
|
/// @returns: string
|
|
return value;
|
|
end;
|
|
""",
|
|
)
|
|
output = self.root / "entry.yaml"
|
|
|
|
result = self.run_cli(source, fmt="yaml", output=output)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
self.assertEqual(
|
|
{
|
|
"module": "示例 / 函数",
|
|
"path": "base/example",
|
|
"declarations": [
|
|
{
|
|
"kind": "function",
|
|
"name": "Describe",
|
|
"signature": "Describe(value)",
|
|
"desc": "返回输入内容。",
|
|
"params": [
|
|
{
|
|
"name": "value",
|
|
"type": "string",
|
|
"desc": "输入内容",
|
|
}
|
|
],
|
|
"returns": "string",
|
|
}
|
|
],
|
|
},
|
|
yaml.safe_load(output.read_text(encoding="utf-8")),
|
|
)
|
|
|
|
def test_gb18030_encoded_tsf_is_supported(self):
|
|
source = self.root / "Legacy.tsf"
|
|
source.write_bytes(textwrap.dedent("""
|
|
function Legacy(): string;
|
|
begin
|
|
/// 返回中文内容。
|
|
return "中文";
|
|
end;
|
|
""").lstrip().encode("gb18030"))
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
function = self.read_declaration()
|
|
self.assertEqual("返回中文内容。", function["desc"])
|
|
|
|
def test_utf8_bom_encoded_tsf_is_supported(self):
|
|
source = self.root / "BomDemo.tsf"
|
|
source.write_text(
|
|
"function BomDemo(): integer;\n"
|
|
"begin\n"
|
|
" /// 带 BOM 的说明。\n"
|
|
" return 1;\n"
|
|
"end;\n",
|
|
encoding="utf-8-sig",
|
|
)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
function = self.read_declaration()
|
|
self.assertEqual("带 BOM 的说明。", function["desc"])
|
|
|
|
def test_output_must_not_overwrite_an_input_tsf(self):
|
|
source = self.write_tsf(
|
|
"KeepSource",
|
|
"""
|
|
function KeepSource(): integer;
|
|
begin
|
|
/// 保留源文件。
|
|
return 1;
|
|
end;
|
|
""",
|
|
)
|
|
original = source.read_text(encoding="utf-8")
|
|
|
|
result = self.run_cli(source, output=source)
|
|
|
|
self.assertEqual(1, result.returncode)
|
|
self.assertIn("输出文件不能覆盖输入 tsf", result.stderr)
|
|
self.assertEqual(original, source.read_text(encoding="utf-8"))
|
|
|
|
def test_atomic_write_failure_preserves_existing_output_and_cleans_temp(self):
|
|
module = load_script()
|
|
self.assertTrue(
|
|
hasattr(module, "atomic_write"), "atomic_write is missing"
|
|
)
|
|
self.output.write_text("原内容\n", encoding="utf-8")
|
|
|
|
with mock.patch.object(
|
|
module.os, "replace", side_effect=OSError("replace failed")
|
|
):
|
|
with self.assertRaisesRegex(OSError, "replace failed"):
|
|
module.atomic_write(self.output, "新内容\n")
|
|
|
|
self.assertEqual(
|
|
"原内容\n", self.output.read_text(encoding="utf-8")
|
|
)
|
|
self.assertEqual(
|
|
[], list(self.root.glob(f".{self.output.name}.*.tmp"))
|
|
)
|
|
|
|
def test_missing_parameter_metadata_and_return_type_create_a_draft(self):
|
|
source = self.write_tsf(
|
|
"Draft",
|
|
"""
|
|
function Draft(value);
|
|
begin
|
|
/// 尚待完善的函数。
|
|
return value;
|
|
end;
|
|
""",
|
|
)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
function = self.read_declaration()
|
|
self.assertEqual(
|
|
[{"name": "value", "type": "", "desc": ""}],
|
|
function["params"],
|
|
)
|
|
self.assertEqual("", function["returns"])
|
|
|
|
def test_legacy_document_block_creates_structural_draft(self):
|
|
source = self.write_tsf(
|
|
"LegacyDraft",
|
|
"""
|
|
function LegacyDraft(required: integer; optional = 1): array;
|
|
begin
|
|
{**
|
|
@param(required)(integer) 必填值
|
|
@param(optional)(integer) 可选值
|
|
@return(array) 结果
|
|
**}
|
|
return array();
|
|
end;
|
|
""",
|
|
)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
self.assertEqual(
|
|
{
|
|
"kind": "function",
|
|
"name": "LegacyDraft",
|
|
"signature": "LegacyDraft(required, optional)",
|
|
"desc": "",
|
|
"params": [
|
|
{"name": "required", "type": "integer", "desc": ""},
|
|
{
|
|
"name": "optional",
|
|
"type": "",
|
|
"optional": True,
|
|
"desc": "",
|
|
},
|
|
],
|
|
"returns": "array",
|
|
},
|
|
self.read_declaration(),
|
|
)
|
|
|
|
def test_json_draft_completes_fixed_fields_for_every_declaration_kind(self):
|
|
function_source = self.write_tsf(
|
|
"DraftFunction",
|
|
"""
|
|
function DraftFunction();
|
|
begin
|
|
/// 草稿函数
|
|
return;
|
|
end;
|
|
""",
|
|
)
|
|
class_source = self.write_tsf(
|
|
"DraftClass",
|
|
"""
|
|
type DraftClass = class
|
|
class function Build(); // 构建草稿
|
|
property Title read title_;
|
|
value_;
|
|
const Limit = 1;
|
|
end;
|
|
""",
|
|
)
|
|
unit_source = self.write_tsf(
|
|
"DraftUnit",
|
|
"""
|
|
unit DraftUnit;
|
|
interface
|
|
function Open();
|
|
var Current;
|
|
const Limit = 1;
|
|
type Item = class
|
|
function Read();
|
|
end;
|
|
implementation
|
|
end.
|
|
""",
|
|
)
|
|
|
|
result = self.run_cli(function_source, class_source, unit_source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
function, cls, unit = self.read_declarations()
|
|
self.assertEqual([], function.get("params"))
|
|
self.assertEqual("", function.get("returns"))
|
|
|
|
method, prop, field, constant = cls["members"]
|
|
self.assertEqual([], method.get("params"))
|
|
self.assertEqual("", method.get("returns"))
|
|
self.assertEqual("", prop.get("type"))
|
|
self.assertEqual([], prop.get("params"))
|
|
self.assertEqual("", field.get("type"))
|
|
self.assertEqual("", constant.get("type"))
|
|
|
|
unit_function, variable, unit_constant, nested_class = unit["members"]
|
|
self.assertEqual([], unit_function.get("params"))
|
|
self.assertEqual("", unit_function.get("returns"))
|
|
self.assertEqual("", variable.get("type"))
|
|
self.assertEqual("", unit_constant.get("type"))
|
|
self.assertEqual([], nested_class["members"][0].get("params"))
|
|
self.assertEqual("", nested_class["members"][0].get("returns"))
|
|
|
|
for item in (function, cls, unit, method, prop, field, constant):
|
|
self.assertNotIn("tags", item)
|
|
|
|
def test_documented_untyped_parameter_and_return_remain_empty_draft(self):
|
|
source = self.write_tsf(
|
|
"DocumentedDraft",
|
|
"""
|
|
function DocumentedDraft(value);
|
|
begin
|
|
/// 保留没有类型注解的函数
|
|
/// @param: value 输入值
|
|
return value;
|
|
end;
|
|
""",
|
|
)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
function = self.read_declaration()
|
|
self.assertEqual(
|
|
[{"name": "value", "type": "", "desc": "输入值"}],
|
|
function["params"],
|
|
)
|
|
self.assertEqual("", function["returns"])
|
|
|
|
def test_yaml_draft_uses_the_same_complete_fixed_fields(self):
|
|
try:
|
|
import yaml
|
|
except ImportError:
|
|
self.skipTest("未安装 pyyaml")
|
|
source = self.write_tsf(
|
|
"YamlDraft",
|
|
"""
|
|
type YamlDraft = class
|
|
class function Build(); // 构建草稿
|
|
property Title read title_;
|
|
const Limit = 1;
|
|
end;
|
|
""",
|
|
)
|
|
output = self.root / "draft.yaml"
|
|
|
|
result = self.run_cli(source, fmt="yaml", output=output)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
method, prop, constant = yaml.safe_load(
|
|
output.read_text(encoding="utf-8")
|
|
)["declarations"][0]["members"]
|
|
self.assertEqual([], method.get("params"))
|
|
self.assertEqual("", method.get("returns"))
|
|
self.assertEqual("", prop.get("type"))
|
|
self.assertEqual([], prop.get("params"))
|
|
self.assertEqual("", constant.get("type"))
|
|
|
|
def test_help_is_written_in_chinese(self):
|
|
result = subprocess.run(
|
|
[sys.executable, str(SCRIPT), "--help"],
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
self.assertIn("用法:", result.stdout)
|
|
self.assertIn("位置参数:", result.stdout)
|
|
self.assertIn("选项:", result.stdout)
|
|
self.assertIn("输出格式", result.stdout)
|
|
self.assertIn("function、class 和 unit", result.stdout)
|
|
self.assertIn("declarations", result.stdout)
|
|
self.assertNotIn("usage:", result.stdout)
|
|
self.assertNotIn("positional arguments:", result.stdout)
|
|
self.assertNotIn("options:", result.stdout)
|
|
|
|
def test_return_type_mismatch_creates_structural_draft(self):
|
|
source = self.write_tsf(
|
|
"Mismatch",
|
|
"""
|
|
function Mismatch(): integer;
|
|
begin
|
|
/// 返回一个值。
|
|
/// @returns: string
|
|
return 1;
|
|
end;
|
|
""",
|
|
)
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
function = self.read_declaration()
|
|
self.assertEqual("", function["desc"])
|
|
self.assertEqual("integer", function["returns"])
|
|
|
|
def test_late_document_block_creates_structural_draft(self):
|
|
source = self.write_tsf(
|
|
"LateDoc",
|
|
"""
|
|
function LateDoc(): integer;
|
|
begin
|
|
value := 1;
|
|
/// 这个文档块出现得太晚。
|
|
return value;
|
|
end;
|
|
""",
|
|
)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
function = self.read_declaration()
|
|
self.assertEqual("", function["desc"])
|
|
self.assertEqual("integer", function["returns"])
|
|
|
|
def test_missing_body_does_not_borrow_later_function_document(self):
|
|
source = self.write_tsf(
|
|
"MissingOwnBody",
|
|
"function MissingOwnBody(): integer;\n"
|
|
"function Helper(): integer;\n"
|
|
"begin\n"
|
|
" /// 辅助函数文档。\n"
|
|
" return 1;\n"
|
|
"end;\n",
|
|
)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
function = self.read_declaration()
|
|
self.assertEqual("MissingOwnBody", function["name"])
|
|
self.assertEqual("", function["desc"])
|
|
|
|
def test_duplicate_enum_value_creates_structural_draft(self):
|
|
source = self.write_tsf(
|
|
"DuplicateValue",
|
|
"""
|
|
function DuplicateValue(mode: integer): integer;
|
|
begin
|
|
/// 检查模式。
|
|
/// @param: mode 模式
|
|
/// @values: mode
|
|
/// 1: 第一项
|
|
/// 1: 重复项
|
|
return mode;
|
|
end;
|
|
""",
|
|
)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
function = self.read_declaration()
|
|
self.assertEqual("", function["desc"])
|
|
self.assertEqual(
|
|
[{"name": "mode", "type": "integer", "desc": ""}],
|
|
function["params"],
|
|
)
|
|
self.assertEqual("integer", function["returns"])
|
|
|
|
def test_directives_after_examples_create_structural_draft(self):
|
|
source = self.write_tsf(
|
|
"BadOrder",
|
|
"""
|
|
function BadOrder(value: integer): integer;
|
|
begin
|
|
/// 返回输入值。
|
|
/// @example: 基本用法
|
|
/// return BadOrder(1);
|
|
/// @param: value 输入值
|
|
return value;
|
|
end;
|
|
""",
|
|
)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
function = self.read_declaration()
|
|
self.assertEqual("", function["desc"])
|
|
self.assertEqual(
|
|
[{"name": "value", "type": "integer", "desc": ""}],
|
|
function["params"],
|
|
)
|
|
self.assertEqual("integer", function["returns"])
|
|
|
|
def test_class_converts_only_first_matching_public_class(self):
|
|
source = self.write_tsf(
|
|
"Widget",
|
|
"""
|
|
type Widget = class(BaseOne, BaseTwo)
|
|
/// 对外组件。
|
|
/// @tags: 组件 示例
|
|
public
|
|
/// 创建组件。
|
|
/// @param: name 组件名称
|
|
function create(name: string); overload;
|
|
/// 从配置创建。
|
|
/// @param: path 配置路径
|
|
class function FromConfig(path: string): Widget;
|
|
/// 组件标题。
|
|
property Title: string read title_ write title_;
|
|
protected
|
|
/// 下一个编号。
|
|
static next_id_: integer;
|
|
private
|
|
/// 内部状态。
|
|
hidden_: string;
|
|
end;
|
|
|
|
type WidgetHelper = class
|
|
public
|
|
function Hidden(): integer;
|
|
end;
|
|
""",
|
|
)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
self.assertEqual(
|
|
{
|
|
"kind": "class",
|
|
"name": "Widget",
|
|
"desc": "对外组件。",
|
|
"tags": ["组件", "示例"],
|
|
"bases": ["BaseOne", "BaseTwo"],
|
|
"members": [
|
|
{
|
|
"kind": "method",
|
|
"name": "create",
|
|
"visibility": "public",
|
|
"binding": "instance",
|
|
"signature": "create(name)",
|
|
"desc": "创建组件。",
|
|
"params": [
|
|
{
|
|
"name": "name",
|
|
"type": "string",
|
|
"desc": "组件名称",
|
|
}
|
|
],
|
|
"returns": "",
|
|
"modifiers": ["overload"],
|
|
},
|
|
{
|
|
"kind": "method",
|
|
"name": "FromConfig",
|
|
"visibility": "public",
|
|
"binding": "class",
|
|
"signature": "FromConfig(path)",
|
|
"desc": "从配置创建。",
|
|
"params": [
|
|
{
|
|
"name": "path",
|
|
"type": "string",
|
|
"desc": "配置路径",
|
|
}
|
|
],
|
|
"returns": "Widget",
|
|
},
|
|
{
|
|
"kind": "property",
|
|
"name": "Title",
|
|
"visibility": "public",
|
|
"desc": "组件标题。",
|
|
"type": "string",
|
|
"params": [],
|
|
"access": "readwrite",
|
|
},
|
|
{
|
|
"kind": "field",
|
|
"name": "next_id_",
|
|
"visibility": "protected",
|
|
"desc": "下一个编号。",
|
|
"type": "integer",
|
|
"static": True,
|
|
},
|
|
],
|
|
},
|
|
self.read_declaration(),
|
|
)
|
|
|
|
def test_class_out_of_class_implementation_does_not_duplicate_member(self):
|
|
source = self.write_tsf(
|
|
"Widget",
|
|
"""
|
|
type Widget = class
|
|
/// 对外组件。
|
|
public
|
|
/// 保存组件。
|
|
/// @param: path 保存路径
|
|
function Save(path: string): boolean;
|
|
end;
|
|
|
|
function Widget.Save(path: string): boolean;
|
|
begin
|
|
return true;
|
|
end;
|
|
""",
|
|
)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
members = self.read_declaration()["members"]
|
|
self.assertEqual(["Save"], [member["name"] for member in members])
|
|
|
|
def test_class_method_trailing_comment_fills_missing_description(self):
|
|
source = self.write_tsf(
|
|
"InlineComments",
|
|
"""
|
|
type InlineComments = class
|
|
class function Plain(value); // 普通行尾说明
|
|
class function Overloaded(value); overload; // 重载行尾说明
|
|
/// 正式文档说明
|
|
class function Documented(value); // 不覆盖正式文档
|
|
end;
|
|
""",
|
|
)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
self.assertEqual(
|
|
["普通行尾说明", "重载行尾说明", "正式文档说明"],
|
|
[member["desc"] for member in self.read_declaration()["members"]],
|
|
)
|
|
|
|
def test_class_parameterized_property_preserves_params_and_values(self):
|
|
source = self.write_tsf(
|
|
"Indexed",
|
|
"""
|
|
type Indexed = class
|
|
/// 索引集合。
|
|
public
|
|
/// 按索引读取项目。
|
|
/// @param: index 项目索引
|
|
/// @values: index
|
|
/// 0: 第一项
|
|
property Items(index: integer): string read GetItem write SetItem;
|
|
end;
|
|
""",
|
|
)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
member = self.read_declaration()["members"][0]
|
|
self.assertEqual("property", member["kind"])
|
|
self.assertEqual("string", member["type"])
|
|
self.assertEqual("readwrite", member["access"])
|
|
self.assertEqual(
|
|
[
|
|
{
|
|
"name": "index",
|
|
"type": "integer",
|
|
"desc": "项目索引",
|
|
"values": [{"value": 0, "desc": "第一项"}],
|
|
}
|
|
],
|
|
member["params"],
|
|
)
|
|
|
|
def test_class_instance_and_static_constants_are_preserved(self):
|
|
source = self.write_tsf(
|
|
"Constants",
|
|
"""
|
|
type Constants = class
|
|
/// 常量集合。
|
|
public
|
|
/// 默认数量。
|
|
const DefaultSize: integer = 0;
|
|
/// 是否禁用。
|
|
static const Disabled = false;
|
|
end;
|
|
""",
|
|
)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
members = self.read_declaration()["members"]
|
|
self.assertEqual(["DefaultSize", "Disabled"], [item["name"] for item in members])
|
|
self.assertEqual("integer", members[0]["type"])
|
|
self.assertEqual("0", members[0]["value"])
|
|
self.assertNotIn("static", members[0])
|
|
self.assertEqual("false", members[1]["value"])
|
|
self.assertIs(members[1]["static"], True)
|
|
|
|
def test_class_virtual_and_override_modifiers_preserve_source_order(self):
|
|
source = self.write_tsf(
|
|
"Modifiers",
|
|
"""
|
|
type Modifiers = class
|
|
/// 修饰符示例。
|
|
public
|
|
/// 检查状态。
|
|
function Inspect(): integer; virtual; overload;
|
|
/// 重置状态。
|
|
function Reset(); override;
|
|
end;
|
|
""",
|
|
)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
members = self.read_declaration()["members"]
|
|
self.assertEqual(["virtual", "overload"], members[0]["modifiers"])
|
|
self.assertEqual(["override"], members[1]["modifiers"])
|
|
|
|
def test_class_inline_method_is_collected_once(self):
|
|
source = self.write_tsf(
|
|
"InlineClass",
|
|
"""
|
|
type InlineClass = class
|
|
/// 内联类。
|
|
public
|
|
/// 读取当前值。
|
|
function Value(): integer;
|
|
begin
|
|
return 1;
|
|
end;
|
|
end;
|
|
""",
|
|
)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
members = self.read_declaration()["members"]
|
|
self.assertEqual(["Value"], [member["name"] for member in members])
|
|
|
|
def test_class_skips_unsupported_public_declaration(self):
|
|
source = self.write_tsf(
|
|
"UnsupportedClass",
|
|
"""
|
|
type UnsupportedClass = class
|
|
public
|
|
type Alias = integer;
|
|
end;
|
|
""",
|
|
)
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
self.assertEqual([], self.read_declaration()["members"])
|
|
|
|
def test_missing_class_and_unit_member_types_remain_explicit_drafts(self):
|
|
cls_source = self.write_tsf(
|
|
"TypeDraft",
|
|
"""
|
|
type TypeDraft = class
|
|
/// 类型草稿。
|
|
public
|
|
/// 未知字段类型。
|
|
value_;
|
|
/// 未知属性类型。
|
|
property Value read value_;
|
|
end;
|
|
""",
|
|
)
|
|
|
|
class_result = self.run_cli(cls_source)
|
|
|
|
self.assertEqual(0, class_result.returncode, class_result.stderr)
|
|
class_members = self.read_declaration()["members"]
|
|
self.assertEqual("", class_members[0]["type"])
|
|
self.assertEqual("", class_members[1]["type"])
|
|
self.assertEqual([], class_members[1]["params"])
|
|
|
|
unit_source = self.write_tsf(
|
|
"UnitTypeDraft",
|
|
"""
|
|
unit UnitTypeDraft;
|
|
/// 类型草稿接口。
|
|
interface
|
|
/// 未知变量类型。
|
|
var Current;
|
|
implementation
|
|
end.
|
|
""",
|
|
)
|
|
|
|
unit_result = self.run_cli(unit_source)
|
|
|
|
self.assertEqual(0, unit_result.returncode, unit_result.stderr)
|
|
unit_member = self.read_declaration()["members"][0]
|
|
self.assertEqual("", unit_member["type"])
|
|
|
|
def test_unit_collects_only_interface_members_in_source_order(self):
|
|
source = self.write_tsf(
|
|
"DemoUnit",
|
|
"""
|
|
unit DemoUnit;
|
|
/// 运行时接口。
|
|
/// @tags: 运行时 文档
|
|
interface
|
|
uses RuntimeSupport;
|
|
/// 默认数量。
|
|
const DefaultSize = 100;
|
|
/// 当前名称。
|
|
var CurrentName: string;
|
|
/// 打开对象。
|
|
/// @param: path 文件路径
|
|
function Open(path: string): Document;
|
|
type Document = class
|
|
/// 文档对象。
|
|
public
|
|
/// 保存文档。
|
|
function Save(): boolean;
|
|
end;
|
|
implementation
|
|
/// 这个实现文档不属于 API。
|
|
var hidden_: integer;
|
|
type HiddenDocument = class
|
|
public
|
|
function Hidden(): integer;
|
|
end;
|
|
const HiddenValue = 1;
|
|
function Open(path: string): Document;
|
|
begin
|
|
return nil;
|
|
end;
|
|
initialization
|
|
/// 生命周期文档也不属于 API。
|
|
hidden_ := 1;
|
|
finalization
|
|
hidden_ := 0;
|
|
end.
|
|
""",
|
|
)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
unit = self.read_declaration()
|
|
self.assertEqual("unit", unit["kind"])
|
|
self.assertEqual("DemoUnit", unit["name"])
|
|
self.assertEqual("运行时接口。", unit["desc"])
|
|
self.assertEqual(["运行时", "文档"], unit["tags"])
|
|
self.assertEqual(
|
|
["DefaultSize", "CurrentName", "Open", "Document"],
|
|
[member["name"] for member in unit["members"]],
|
|
)
|
|
self.assertEqual(
|
|
{
|
|
"kind": "constant",
|
|
"name": "DefaultSize",
|
|
"desc": "默认数量。",
|
|
"type": "",
|
|
"value": "100",
|
|
},
|
|
unit["members"][0],
|
|
)
|
|
self.assertEqual(
|
|
{
|
|
"kind": "variable",
|
|
"name": "CurrentName",
|
|
"desc": "当前名称。",
|
|
"type": "string",
|
|
},
|
|
unit["members"][1],
|
|
)
|
|
self.assertEqual("Document", unit["members"][2]["returns"])
|
|
self.assertEqual("class", unit["members"][3]["kind"])
|
|
self.assertEqual("Save", unit["members"][3]["members"][0]["name"])
|
|
serialized = json.dumps(unit, ensure_ascii=False)
|
|
self.assertNotIn("hidden_", serialized)
|
|
self.assertNotIn("HiddenDocument", serialized)
|
|
self.assertNotIn("HiddenValue", serialized)
|
|
self.assertNotIn("生命周期文档", serialized)
|
|
|
|
def test_incomplete_interface_class_does_not_scan_implementation(self):
|
|
source = self.write_tsf(
|
|
"IncompleteInterfaceClass",
|
|
"unit IncompleteInterfaceClass;\n"
|
|
"interface\n"
|
|
"type PublicType = class\n"
|
|
"public\n"
|
|
"function Keep(): integer;\n"
|
|
"implementation\n"
|
|
"function Hidden(): integer;\n"
|
|
"begin\n"
|
|
" return 1;\n"
|
|
"end;\n"
|
|
"end.\n",
|
|
)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
unit = self.read_declaration()
|
|
self.assertEqual(["PublicType"], [member["name"] for member in unit["members"]])
|
|
self.assertEqual(
|
|
["Keep"],
|
|
[member["name"] for member in unit["members"][0]["members"]],
|
|
)
|
|
|
|
def test_unit_filename_mismatch_keeps_declared_name(self):
|
|
source = self.write_tsf(
|
|
"Wrong",
|
|
"unit Right;\ninterface\nimplementation\nend.\n",
|
|
)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
self.assertEqual("Right", self.read_declaration()["name"])
|
|
|
|
def test_unit_preserves_multiple_interface_classes_in_source_order(self):
|
|
source = self.write_tsf(
|
|
"ClassUnit",
|
|
"""
|
|
unit ClassUnit;
|
|
/// 类接口。
|
|
interface
|
|
type First = class
|
|
/// 第一个类。
|
|
end;
|
|
type Second = class
|
|
/// 第二个类。
|
|
end;
|
|
implementation
|
|
end.
|
|
""",
|
|
)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
members = self.read_declaration()["members"]
|
|
self.assertEqual(["First", "Second"], [member["name"] for member in members])
|
|
|
|
def test_unit_unbound_document_is_ignored(self):
|
|
source = self.write_tsf(
|
|
"UnboundUnit",
|
|
"""
|
|
unit UnboundUnit;
|
|
interface
|
|
/// 无法绑定的接口文档。
|
|
// 普通注释切断绑定。
|
|
var Current: integer;
|
|
implementation
|
|
end.
|
|
""",
|
|
)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
current = self.read_declaration()["members"][0]
|
|
self.assertEqual("Current", current["name"])
|
|
self.assertEqual("", current["desc"])
|
|
|
|
def test_unit_unbound_header_document_is_ignored(self):
|
|
source = self.write_tsf(
|
|
"UnboundHeaderUnit",
|
|
"""
|
|
unit UnboundHeaderUnit;
|
|
/// 无法绑定的 unit 文档。
|
|
// 普通注释切断绑定。
|
|
interface
|
|
implementation
|
|
end.
|
|
""",
|
|
)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
self.assertEqual("", self.read_declaration()["desc"])
|
|
|
|
def test_unit_ignores_tokens_after_terminal_end_dot(self):
|
|
source = self.write_tsf(
|
|
"TrailingUnit",
|
|
"""
|
|
unit TrailingUnit;
|
|
interface
|
|
implementation
|
|
end.
|
|
function Trailing(): integer;
|
|
""",
|
|
)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
self.assertEqual("TrailingUnit", self.read_declaration()["name"])
|
|
|
|
def test_class_missing_docs_default_visibility_and_private_filter_create_draft(self):
|
|
source = self.write_tsf(
|
|
"DraftClass",
|
|
"""
|
|
type DraftClass = class
|
|
value_: integer;
|
|
private
|
|
/// 不应进入草稿。
|
|
hidden_: string;
|
|
protected
|
|
/// 检查状态。
|
|
function Inspect();
|
|
end;
|
|
""",
|
|
)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
cls = self.read_declaration()
|
|
self.assertEqual("", cls["desc"])
|
|
self.assertEqual(["value_", "Inspect"], [item["name"] for item in cls["members"]])
|
|
self.assertEqual("public", cls["members"][0]["visibility"])
|
|
self.assertEqual("", cls["members"][0]["desc"])
|
|
self.assertEqual("protected", cls["members"][1]["visibility"])
|
|
|
|
def test_class_private_multi_name_fields_and_constants_are_ignored(self):
|
|
source = self.write_tsf(
|
|
"PrivateMulti",
|
|
"""
|
|
type PrivateMulti = class
|
|
/// 私有声明示例。
|
|
private
|
|
/// 多名称私有字段。
|
|
left_, right_: integer;
|
|
/// 多名称私有常量。
|
|
const First, Second = 1;
|
|
public
|
|
/// 对外值。
|
|
value_: integer;
|
|
end;
|
|
""",
|
|
)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
members = self.read_declaration()["members"]
|
|
self.assertEqual(["value_"], [member["name"] for member in members])
|
|
|
|
def test_class_structure_errors_create_best_effort_drafts(self):
|
|
cases = {
|
|
"WrongName": (
|
|
"type Actual = class\nend;\n",
|
|
"Actual",
|
|
[],
|
|
),
|
|
"StaticMethod": (
|
|
"type StaticMethod = class\npublic\nstatic function Bad();\nend;\n",
|
|
"StaticMethod",
|
|
["Bad"],
|
|
),
|
|
"ManyFields": (
|
|
"type ManyFields = class\npublic\nleft_, right_: integer;\nend;\n",
|
|
"ManyFields",
|
|
["left_", "right_"],
|
|
),
|
|
}
|
|
for name, (text, expected_name, expected_members) in cases.items():
|
|
with self.subTest(name=name):
|
|
source = self.write_tsf(name, text)
|
|
result = self.run_cli(source)
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
declaration = self.read_declaration()
|
|
self.assertEqual(expected_name, declaration["name"])
|
|
self.assertEqual(
|
|
expected_members,
|
|
[member["name"] for member in declaration["members"]],
|
|
)
|
|
|
|
def test_class_and_unit_constants_split_multiple_names_into_drafts(self):
|
|
cases = {
|
|
"ManyConstants": (
|
|
"type ManyConstants = class\npublic\nconst Left, Right = 1;\nend;\n",
|
|
3,
|
|
),
|
|
"ManyUnitConstants": (
|
|
"unit ManyUnitConstants;\n"
|
|
"interface\n"
|
|
"const Left, Right = 1;\n"
|
|
"implementation\n"
|
|
"end.\n",
|
|
3,
|
|
),
|
|
}
|
|
for name, (text, _line) in cases.items():
|
|
with self.subTest(name=name):
|
|
source = self.write_tsf(name, text)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
self.assertEqual(
|
|
["Left", "Right"],
|
|
[member["name"] for member in self.read_declaration()["members"]],
|
|
)
|
|
|
|
def test_class_converts_procedure_to_method_draft(self):
|
|
source = self.write_tsf(
|
|
"ProcedureClass",
|
|
"""
|
|
type ProcedureClass = class
|
|
public
|
|
procedure Open();
|
|
end;
|
|
""",
|
|
)
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
method = self.read_declaration()["members"][0]
|
|
self.assertEqual("Open", method["name"])
|
|
self.assertEqual("method", method["kind"])
|
|
self.assertEqual("", method["returns"])
|
|
|
|
def test_class_unbound_document_is_ignored(self):
|
|
source = self.write_tsf(
|
|
"UnboundDoc",
|
|
"""
|
|
type UnboundDoc = class
|
|
/// 类描述。
|
|
public
|
|
/// 无法绑定的成员描述。
|
|
// 普通注释切断绑定。
|
|
value_: integer;
|
|
end;
|
|
""",
|
|
)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
field = self.read_declaration()["members"][0]
|
|
self.assertEqual("value_", field["name"])
|
|
self.assertEqual("", field["desc"])
|
|
|
|
def test_unit_converts_shorthand_and_skips_unsupported_types(self):
|
|
cases = {
|
|
"ShortUnit": (
|
|
"unit ShortUnit;\nfunction Open(): integer;\nbegin return 1; end;\nend.\n",
|
|
["Open"],
|
|
),
|
|
"ProcedureUnit": (
|
|
"unit ProcedureUnit;\ninterface\nprocedure Open();\nimplementation\nend.\n",
|
|
["Open"],
|
|
),
|
|
"AliasUnit": (
|
|
"unit AliasUnit;\ninterface\ntype Size = integer;\nimplementation\nend.\n",
|
|
[],
|
|
),
|
|
}
|
|
for name, (text, expected_members) in cases.items():
|
|
with self.subTest(name=name):
|
|
source = self.write_tsf(name, text)
|
|
result = self.run_cli(source)
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
self.assertEqual(
|
|
expected_members,
|
|
[member["name"] for member in self.read_declaration()["members"]],
|
|
)
|
|
|
|
def test_malformed_class_and_unit_documents_create_structural_drafts(self):
|
|
cls = self.write_tsf(
|
|
"BadClassDocs",
|
|
"""
|
|
type BadClassDocs = class
|
|
public
|
|
/// 打开值
|
|
/// @param: value {string} 输入值
|
|
function Open(value: integer): integer;
|
|
end;
|
|
""",
|
|
)
|
|
unit = self.write_tsf(
|
|
"BadUnitDocs",
|
|
"""
|
|
unit BadUnitDocs;
|
|
interface
|
|
/// 打开值
|
|
/// @returns: string
|
|
function Open(): integer;
|
|
implementation
|
|
end.
|
|
""",
|
|
)
|
|
|
|
result = self.run_cli(cls, unit)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
class_method = self.read_declarations()[0]["members"][0]
|
|
unit_function = self.read_declarations()[1]["members"][0]
|
|
self.assertEqual("", class_method["desc"])
|
|
self.assertEqual("integer", class_method["params"][0]["type"])
|
|
self.assertEqual("", unit_function["desc"])
|
|
self.assertEqual("integer", unit_function["returns"])
|
|
|
|
def test_incomplete_class_and_unit_keep_recognizable_members(self):
|
|
cases = {
|
|
"IncompleteClass": (
|
|
"type IncompleteClass = class\n"
|
|
"public\n"
|
|
"function Keep(value: integer): integer;\n",
|
|
"Keep",
|
|
),
|
|
"IncompleteUnit": (
|
|
"unit IncompleteUnit;\n"
|
|
"function Keep(value: integer): integer;\n",
|
|
"Keep",
|
|
),
|
|
}
|
|
for name, (text, expected_member) in cases.items():
|
|
with self.subTest(name=name):
|
|
source = self.write_tsf(name, text)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
self.assertEqual(
|
|
[expected_member],
|
|
[member["name"] for member in self.read_declaration()["members"]],
|
|
)
|
|
|
|
def test_broken_member_does_not_hide_following_function(self):
|
|
cases = {
|
|
"RecoverClass": (
|
|
"type RecoverClass = class\n"
|
|
"public\n"
|
|
"property Broken\n"
|
|
"function Keep(): integer;\n"
|
|
"end;\n",
|
|
"class",
|
|
),
|
|
"RecoverUnit": (
|
|
"unit RecoverUnit;\n"
|
|
"interface\n"
|
|
"const Broken\n"
|
|
"function Keep(): integer;\n"
|
|
"implementation\n"
|
|
"end.\n",
|
|
"unit",
|
|
),
|
|
}
|
|
for name, (text, expected_kind) in cases.items():
|
|
with self.subTest(name=name):
|
|
source = self.write_tsf(name, text)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
declaration = self.read_declaration()
|
|
self.assertEqual(expected_kind, declaration["kind"])
|
|
self.assertIn(
|
|
"Keep",
|
|
[member["name"] for member in declaration["members"]],
|
|
)
|
|
|
|
def test_unnamed_class_and_unit_functions_are_skipped(self):
|
|
cases = {
|
|
"UnnamedClassFunction": (
|
|
"type UnnamedClassFunction = class\n"
|
|
"public\n"
|
|
"function\n"
|
|
"function Keep(): integer;\n"
|
|
"end;\n",
|
|
"class",
|
|
),
|
|
"UnnamedUnitFunction": (
|
|
"unit UnnamedUnitFunction;\n"
|
|
"interface\n"
|
|
"function\n"
|
|
"function Keep(): integer;\n"
|
|
"implementation\n"
|
|
"end.\n",
|
|
"unit",
|
|
),
|
|
}
|
|
for name, (text, expected_kind) in cases.items():
|
|
with self.subTest(name=name):
|
|
source = self.write_tsf(name, text)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
declaration = self.read_declaration()
|
|
self.assertEqual(expected_kind, declaration["kind"])
|
|
self.assertEqual(
|
|
["Keep"],
|
|
[member["name"] for member in declaration["members"]],
|
|
)
|
|
|
|
def test_unnamed_members_do_not_hide_following_functions(self):
|
|
cases = {
|
|
"UnnamedProperty": (
|
|
"type UnnamedProperty = class\n"
|
|
"public\n"
|
|
"property\n"
|
|
"function Keep(): integer;\n"
|
|
"end;\n",
|
|
"class",
|
|
),
|
|
"UnnamedConstant": (
|
|
"unit UnnamedConstant;\n"
|
|
"interface\n"
|
|
"const\n"
|
|
"function Keep(): integer;\n"
|
|
"implementation\n"
|
|
"end.\n",
|
|
"unit",
|
|
),
|
|
}
|
|
for name, (text, expected_kind) in cases.items():
|
|
with self.subTest(name=name):
|
|
source = self.write_tsf(name, text)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
declaration = self.read_declaration()
|
|
self.assertEqual(expected_kind, declaration["kind"])
|
|
self.assertEqual(
|
|
["Keep"],
|
|
[member["name"] for member in declaration["members"]],
|
|
)
|
|
|
|
def test_method_missing_signature_semicolon_does_not_hide_next_member(self):
|
|
source = self.write_tsf(
|
|
"BrokenInlineMethod",
|
|
"type BrokenInlineMethod = class\n"
|
|
"public\n"
|
|
"function Broken()\n"
|
|
"begin\n"
|
|
" return 1;\n"
|
|
"end;\n"
|
|
"function Keep(): integer;\n"
|
|
"end;\n",
|
|
)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
self.assertEqual(
|
|
["Broken", "Keep"],
|
|
[member["name"] for member in self.read_declaration()["members"]],
|
|
)
|
|
|
|
def test_broken_headers_do_not_hide_first_function(self):
|
|
cases = {
|
|
"BrokenBase": (
|
|
"type BrokenBase = class(Base\n"
|
|
"function Keep(): integer;\n",
|
|
"class",
|
|
),
|
|
"MissingClassName": (
|
|
"type\n"
|
|
"class\n"
|
|
"function Keep(): integer;\n",
|
|
"class",
|
|
),
|
|
"BrokenUnitHeader": (
|
|
"unit BrokenUnitHeader\n"
|
|
"function Keep(): integer;\n",
|
|
"unit",
|
|
),
|
|
"MissingUnitName": (
|
|
"unit\n"
|
|
"function Keep(): integer;\n",
|
|
"unit",
|
|
),
|
|
}
|
|
for name, (text, expected_kind) in cases.items():
|
|
with self.subTest(name=name):
|
|
source = self.write_tsf(name, text)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
declaration = self.read_declaration()
|
|
self.assertEqual(expected_kind, declaration["kind"])
|
|
self.assertEqual(
|
|
["Keep"],
|
|
[member["name"] for member in declaration["members"]],
|
|
)
|
|
|
|
def test_declaration_truncation_after_name_always_creates_a_draft(self):
|
|
module = load_script()
|
|
samples = {
|
|
"function": (
|
|
"TruncateFunction.tsf",
|
|
"function TruncateFunction(required: integer; optional = 1): array;\n"
|
|
"begin\n"
|
|
" return array();\n"
|
|
"end;\n",
|
|
),
|
|
"class": (
|
|
"TruncateClass.tsf",
|
|
"type TruncateClass = class(Base)\n"
|
|
"public\n"
|
|
"function Open(value: integer): string;\n"
|
|
"property Value: integer read value_ write value_;\n"
|
|
"const Limit = 2;\n"
|
|
"value_: integer;\n"
|
|
"end;\n",
|
|
),
|
|
"unit": (
|
|
"TruncateUnit.tsf",
|
|
"unit TruncateUnit;\n"
|
|
"interface\n"
|
|
"function Open(value: integer): string;\n"
|
|
"const Limit = 2;\n"
|
|
"var Current: integer;\n"
|
|
"implementation\n"
|
|
"end.\n",
|
|
),
|
|
}
|
|
for expected_kind, (filename, source) in samples.items():
|
|
name_end = module.tokenize(source)[1].end
|
|
for cut in range(name_end, len(source) + 1):
|
|
with self.subTest(kind=expected_kind, cut=cut):
|
|
declaration = module.complete_draft_fields(
|
|
module.convert_source(source[:cut], Path(filename))
|
|
)
|
|
self.assertEqual(expected_kind, declaration["kind"])
|
|
self.assertTrue(declaration["name"])
|
|
|
|
def test_unit_fallback_keeps_bare_const_and_var_sections(self):
|
|
source = self.write_tsf(
|
|
"RecoverSections",
|
|
"""
|
|
unit RecoverSections;
|
|
interface
|
|
/// 无法绑定
|
|
// 切断文档绑定
|
|
const First = 1;
|
|
Second = 2;
|
|
var Current: integer;
|
|
Previous: integer;
|
|
implementation
|
|
end.
|
|
""",
|
|
)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
self.assertEqual(
|
|
["First", "Second", "Current", "Previous"],
|
|
[member["name"] for member in self.read_declaration()["members"]],
|
|
)
|
|
|
|
def test_unit_var_and_const_sections_keep_each_declaration(self):
|
|
source = self.write_tsf(
|
|
"StateUnit",
|
|
"""
|
|
unit StateUnit;
|
|
/// 状态接口。
|
|
interface
|
|
var
|
|
/// 当前名称。
|
|
CurrentName: string;
|
|
/// 当前编号。
|
|
CurrentId: integer;
|
|
const
|
|
/// 默认名称。
|
|
DefaultName = "demo";
|
|
/// 默认编号。
|
|
DefaultId = 1;
|
|
implementation
|
|
end.
|
|
""",
|
|
)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
members = self.read_declaration()["members"]
|
|
self.assertEqual(
|
|
["CurrentName", "CurrentId", "DefaultName", "DefaultId"],
|
|
[member["name"] for member in members],
|
|
)
|
|
self.assertEqual(
|
|
["当前名称。", "当前编号。", "默认名称。", "默认编号。"],
|
|
[member["desc"] for member in members],
|
|
)
|
|
|
|
def test_function_overloads_are_preserved_in_input_order(self):
|
|
first = self.write_nested_tsf(
|
|
"first",
|
|
"Parse",
|
|
"""
|
|
function Parse(path: string): integer;
|
|
begin
|
|
/// 按路径解析。
|
|
/// @param: path 文件路径
|
|
return 1;
|
|
end;
|
|
""",
|
|
)
|
|
second = self.write_nested_tsf(
|
|
"second",
|
|
"Parse",
|
|
"""
|
|
function Parse(mode: integer): integer;
|
|
begin
|
|
/// 按模式解析。
|
|
/// @param: mode 解析模式
|
|
return 1;
|
|
end;
|
|
""",
|
|
)
|
|
|
|
result = self.run_cli(first, second)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
self.assertEqual(
|
|
["Parse(path)", "Parse(mode)"],
|
|
[item["signature"] for item in self.read_declarations()],
|
|
)
|
|
|
|
def test_duplicate_function_signature_is_preserved_in_input_order(self):
|
|
first = self.write_nested_tsf(
|
|
"first",
|
|
"Parse",
|
|
"""
|
|
function Parse(value: string): integer;
|
|
begin
|
|
/// 解析值。
|
|
/// @param: value 输入值
|
|
return 1;
|
|
end;
|
|
""",
|
|
)
|
|
second = self.write_nested_tsf(
|
|
"second",
|
|
"parse",
|
|
"""
|
|
function parse(value: string): integer;
|
|
begin
|
|
/// 再次解析值。
|
|
/// @param: value 输入值
|
|
return 1;
|
|
end;
|
|
""",
|
|
)
|
|
result = self.run_cli(first, second)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
self.assertEqual(
|
|
["Parse(value)", "parse(value)"],
|
|
[item["signature"] for item in self.read_declarations()],
|
|
)
|
|
|
|
def test_duplicate_class_and_unit_names_are_preserved_in_input_order(self):
|
|
cases = (
|
|
(
|
|
self.write_nested_tsf(
|
|
"class-a", "Widget", "type Widget = class\nend;\n"
|
|
),
|
|
self.write_nested_tsf(
|
|
"class-b", "widget", "type widget = class\nend;\n"
|
|
),
|
|
"重复 class",
|
|
),
|
|
(
|
|
self.write_nested_tsf(
|
|
"unit-a",
|
|
"Runtime",
|
|
"unit Runtime;\ninterface\nimplementation\nend.\n",
|
|
),
|
|
self.write_nested_tsf(
|
|
"unit-b",
|
|
"runtime",
|
|
"unit runtime;\ninterface\nimplementation\nend.\n",
|
|
),
|
|
"重复 unit",
|
|
),
|
|
)
|
|
for first, second, kind in cases:
|
|
with self.subTest(kind=kind):
|
|
result = self.run_cli(first, second)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
self.assertEqual(
|
|
2,
|
|
len(self.read_declarations()),
|
|
)
|
|
|
|
def test_same_name_across_kinds_is_allowed(self):
|
|
function = self.write_nested_tsf(
|
|
"function",
|
|
"Widget",
|
|
"""
|
|
function Widget(): integer;
|
|
begin
|
|
/// 同名函数。
|
|
return 1;
|
|
end;
|
|
""",
|
|
)
|
|
cls = self.write_nested_tsf(
|
|
"class", "Widget", "type Widget = class\nend;\n"
|
|
)
|
|
|
|
result = self.run_cli(function, cls)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
self.assertEqual(
|
|
["function", "class"],
|
|
[item["kind"] for item in self.read_declarations()],
|
|
)
|
|
|
|
def test_multiple_classes_and_units_are_allowed(self):
|
|
inputs = (
|
|
self.write_tsf("FirstClass", "type FirstClass = class\nend;\n"),
|
|
self.write_tsf("SecondClass", "type SecondClass = class\nend;\n"),
|
|
self.write_tsf(
|
|
"FirstUnit",
|
|
"unit FirstUnit;\ninterface\nimplementation\nend.\n",
|
|
),
|
|
self.write_tsf(
|
|
"SecondUnit",
|
|
"unit SecondUnit;\ninterface\nimplementation\nend.\n",
|
|
),
|
|
)
|
|
|
|
result = self.run_cli(*inputs)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
self.assertEqual(
|
|
["FirstClass", "SecondClass", "FirstUnit", "SecondUnit"],
|
|
[item["name"] for item in self.read_declarations()],
|
|
)
|
|
|
|
def test_top_level_procedure_creates_function_draft(self):
|
|
source = self.write_tsf(
|
|
"ProcedureDemo",
|
|
"procedure ProcedureDemo(value: integer);\nbegin\nend;\n",
|
|
)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
self.assertEqual(
|
|
{
|
|
"kind": "function",
|
|
"name": "ProcedureDemo",
|
|
"signature": "ProcedureDemo(value)",
|
|
"desc": "",
|
|
"params": [
|
|
{"name": "value", "type": "integer", "desc": ""}
|
|
],
|
|
"returns": "",
|
|
},
|
|
self.read_declaration(),
|
|
)
|
|
|
|
def test_malformed_top_level_function_keeps_recognizable_signature(self):
|
|
cases = {
|
|
"MissingBody": (
|
|
"function MissingBody(value: integer): array;\n",
|
|
"MissingBody",
|
|
[{"name": "value", "type": "integer", "desc": ""}],
|
|
"array",
|
|
),
|
|
"BrokenParams": (
|
|
"function BrokenParams(required: integer; optional = 1\n",
|
|
"BrokenParams",
|
|
[
|
|
{"name": "required", "type": "integer", "desc": ""},
|
|
{
|
|
"name": "optional",
|
|
"type": "",
|
|
"optional": True,
|
|
"desc": "",
|
|
},
|
|
],
|
|
"",
|
|
),
|
|
"MissingName": (
|
|
"function\n",
|
|
"MissingName",
|
|
[],
|
|
"",
|
|
),
|
|
}
|
|
for filename, (text, name, params, returns) in cases.items():
|
|
with self.subTest(filename=filename):
|
|
source = self.write_tsf(filename, text)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
declaration = self.read_declaration()
|
|
self.assertEqual(name, declaration["name"])
|
|
parameter_names = ", ".join(
|
|
parameter["name"] for parameter in params
|
|
)
|
|
self.assertEqual(
|
|
f"{name}({parameter_names})",
|
|
declaration["signature"],
|
|
)
|
|
self.assertEqual(params, declaration["params"])
|
|
self.assertEqual(returns, declaration["returns"])
|
|
|
|
def test_unrecognized_tsf_header_still_creates_function_draft(self):
|
|
cases = {
|
|
"LooseCall": (
|
|
"LegacyName(first, second)\n",
|
|
"LegacyName",
|
|
["first", "second"],
|
|
),
|
|
"EmptySource": ("", "EmptySource", []),
|
|
}
|
|
for filename, (text, name, params) in cases.items():
|
|
with self.subTest(filename=filename):
|
|
source = self.write_tsf(filename, text)
|
|
|
|
result = self.run_cli(source)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
declaration = self.read_declaration()
|
|
self.assertEqual("function", declaration["kind"])
|
|
self.assertEqual(name, declaration["name"])
|
|
self.assertEqual(
|
|
params,
|
|
[parameter["name"] for parameter in declaration["params"]],
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|