Files
csh 9edc8fc868 feat(tsl-codegen): support unified API declarations
Add TSF conversion and shared Markdown recognition for mixed
function, class, and unit declarations.

Extend generation, linting, indexing, examples, and tests around
the unified declaration model.
2026-07-29 15:45:50 +08:00

360 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Shared recognition of TSL codegen API headings and declaration lines."""
from bisect import bisect_right
from dataclasses import dataclass
import re
TOP_LEVEL_RE = re.compile(r"^##(?!#)\s+`(.+?)`\s*$")
PLAIN_H2_RE = re.compile(r"^##(?!#)\s+")
BARE_API_RE = re.compile(r"^(#{3,5})(?!#)\s+`(.+?)`\s*$")
LEGACY_TYPED_RE = re.compile(
r"^(#{3,5})(?!#)\s+"
r"(class function|static function|static field|static const|"
r"function|property|field|const|var|class)\s+`(.+?)`\s*$"
)
DECLARATION_LINE_RE = re.compile(r"^声明:(.*?)\s*$")
DECLARATION_PREFIX_RE = re.compile(r"^声明[:]", re.IGNORECASE)
LOOSE_DECLARATION_RE = re.compile(r"^声明[:]\s*(.*?)\s*$", re.IGNORECASE)
TOP_LEVEL_TYPES = {"function", "class", "unit"}
CLASS_DECLARATIONS = {
"function": ("method", "instance"),
"class function": ("method", "class"),
"property": ("property", "instance"),
"field": ("field", "instance"),
"static field": ("field", "static"),
"const": ("constant", "instance"),
"static const": ("constant", "static"),
}
UNIT_DECLARATIONS = {
"function": ("function", "unit"),
"var": ("variable", "unit"),
"const": ("constant", "unit"),
"class": ("class", "unit"),
}
HEADING_RE = re.compile(r"^(#{1,6})(?!#)\s+")
FENCE_RE = re.compile(r"^\s*(```|~~~)")
@dataclass(frozen=True)
class ApiHeading:
level: int
kind: str
binding: str
name: str
signature: str
visible_title: str
root_kind: str
valid: bool = True
error: str = ""
@dataclass(frozen=True)
class ApiEntry:
heading: ApiHeading
start: int
end: int
def simple_name(signature):
return signature.split("(", 1)[0].strip()
def fence_flags(lines):
flags = []
in_fence = False
marker = ""
for line in lines:
stripped = line.lstrip()
if not in_fence and FENCE_RE.match(line):
marker = stripped[:3]
flags.append(True)
in_fence = True
continue
flags.append(in_fence)
if in_fence and stripped.startswith(marker):
in_fence = False
marker = ""
return flags
def heading_body_end(lines, start, flags=None):
if flags is None:
flags = fence_flags(lines)
for index in range(start + 1, len(lines)):
if flags[index]:
continue
if HEADING_RE.match(lines[index]):
return index
return len(lines)
def top_level_header_end(lines, start, flags=None):
return heading_body_end(lines, start, flags)
def _declaration(lines, start, end, flags):
content = []
declaration_lines = []
for index in range(start + 1, end):
if flags[index]:
continue
stripped = lines[index].strip()
if not stripped:
continue
content.append((index, stripped))
if DECLARATION_PREFIX_RE.match(stripped):
declaration_lines.append((index, stripped))
if not content:
return "", "API 标题后的第一条非空正文必须是 `声明:...`"
first_index, first = content[0]
match = DECLARATION_LINE_RE.fullmatch(first)
if not match:
loose = LOOSE_DECLARATION_RE.fullmatch(first)
if loose and loose.group(1).strip():
expected = f"声明:{loose.group(1).strip().casefold()}"
return "", f"声明行必须精确写为 `{expected}`"
return (
"",
"API 标题后的第一条非空正文必须是规范的 `声明:...` 行",
)
value = match.group(1).strip().casefold()
expected = f"声明:{value}"
if first != expected:
return "", f"声明行必须精确写为 `{expected}`"
if len(declaration_lines) != 1 or declaration_lines[0][0] != first_index:
return "", "每个 API 必须且只能包含一行声明"
return value, ""
def top_level_type(lines, start, end):
return _declaration(lines, start, end, fence_flags(lines))
def invalid_heading(level, name, signature, visible_title, root_kind, message):
return ApiHeading(
level,
"invalid",
"",
name,
signature,
visible_title,
root_kind,
False,
message,
)
def _valid_heading(level, kind, binding, signature, root_kind):
return ApiHeading(
level,
kind,
binding,
simple_name(signature),
signature,
signature,
root_kind,
)
def top_level_heading(signature, declaration, error):
if error:
return invalid_heading(
2,
simple_name(signature),
signature,
signature,
"",
error,
)
if declaration not in TOP_LEVEL_TYPES:
return invalid_heading(
2,
simple_name(signature),
signature,
signature,
"",
f"顶级 API 声明必须是 function、class 或 unit,实为 {declaration}",
)
return _valid_heading(2, declaration, "", signature, declaration)
def member_heading(
level,
declaration,
signature,
root_kind,
unit_class_open,
error,
):
name = simple_name(signature)
if error:
return invalid_heading(
level, name, signature, signature, root_kind, error
)
if declaration == "static function":
return invalid_heading(
level,
name,
signature,
signature,
root_kind,
"不存在 static function,请使用 class function",
)
if root_kind == "class":
if level != 3 or declaration not in CLASS_DECLARATIONS:
return invalid_heading(
level,
name,
signature,
signature,
root_kind,
"class API 成员必须使用约定的 H3 声明",
)
kind, binding = CLASS_DECLARATIONS[declaration]
return _valid_heading(level, kind, binding, signature, root_kind)
if root_kind == "unit":
if level == 3:
if declaration not in UNIT_DECLARATIONS:
return invalid_heading(
level,
name,
signature,
signature,
root_kind,
"unit interface 成员必须使用约定的 H3 声明",
)
kind, binding = UNIT_DECLARATIONS[declaration]
return _valid_heading(level, kind, binding, signature, root_kind)
if (
level == 4
and unit_class_open
and declaration in CLASS_DECLARATIONS
):
kind, binding = CLASS_DECLARATIONS[declaration]
return _valid_heading(level, kind, binding, signature, root_kind)
return invalid_heading(
level,
name,
signature,
signature,
root_kind,
"unit class 成员必须位于所属 class 的 H4",
)
return invalid_heading(
level,
name,
signature,
signature,
root_kind,
"function 顶级 API 不能包含 API 成员",
)
def collect_headings_and_boundaries(lines):
flags = fence_flags(lines)
headings = []
h2_boundaries = []
root_kind = ""
unit_class_open = False
for index, line in enumerate(lines):
if flags[index]:
continue
if PLAIN_H2_RE.match(line):
h2_boundaries.append(index)
root_kind = ""
unit_class_open = False
top_level = TOP_LEVEL_RE.match(line)
if not top_level:
continue
signature = top_level.group(1)
end = heading_body_end(lines, index, flags)
declaration, error = _declaration(
lines, index, end, flags
)
heading = top_level_heading(signature, declaration, error)
if heading.valid:
root_kind = heading.kind
headings.append((index, heading))
continue
if not root_kind:
continue
heading_match = HEADING_RE.match(line)
if root_kind == "unit" and heading_match:
if len(heading_match.group(1)) == 3:
unit_class_open = False
legacy = LEGACY_TYPED_RE.match(line)
if legacy:
level = len(legacy.group(1))
signature = legacy.group(3)
headings.append(
(
index,
invalid_heading(
level,
simple_name(signature),
signature,
line.lstrip("# "),
root_kind,
"API 标题只写名称或调用签名,声明种类写在声明行",
),
)
)
continue
bare = BARE_API_RE.match(line)
if not bare:
continue
level = len(bare.group(1))
signature = bare.group(2)
end = heading_body_end(lines, index, flags)
declaration, error = _declaration(lines, index, end, flags)
heading = member_heading(
level,
declaration,
signature,
root_kind,
unit_class_open,
error,
)
if root_kind == "unit" and level == 3:
unit_class_open = heading.valid and heading.kind == "class"
headings.append((index, heading))
return headings, h2_boundaries
def collect_headings(lines):
headings, _ = collect_headings_and_boundaries(lines)
return headings
def iter_api_entries(lines):
headings, h2_boundaries = collect_headings_and_boundaries(lines)
for index, (start, heading) in enumerate(headings):
next_heading = (
headings[index + 1][0]
if index + 1 < len(headings)
else len(lines)
)
boundary_index = bisect_right(h2_boundaries, start)
next_h2 = (
h2_boundaries[boundary_index]
if boundary_index < len(h2_boundaries)
else len(lines)
)
yield ApiEntry(heading, start, min(next_heading, next_h2))
def slug(text):
return re.sub(r"[^a-z0-9_]", "", text.casefold())