🐛 fix(tsl-codegen): make TSF conversion best effort
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.
This commit is contained in:
@@ -237,7 +237,7 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
self.read_declaration()["params"],
|
||||
)
|
||||
|
||||
def test_parameter_type_mismatch_reports_param_line_and_preserves_output(self):
|
||||
def test_parameter_type_mismatch_creates_structural_draft(self):
|
||||
source = self.write_tsf(
|
||||
"ParamTypeMismatch",
|
||||
"""
|
||||
@@ -249,32 +249,24 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
end;
|
||||
""",
|
||||
)
|
||||
self.output.write_text("原内容\n", encoding="utf-8")
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:4:", result.stderr)
|
||||
self.assertIn("参数 value 的类型与函数声明不一致", result.stderr)
|
||||
self.assertIn("注释为 string,声明为 integer", result.stderr)
|
||||
self.assertEqual("原内容\n", self.output.read_text(encoding="utf-8"))
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
function = self.read_declaration()
|
||||
self.assertEqual("", function["desc"])
|
||||
self.assertEqual(
|
||||
[{"name": "value", "type": "integer", "desc": ""}],
|
||||
function["params"],
|
||||
)
|
||||
self.assertEqual("integer", function["returns"])
|
||||
|
||||
def test_parameter_type_braces_must_be_complete_and_nonempty(self):
|
||||
def test_malformed_parameter_docs_create_structural_draft(self):
|
||||
cases = {
|
||||
"EmptyParamType": (
|
||||
"/// @param: value {} 输入值",
|
||||
"@param: 参数类型不能为空",
|
||||
),
|
||||
"UnclosedParamType": (
|
||||
"/// @param: value {integer 输入值",
|
||||
"@param: 参数类型缺少右花括号",
|
||||
),
|
||||
"MissingParamDescription": (
|
||||
"/// @param: value {integer}",
|
||||
"@param: 参数说明不能为空",
|
||||
),
|
||||
"EmptyParamType": "/// @param: value {} 输入值",
|
||||
"UnclosedParamType": "/// @param: value {integer 输入值",
|
||||
"MissingParamDescription": "/// @param: value {integer}",
|
||||
}
|
||||
for name, (param_line, expected) in cases.items():
|
||||
for name, param_line in cases.items():
|
||||
with self.subTest(name=name):
|
||||
source = self.write_tsf(
|
||||
name,
|
||||
@@ -290,10 +282,14 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:4:", result.stderr)
|
||||
self.assertIn(expected, result.stderr)
|
||||
self.assertFalse(self.output.exists())
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
function = self.read_declaration()
|
||||
self.assertEqual("", function["desc"])
|
||||
self.assertEqual(
|
||||
[{"name": "value", "type": "", "desc": ""}],
|
||||
function["params"],
|
||||
)
|
||||
self.assertEqual("integer", function["returns"])
|
||||
|
||||
def test_multiple_tsf_files_are_merged_in_input_order(self):
|
||||
first = self.write_tsf(
|
||||
@@ -514,6 +510,45 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual("", function["returns"])
|
||||
|
||||
def test_legacy_document_block_creates_structural_draft(self):
|
||||
source = self.write_tsf(
|
||||
"LegacyDraft",
|
||||
"""
|
||||
function LegacyDraft(required: integer; optional = 1): array;
|
||||
begin
|
||||
{**
|
||||
@param(required)(integer) 必填值
|
||||
@param(optional)(integer) 可选值
|
||||
@return(array) 结果
|
||||
**}
|
||||
return array();
|
||||
end;
|
||||
""",
|
||||
)
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual(
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "LegacyDraft",
|
||||
"signature": "LegacyDraft(required, optional)",
|
||||
"desc": "",
|
||||
"params": [
|
||||
{"name": "required", "type": "integer", "desc": ""},
|
||||
{
|
||||
"name": "optional",
|
||||
"type": "",
|
||||
"optional": True,
|
||||
"desc": "",
|
||||
},
|
||||
],
|
||||
"returns": "array",
|
||||
},
|
||||
self.read_declaration(),
|
||||
)
|
||||
|
||||
def test_json_draft_completes_fixed_fields_for_every_declaration_kind(self):
|
||||
function_source = self.write_tsf(
|
||||
"DraftFunction",
|
||||
@@ -649,7 +684,7 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
self.assertNotIn("positional arguments:", result.stdout)
|
||||
self.assertNotIn("options:", result.stdout)
|
||||
|
||||
def test_return_type_mismatch_reports_directive_line_and_preserves_output(self):
|
||||
def test_return_type_mismatch_creates_structural_draft(self):
|
||||
source = self.write_tsf(
|
||||
"Mismatch",
|
||||
"""
|
||||
@@ -661,17 +696,14 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
end;
|
||||
""",
|
||||
)
|
||||
self.output.write_text("原内容\n", encoding="utf-8")
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:4:", result.stderr)
|
||||
self.assertIn("返回类型与函数声明不一致", result.stderr)
|
||||
self.assertIn("注释为 string,声明为 integer", result.stderr)
|
||||
self.assertEqual("原内容\n", self.output.read_text(encoding="utf-8"))
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
function = self.read_declaration()
|
||||
self.assertEqual("", function["desc"])
|
||||
self.assertEqual("integer", function["returns"])
|
||||
|
||||
def test_document_block_must_be_first_content_after_begin(self):
|
||||
def test_late_document_block_creates_structural_draft(self):
|
||||
source = self.write_tsf(
|
||||
"LateDoc",
|
||||
"""
|
||||
@@ -686,12 +718,30 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:3:", result.stderr)
|
||||
self.assertIn("begin 后第一段内容必须是 /// 文档块", result.stderr)
|
||||
self.assertFalse(self.output.exists())
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
function = self.read_declaration()
|
||||
self.assertEqual("", function["desc"])
|
||||
self.assertEqual("integer", function["returns"])
|
||||
|
||||
def test_duplicate_enum_value_is_rejected(self):
|
||||
def test_missing_body_does_not_borrow_later_function_document(self):
|
||||
source = self.write_tsf(
|
||||
"MissingOwnBody",
|
||||
"function MissingOwnBody(): integer;\n"
|
||||
"function Helper(): integer;\n"
|
||||
"begin\n"
|
||||
" /// 辅助函数文档。\n"
|
||||
" return 1;\n"
|
||||
"end;\n",
|
||||
)
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
function = self.read_declaration()
|
||||
self.assertEqual("MissingOwnBody", function["name"])
|
||||
self.assertEqual("", function["desc"])
|
||||
|
||||
def test_duplicate_enum_value_creates_structural_draft(self):
|
||||
source = self.write_tsf(
|
||||
"DuplicateValue",
|
||||
"""
|
||||
@@ -709,11 +759,16 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:7:", result.stderr)
|
||||
self.assertIn("枚举值重复", result.stderr)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
function = self.read_declaration()
|
||||
self.assertEqual("", function["desc"])
|
||||
self.assertEqual(
|
||||
[{"name": "mode", "type": "integer", "desc": ""}],
|
||||
function["params"],
|
||||
)
|
||||
self.assertEqual("integer", function["returns"])
|
||||
|
||||
def test_directives_after_examples_are_rejected(self):
|
||||
def test_directives_after_examples_create_structural_draft(self):
|
||||
source = self.write_tsf(
|
||||
"BadOrder",
|
||||
"""
|
||||
@@ -730,9 +785,14 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:6:", result.stderr)
|
||||
self.assertIn("示例组之后不能再写函数级指令", result.stderr)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
function = self.read_declaration()
|
||||
self.assertEqual("", function["desc"])
|
||||
self.assertEqual(
|
||||
[{"name": "value", "type": "integer", "desc": ""}],
|
||||
function["params"],
|
||||
)
|
||||
self.assertEqual("integer", function["returns"])
|
||||
|
||||
def test_class_converts_only_first_matching_public_class(self):
|
||||
source = self.write_tsf(
|
||||
@@ -982,7 +1042,7 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
members = self.read_declaration()["members"]
|
||||
self.assertEqual(["Value"], [member["name"] for member in members])
|
||||
|
||||
def test_class_rejects_unsupported_public_declaration_at_exact_line(self):
|
||||
def test_class_skips_unsupported_public_declaration(self):
|
||||
source = self.write_tsf(
|
||||
"UnsupportedClass",
|
||||
"""
|
||||
@@ -992,14 +1052,10 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
end;
|
||||
""",
|
||||
)
|
||||
self.output.write_text("原内容\n", encoding="utf-8")
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:3:", result.stderr)
|
||||
self.assertIn("class 不支持成员声明:type", result.stderr)
|
||||
self.assertEqual("原内容\n", self.output.read_text(encoding="utf-8"))
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual([], self.read_declaration()["members"])
|
||||
|
||||
def test_missing_class_and_unit_member_types_remain_explicit_drafts(self):
|
||||
cls_source = self.write_tsf(
|
||||
@@ -1126,7 +1182,33 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
self.assertNotIn("HiddenValue", serialized)
|
||||
self.assertNotIn("生命周期文档", serialized)
|
||||
|
||||
def test_unit_filename_must_match_name_at_declaration_line(self):
|
||||
def test_incomplete_interface_class_does_not_scan_implementation(self):
|
||||
source = self.write_tsf(
|
||||
"IncompleteInterfaceClass",
|
||||
"unit IncompleteInterfaceClass;\n"
|
||||
"interface\n"
|
||||
"type PublicType = class\n"
|
||||
"public\n"
|
||||
"function Keep(): integer;\n"
|
||||
"implementation\n"
|
||||
"function Hidden(): integer;\n"
|
||||
"begin\n"
|
||||
" return 1;\n"
|
||||
"end;\n"
|
||||
"end.\n",
|
||||
)
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
unit = self.read_declaration()
|
||||
self.assertEqual(["PublicType"], [member["name"] for member in unit["members"]])
|
||||
self.assertEqual(
|
||||
["Keep"],
|
||||
[member["name"] for member in unit["members"][0]["members"]],
|
||||
)
|
||||
|
||||
def test_unit_filename_mismatch_keeps_declared_name(self):
|
||||
source = self.write_tsf(
|
||||
"Wrong",
|
||||
"unit Right;\ninterface\nimplementation\nend.\n",
|
||||
@@ -1134,9 +1216,8 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:1:", result.stderr)
|
||||
self.assertIn("unit 名称必须与文件名一致", result.stderr)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual("Right", self.read_declaration()["name"])
|
||||
|
||||
def test_unit_preserves_multiple_interface_classes_in_source_order(self):
|
||||
source = self.write_tsf(
|
||||
@@ -1162,7 +1243,7 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
members = self.read_declaration()["members"]
|
||||
self.assertEqual(["First", "Second"], [member["name"] for member in members])
|
||||
|
||||
def test_unit_unbound_document_reports_exact_line(self):
|
||||
def test_unit_unbound_document_is_ignored(self):
|
||||
source = self.write_tsf(
|
||||
"UnboundUnit",
|
||||
"""
|
||||
@@ -1178,11 +1259,12 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:3:", result.stderr)
|
||||
self.assertIn("unit interface 文档块无法绑定", result.stderr)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
current = self.read_declaration()["members"][0]
|
||||
self.assertEqual("Current", current["name"])
|
||||
self.assertEqual("", current["desc"])
|
||||
|
||||
def test_unit_unbound_header_document_reports_exact_line(self):
|
||||
def test_unit_unbound_header_document_is_ignored(self):
|
||||
source = self.write_tsf(
|
||||
"UnboundHeaderUnit",
|
||||
"""
|
||||
@@ -1197,11 +1279,10 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:2:", result.stderr)
|
||||
self.assertIn("unit interface 文档块无法绑定", result.stderr)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual("", self.read_declaration()["desc"])
|
||||
|
||||
def test_unit_end_dot_must_be_the_terminal_tokens(self):
|
||||
def test_unit_ignores_tokens_after_terminal_end_dot(self):
|
||||
source = self.write_tsf(
|
||||
"TrailingUnit",
|
||||
"""
|
||||
@@ -1215,9 +1296,8 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:5:", result.stderr)
|
||||
self.assertIn("end. 必须结束整个文件", result.stderr)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual("TrailingUnit", self.read_declaration()["name"])
|
||||
|
||||
def test_class_missing_docs_default_visibility_and_private_filter_create_draft(self):
|
||||
source = self.write_tsf(
|
||||
@@ -1269,35 +1349,37 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
members = self.read_declaration()["members"]
|
||||
self.assertEqual(["value_"], [member["name"] for member in members])
|
||||
|
||||
def test_class_structure_errors_preserve_existing_output(self):
|
||||
def test_class_structure_errors_create_best_effort_drafts(self):
|
||||
cases = {
|
||||
"WrongName": (
|
||||
"type Actual = class\nend;\n",
|
||||
1,
|
||||
"对外 class 名称必须与文件名一致",
|
||||
"Actual",
|
||||
[],
|
||||
),
|
||||
"StaticMethod": (
|
||||
"type StaticMethod = class\npublic\nstatic function Bad();\nend;\n",
|
||||
3,
|
||||
"不存在 static function",
|
||||
"StaticMethod",
|
||||
["Bad"],
|
||||
),
|
||||
"ManyFields": (
|
||||
"type ManyFields = class\npublic\nleft_, right_: integer;\nend;\n",
|
||||
3,
|
||||
"一项一条声明",
|
||||
"ManyFields",
|
||||
["left_", "right_"],
|
||||
),
|
||||
}
|
||||
for name, (text, line, expected) in cases.items():
|
||||
for name, (text, expected_name, expected_members) in cases.items():
|
||||
with self.subTest(name=name):
|
||||
source = self.write_tsf(name, text)
|
||||
self.output.write_text("原内容\n", encoding="utf-8")
|
||||
result = self.run_cli(source)
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:{line}:", result.stderr)
|
||||
self.assertIn(expected, result.stderr)
|
||||
self.assertEqual("原内容\n", self.output.read_text(encoding="utf-8"))
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
declaration = self.read_declaration()
|
||||
self.assertEqual(expected_name, declaration["name"])
|
||||
self.assertEqual(
|
||||
expected_members,
|
||||
[member["name"] for member in declaration["members"]],
|
||||
)
|
||||
|
||||
def test_class_and_unit_constants_reject_multiple_names_at_declaration_line(self):
|
||||
def test_class_and_unit_constants_split_multiple_names_into_drafts(self):
|
||||
cases = {
|
||||
"ManyConstants": (
|
||||
"type ManyConstants = class\npublic\nconst Left, Right = 1;\nend;\n",
|
||||
@@ -1312,19 +1394,19 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
3,
|
||||
),
|
||||
}
|
||||
for name, (text, line) in cases.items():
|
||||
for name, (text, _line) in cases.items():
|
||||
with self.subTest(name=name):
|
||||
source = self.write_tsf(name, text)
|
||||
self.output.write_text("原内容\n", encoding="utf-8")
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:{line}:", result.stderr)
|
||||
self.assertIn("对外常量必须一项一条声明", result.stderr)
|
||||
self.assertEqual("原内容\n", self.output.read_text(encoding="utf-8"))
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual(
|
||||
["Left", "Right"],
|
||||
[member["name"] for member in self.read_declaration()["members"]],
|
||||
)
|
||||
|
||||
def test_class_rejects_procedure_at_declaration_line(self):
|
||||
def test_class_converts_procedure_to_method_draft(self):
|
||||
source = self.write_tsf(
|
||||
"ProcedureClass",
|
||||
"""
|
||||
@@ -1334,16 +1416,15 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
end;
|
||||
""",
|
||||
)
|
||||
self.output.write_text("原内容\n", encoding="utf-8")
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:3:", result.stderr)
|
||||
self.assertIn("class 暂不支持 procedure", result.stderr)
|
||||
self.assertEqual("原内容\n", self.output.read_text(encoding="utf-8"))
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
method = self.read_declaration()["members"][0]
|
||||
self.assertEqual("Open", method["name"])
|
||||
self.assertEqual("method", method["kind"])
|
||||
self.assertEqual("", method["returns"])
|
||||
|
||||
def test_class_unbound_document_reports_its_line(self):
|
||||
def test_class_unbound_document_is_ignored(self):
|
||||
source = self.write_tsf(
|
||||
"UnboundDoc",
|
||||
"""
|
||||
@@ -1359,35 +1440,323 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:4:", result.stderr)
|
||||
self.assertIn("文档块无法绑定", result.stderr)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
field = self.read_declaration()["members"][0]
|
||||
self.assertEqual("value_", field["name"])
|
||||
self.assertEqual("", field["desc"])
|
||||
|
||||
def test_unit_rejects_shorthand_procedure_and_non_class_type(self):
|
||||
def test_unit_converts_shorthand_and_skips_unsupported_types(self):
|
||||
cases = {
|
||||
"ShortUnit": (
|
||||
"unit ShortUnit;\nfunction Open(): integer;\nbegin return 1; end;\nend.\n",
|
||||
1,
|
||||
"显式 interface",
|
||||
["Open"],
|
||||
),
|
||||
"ProcedureUnit": (
|
||||
"unit ProcedureUnit;\ninterface\nprocedure Open();\nimplementation\nend.\n",
|
||||
3,
|
||||
"暂不支持 procedure",
|
||||
["Open"],
|
||||
),
|
||||
"AliasUnit": (
|
||||
"unit AliasUnit;\ninterface\ntype Size = integer;\nimplementation\nend.\n",
|
||||
3,
|
||||
"只支持 class type",
|
||||
[],
|
||||
),
|
||||
}
|
||||
for name, (text, line, expected) in cases.items():
|
||||
for name, (text, expected_members) in cases.items():
|
||||
with self.subTest(name=name):
|
||||
source = self.write_tsf(name, text)
|
||||
result = self.run_cli(source)
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(f"{source}:{line}:", result.stderr)
|
||||
self.assertIn(expected, result.stderr)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual(
|
||||
expected_members,
|
||||
[member["name"] for member in self.read_declaration()["members"]],
|
||||
)
|
||||
|
||||
def test_malformed_class_and_unit_documents_create_structural_drafts(self):
|
||||
cls = self.write_tsf(
|
||||
"BadClassDocs",
|
||||
"""
|
||||
type BadClassDocs = class
|
||||
public
|
||||
/// 打开值
|
||||
/// @param: value {string} 输入值
|
||||
function Open(value: integer): integer;
|
||||
end;
|
||||
""",
|
||||
)
|
||||
unit = self.write_tsf(
|
||||
"BadUnitDocs",
|
||||
"""
|
||||
unit BadUnitDocs;
|
||||
interface
|
||||
/// 打开值
|
||||
/// @returns: string
|
||||
function Open(): integer;
|
||||
implementation
|
||||
end.
|
||||
""",
|
||||
)
|
||||
|
||||
result = self.run_cli(cls, unit)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
class_method = self.read_declarations()[0]["members"][0]
|
||||
unit_function = self.read_declarations()[1]["members"][0]
|
||||
self.assertEqual("", class_method["desc"])
|
||||
self.assertEqual("integer", class_method["params"][0]["type"])
|
||||
self.assertEqual("", unit_function["desc"])
|
||||
self.assertEqual("integer", unit_function["returns"])
|
||||
|
||||
def test_incomplete_class_and_unit_keep_recognizable_members(self):
|
||||
cases = {
|
||||
"IncompleteClass": (
|
||||
"type IncompleteClass = class\n"
|
||||
"public\n"
|
||||
"function Keep(value: integer): integer;\n",
|
||||
"Keep",
|
||||
),
|
||||
"IncompleteUnit": (
|
||||
"unit IncompleteUnit;\n"
|
||||
"function Keep(value: integer): integer;\n",
|
||||
"Keep",
|
||||
),
|
||||
}
|
||||
for name, (text, expected_member) in cases.items():
|
||||
with self.subTest(name=name):
|
||||
source = self.write_tsf(name, text)
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual(
|
||||
[expected_member],
|
||||
[member["name"] for member in self.read_declaration()["members"]],
|
||||
)
|
||||
|
||||
def test_broken_member_does_not_hide_following_function(self):
|
||||
cases = {
|
||||
"RecoverClass": (
|
||||
"type RecoverClass = class\n"
|
||||
"public\n"
|
||||
"property Broken\n"
|
||||
"function Keep(): integer;\n"
|
||||
"end;\n",
|
||||
"class",
|
||||
),
|
||||
"RecoverUnit": (
|
||||
"unit RecoverUnit;\n"
|
||||
"interface\n"
|
||||
"const Broken\n"
|
||||
"function Keep(): integer;\n"
|
||||
"implementation\n"
|
||||
"end.\n",
|
||||
"unit",
|
||||
),
|
||||
}
|
||||
for name, (text, expected_kind) in cases.items():
|
||||
with self.subTest(name=name):
|
||||
source = self.write_tsf(name, text)
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
declaration = self.read_declaration()
|
||||
self.assertEqual(expected_kind, declaration["kind"])
|
||||
self.assertIn(
|
||||
"Keep",
|
||||
[member["name"] for member in declaration["members"]],
|
||||
)
|
||||
|
||||
def test_unnamed_class_and_unit_functions_are_skipped(self):
|
||||
cases = {
|
||||
"UnnamedClassFunction": (
|
||||
"type UnnamedClassFunction = class\n"
|
||||
"public\n"
|
||||
"function\n"
|
||||
"function Keep(): integer;\n"
|
||||
"end;\n",
|
||||
"class",
|
||||
),
|
||||
"UnnamedUnitFunction": (
|
||||
"unit UnnamedUnitFunction;\n"
|
||||
"interface\n"
|
||||
"function\n"
|
||||
"function Keep(): integer;\n"
|
||||
"implementation\n"
|
||||
"end.\n",
|
||||
"unit",
|
||||
),
|
||||
}
|
||||
for name, (text, expected_kind) in cases.items():
|
||||
with self.subTest(name=name):
|
||||
source = self.write_tsf(name, text)
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
declaration = self.read_declaration()
|
||||
self.assertEqual(expected_kind, declaration["kind"])
|
||||
self.assertEqual(
|
||||
["Keep"],
|
||||
[member["name"] for member in declaration["members"]],
|
||||
)
|
||||
|
||||
def test_unnamed_members_do_not_hide_following_functions(self):
|
||||
cases = {
|
||||
"UnnamedProperty": (
|
||||
"type UnnamedProperty = class\n"
|
||||
"public\n"
|
||||
"property\n"
|
||||
"function Keep(): integer;\n"
|
||||
"end;\n",
|
||||
"class",
|
||||
),
|
||||
"UnnamedConstant": (
|
||||
"unit UnnamedConstant;\n"
|
||||
"interface\n"
|
||||
"const\n"
|
||||
"function Keep(): integer;\n"
|
||||
"implementation\n"
|
||||
"end.\n",
|
||||
"unit",
|
||||
),
|
||||
}
|
||||
for name, (text, expected_kind) in cases.items():
|
||||
with self.subTest(name=name):
|
||||
source = self.write_tsf(name, text)
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
declaration = self.read_declaration()
|
||||
self.assertEqual(expected_kind, declaration["kind"])
|
||||
self.assertEqual(
|
||||
["Keep"],
|
||||
[member["name"] for member in declaration["members"]],
|
||||
)
|
||||
|
||||
def test_method_missing_signature_semicolon_does_not_hide_next_member(self):
|
||||
source = self.write_tsf(
|
||||
"BrokenInlineMethod",
|
||||
"type BrokenInlineMethod = class\n"
|
||||
"public\n"
|
||||
"function Broken()\n"
|
||||
"begin\n"
|
||||
" return 1;\n"
|
||||
"end;\n"
|
||||
"function Keep(): integer;\n"
|
||||
"end;\n",
|
||||
)
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual(
|
||||
["Broken", "Keep"],
|
||||
[member["name"] for member in self.read_declaration()["members"]],
|
||||
)
|
||||
|
||||
def test_broken_headers_do_not_hide_first_function(self):
|
||||
cases = {
|
||||
"BrokenBase": (
|
||||
"type BrokenBase = class(Base\n"
|
||||
"function Keep(): integer;\n",
|
||||
"class",
|
||||
),
|
||||
"MissingClassName": (
|
||||
"type\n"
|
||||
"class\n"
|
||||
"function Keep(): integer;\n",
|
||||
"class",
|
||||
),
|
||||
"BrokenUnitHeader": (
|
||||
"unit BrokenUnitHeader\n"
|
||||
"function Keep(): integer;\n",
|
||||
"unit",
|
||||
),
|
||||
"MissingUnitName": (
|
||||
"unit\n"
|
||||
"function Keep(): integer;\n",
|
||||
"unit",
|
||||
),
|
||||
}
|
||||
for name, (text, expected_kind) in cases.items():
|
||||
with self.subTest(name=name):
|
||||
source = self.write_tsf(name, text)
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
declaration = self.read_declaration()
|
||||
self.assertEqual(expected_kind, declaration["kind"])
|
||||
self.assertEqual(
|
||||
["Keep"],
|
||||
[member["name"] for member in declaration["members"]],
|
||||
)
|
||||
|
||||
def test_declaration_truncation_after_name_always_creates_a_draft(self):
|
||||
module = load_script()
|
||||
samples = {
|
||||
"function": (
|
||||
"TruncateFunction.tsf",
|
||||
"function TruncateFunction(required: integer; optional = 1): array;\n"
|
||||
"begin\n"
|
||||
" return array();\n"
|
||||
"end;\n",
|
||||
),
|
||||
"class": (
|
||||
"TruncateClass.tsf",
|
||||
"type TruncateClass = class(Base)\n"
|
||||
"public\n"
|
||||
"function Open(value: integer): string;\n"
|
||||
"property Value: integer read value_ write value_;\n"
|
||||
"const Limit = 2;\n"
|
||||
"value_: integer;\n"
|
||||
"end;\n",
|
||||
),
|
||||
"unit": (
|
||||
"TruncateUnit.tsf",
|
||||
"unit TruncateUnit;\n"
|
||||
"interface\n"
|
||||
"function Open(value: integer): string;\n"
|
||||
"const Limit = 2;\n"
|
||||
"var Current: integer;\n"
|
||||
"implementation\n"
|
||||
"end.\n",
|
||||
),
|
||||
}
|
||||
for expected_kind, (filename, source) in samples.items():
|
||||
name_end = module.tokenize(source)[1].end
|
||||
for cut in range(name_end, len(source) + 1):
|
||||
with self.subTest(kind=expected_kind, cut=cut):
|
||||
declaration = module.complete_draft_fields(
|
||||
module.convert_source(source[:cut], Path(filename))
|
||||
)
|
||||
self.assertEqual(expected_kind, declaration["kind"])
|
||||
self.assertTrue(declaration["name"])
|
||||
|
||||
def test_unit_fallback_keeps_bare_const_and_var_sections(self):
|
||||
source = self.write_tsf(
|
||||
"RecoverSections",
|
||||
"""
|
||||
unit RecoverSections;
|
||||
interface
|
||||
/// 无法绑定
|
||||
// 切断文档绑定
|
||||
const First = 1;
|
||||
Second = 2;
|
||||
var Current: integer;
|
||||
Previous: integer;
|
||||
implementation
|
||||
end.
|
||||
""",
|
||||
)
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual(
|
||||
["First", "Second", "Current", "Previous"],
|
||||
[member["name"] for member in self.read_declaration()["members"]],
|
||||
)
|
||||
|
||||
def test_unit_var_and_const_sections_keep_each_declaration(self):
|
||||
source = self.write_tsf(
|
||||
@@ -1458,7 +1827,7 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
[item["signature"] for item in self.read_declarations()],
|
||||
)
|
||||
|
||||
def test_duplicate_function_signature_preserves_existing_output(self):
|
||||
def test_duplicate_function_signature_is_preserved_in_input_order(self):
|
||||
first = self.write_nested_tsf(
|
||||
"first",
|
||||
"Parse",
|
||||
@@ -1483,15 +1852,15 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
end;
|
||||
""",
|
||||
)
|
||||
self.output.write_text("原内容\n", encoding="utf-8")
|
||||
|
||||
result = self.run_cli(first, second)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("重复 function signature", result.stderr)
|
||||
self.assertEqual("原内容\n", self.output.read_text(encoding="utf-8"))
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual(
|
||||
["Parse(value)", "parse(value)"],
|
||||
[item["signature"] for item in self.read_declarations()],
|
||||
)
|
||||
|
||||
def test_duplicate_class_and_unit_names_are_rejected_case_insensitively(self):
|
||||
def test_duplicate_class_and_unit_names_are_preserved_in_input_order(self):
|
||||
cases = (
|
||||
(
|
||||
self.write_nested_tsf(
|
||||
@@ -1516,16 +1885,14 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
"重复 unit",
|
||||
),
|
||||
)
|
||||
for first, second, expected in cases:
|
||||
with self.subTest(expected=expected):
|
||||
self.output.write_text("原内容\n", encoding="utf-8")
|
||||
|
||||
for first, second, kind in cases:
|
||||
with self.subTest(kind=kind):
|
||||
result = self.run_cli(first, second)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn(expected, result.stderr)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual(
|
||||
"原内容\n", self.output.read_text(encoding="utf-8")
|
||||
2,
|
||||
len(self.read_declarations()),
|
||||
)
|
||||
|
||||
def test_same_name_across_kinds_is_allowed(self):
|
||||
@@ -1574,16 +1941,100 @@ class ConvertTsfCliTest(unittest.TestCase):
|
||||
[item["name"] for item in self.read_declarations()],
|
||||
)
|
||||
|
||||
def test_top_level_procedure_remains_unsupported(self):
|
||||
def test_top_level_procedure_creates_function_draft(self):
|
||||
source = self.write_tsf(
|
||||
"ProcedureDemo",
|
||||
"procedure ProcedureDemo();\nbegin\nend;\n",
|
||||
"procedure ProcedureDemo(value: integer);\nbegin\nend;\n",
|
||||
)
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertIn("目前支持独立顶层 function、class 和完整 unit", result.stderr)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual(
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "ProcedureDemo",
|
||||
"signature": "ProcedureDemo(value)",
|
||||
"desc": "",
|
||||
"params": [
|
||||
{"name": "value", "type": "integer", "desc": ""}
|
||||
],
|
||||
"returns": "",
|
||||
},
|
||||
self.read_declaration(),
|
||||
)
|
||||
|
||||
def test_malformed_top_level_function_keeps_recognizable_signature(self):
|
||||
cases = {
|
||||
"MissingBody": (
|
||||
"function MissingBody(value: integer): array;\n",
|
||||
"MissingBody",
|
||||
[{"name": "value", "type": "integer", "desc": ""}],
|
||||
"array",
|
||||
),
|
||||
"BrokenParams": (
|
||||
"function BrokenParams(required: integer; optional = 1\n",
|
||||
"BrokenParams",
|
||||
[
|
||||
{"name": "required", "type": "integer", "desc": ""},
|
||||
{
|
||||
"name": "optional",
|
||||
"type": "",
|
||||
"optional": True,
|
||||
"desc": "",
|
||||
},
|
||||
],
|
||||
"",
|
||||
),
|
||||
"MissingName": (
|
||||
"function\n",
|
||||
"MissingName",
|
||||
[],
|
||||
"",
|
||||
),
|
||||
}
|
||||
for filename, (text, name, params, returns) in cases.items():
|
||||
with self.subTest(filename=filename):
|
||||
source = self.write_tsf(filename, text)
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
declaration = self.read_declaration()
|
||||
self.assertEqual(name, declaration["name"])
|
||||
parameter_names = ", ".join(
|
||||
parameter["name"] for parameter in params
|
||||
)
|
||||
self.assertEqual(
|
||||
f"{name}({parameter_names})",
|
||||
declaration["signature"],
|
||||
)
|
||||
self.assertEqual(params, declaration["params"])
|
||||
self.assertEqual(returns, declaration["returns"])
|
||||
|
||||
def test_unrecognized_tsf_header_still_creates_function_draft(self):
|
||||
cases = {
|
||||
"LooseCall": (
|
||||
"LegacyName(first, second)\n",
|
||||
"LegacyName",
|
||||
["first", "second"],
|
||||
),
|
||||
"EmptySource": ("", "EmptySource", []),
|
||||
}
|
||||
for filename, (text, name, params) in cases.items():
|
||||
with self.subTest(filename=filename):
|
||||
source = self.write_tsf(filename, text)
|
||||
|
||||
result = self.run_cli(source)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
declaration = self.read_declaration()
|
||||
self.assertEqual("function", declaration["kind"])
|
||||
self.assertEqual(name, declaration["name"])
|
||||
self.assertEqual(
|
||||
params,
|
||||
[parameter["name"] for parameter in declaration["params"]],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user