diff options
Diffstat (limited to 'tools/reformat_docstrings.py')
| -rw-r--r-- | tools/reformat_docstrings.py | 1132 |
1 files changed, 1132 insertions, 0 deletions
diff --git a/tools/reformat_docstrings.py b/tools/reformat_docstrings.py new file mode 100644 index 0000000..31dafd6 --- /dev/null +++ b/tools/reformat_docstrings.py @@ -0,0 +1,1132 @@ +#!/usr/bin/env python3 +import re +from pathlib import Path +from typing import Dict, List, Optional, Tuple + + +ROOT = Path("addons/escoria-core") + +DEFAULT_PARAM_DESC = "No description provided." + +PARAM_OVERRIDES_BY_SIGNATURE = { + ( + "addons/escoria-core/game/core-scripts/esc/compiler/esc_grammar_stmts.gd", + "func init(name: ESCToken, initializer: ESCGrammarExpr):", + "name", + ): "Token representing the variable's name.", + ( + "addons/escoria-core/game/core-scripts/esc/compiler/esc_grammar_stmts.gd", + "func init(name: ESCToken, target: ESCGrammarExprs.Literal, flags: Dictionary, body: ESCGrammarStmts.Block, object_global_id: String):", + "name", + ): "Token representing the event name.", + ( + "addons/escoria-core/game/core-scripts/esc/compiler/esc_grammar_exprs.gd", + "func init(name: ESCToken):", + "name", + ): "Token representing the variable's name.", + ( + "addons/escoria-core/game/core-scripts/esc/compiler/esc_grammar_exprs.gd", + "func init(name: ESCToken, value: ESCGrammarExpr):", + "name", + ): "Token representing the variable's name to assign.", +} + +PARAM_OVERRIDES_BY_FUNCTION = { + ( + "addons/escoria-core/game/esc_project_settings_manager.gd", + "register_setting", + "name", + ): "Fully qualified Project Settings key to register.", + ( + "addons/escoria-core/game/esc_project_settings_manager.gd", + "remove_setting", + "name", + ): "Fully qualified Project Settings key to remove.", + ( + "addons/escoria-core/game/core-scripts/esc_animation_player.gd", + "_on_animation_finished", + "name", + ): "Name of the animation that triggered the callback.", + ( + "addons/escoria-core/game/scenes/dialogs/esc_dialog_player.gd", + "_update_dialog_manager", + "dialog_manager_type", + ): "Type name of the dialog manager implementation to instantiate.", + ( + "addons/escoria-core/game/core-scripts/esc/esc_object_manager.gd", + "register_object", + "auto_unregister", + ): "(optional) Automatically unregister the object when its node exits the scene tree (default: `true`).", + ( + "addons/escoria-core/game/core-scripts/esc/esc_event_manager.gd", + "is_channel_free", + "name", + ): "Name of the channel to inspect.", + ( + "addons/escoria-core/game/core-scripts/esc/esc_event_manager.gd", + "get_running_event", + "name", + ): "Name of the channel whose running event should be returned.", + ( + "addons/escoria-core/game/core-scripts/esc/compiler/esc_environment.gd", + "is_valid_key", + "name", + ): "Token describing the variable name to look up.", + ( + "addons/escoria-core/game/core-scripts/esc/compiler/esc_environment.gd", + "get_value", + "name", + ): "Token describing the variable name whose value is requested.", + ( + "addons/escoria-core/game/core-scripts/esc/compiler/esc_environment.gd", + "assign", + "name", + ): "Token describing the variable name to assign.", + ( + "addons/escoria-core/game/core-scripts/esc/compiler/esc_environment.gd", + "define", + "name", + ): "Variable name to register in this scope.", + ( + "addons/escoria-core/game/core-scripts/esc/compiler/esc_environment.gd", + "get_at", + "name", + ): "Variable name to resolve at the requested scope depth.", + ( + "addons/escoria-core/game/core-scripts/esc/compiler/esc_environment.gd", + "assign_at", + "name", + ): "Token describing the variable name to modify at the requested scope depth.", + ( + "addons/escoria-core/game/core-scripts/esc/compiler/esc_interpreter.gd", + "look_up_variable", + "name", + ): "Token representing the variable name to resolve.", + ( + "addons/escoria-core/game/core-scripts/esc/compiler/esc_script_builder.gd", + "add_event", + "name", + ): "Event identifier to add to the script.", + ( + "addons/escoria-core/game/core-scripts/esc/compiler/esc_script_builder.gd", + "add_command", + "name", + ): "Command name to append to the script.", + ( + "addons/escoria-core/game/core-scripts/esc_animation_player.gd", + "play", + "name", + ): "Animation name to play.", + ( + "addons/escoria-core/game/core-scripts/esc_animation_player.gd", + "play_backwards", + "name", + ): "Animation name to play in reverse.", + ( + "addons/escoria-core/game/core-scripts/esc_animation_player.gd", + "has_animation", + "name", + ): "Animation name to test for availability.", + ( + "addons/escoria-core/game/core-scripts/esc_animation_player.gd", + "seek_end", + "name", + ): "Animation name to jump to the last frame of.", + ( + "addons/escoria-core/game/core-scripts/esc_animation_player.gd", + "get_length", + "name", + ): "Animation name whose duration should be returned.", + ( + "addons/escoria-core/game/scenes/transitions/esc_transition_player.gd", + "get_transition", + "name", + ): "Transition name whose material path should be resolved.", + ( + "addons/escoria-core/game/scenes/transitions/esc_transition_player.gd", + "has_transition", + "name", + ): "Transition name to check for availability.", + ( + "addons/escoria-core/plugin.gd", + "register_setting", + "name", + ): "Fully qualified Project Settings key to register.", +} + +PARAM_OVERRIDES_BY_COMMAND = { + ( + "addons/escoria-core/game/core-scripts/esc/commands/anim.gd", + "anim", + "name", + ): "Name of the animation to start on the object.", + ( + "addons/escoria-core/game/core-scripts/esc/commands/anim_block.gd", + "anim_block", + "name", + ): "Name of the animation to play before continuing.", + ( + "addons/escoria-core/game/core-scripts/esc/commands/dec_global.gd", + "dec_global", + "name", + ): "Name of the global variable to decrement.", + ( + "addons/escoria-core/game/core-scripts/esc/commands/inc_global.gd", + "inc_global", + "name", + ): "Name of the global variable to increment.", + ( + "addons/escoria-core/game/core-scripts/esc/commands/rand_global.gd", + "rand_global", + "name", + ): "Name of the global variable that will receive the random value.", + ( + "addons/escoria-core/game/core-scripts/esc/commands/set_global.gd", + "set_global", + "name", + ): "Name of the global variable to set.", +} + +PARAM_OVERRIDES_BY_SIGNAL = { + ( + "addons/escoria-core/game/core-scripts/esc_location.gd", + "editor_is_start_location_set", + "node_to_ignore", + ): "`ESCLocation` that should be ignored while validating start locations.", + ( + "addons/escoria-core/game/core-scripts/esc/esc_globals_manager.gd", + "global_changed", + "global", + ): "Key of the global that changed.", + ( + "addons/escoria-core/game/core-scripts/esc/esc_globals_manager.gd", + "global_changed", + "old_value", + ): "Value stored under the key before the change.", + ( + "addons/escoria-core/game/core-scripts/esc/esc_globals_manager.gd", + "global_changed", + "new_value", + ): "Updated value stored for the key.", + ( + "addons/escoria-core/game/core-scripts/esc_animation_player.gd", + "animation_finished", + "name", + ): "Name of the animation that completed playback.", + ( + "addons/escoria-core/game/core-scripts/esc/esc_event_manager.gd", + "event_started", + "event_name", + ): "Name of the event whose execution has started.", + ( + "addons/escoria-core/game/core-scripts/esc/esc_event_manager.gd", + "background_event_started", + "channel_name", + ): "Name of the background channel where the event runs.", + ( + "addons/escoria-core/game/core-scripts/esc/esc_event_manager.gd", + "background_event_started", + "event_name", + ): "Name of the event that began on the background channel.", + ( + "addons/escoria-core/game/core-scripts/esc/esc_event_manager.gd", + "event_finished", + "return_code", + ): "Execution result returned by the event.", + ( + "addons/escoria-core/game/core-scripts/esc/esc_event_manager.gd", + "event_finished", + "event_name", + ): "Name of the event that just finished.", + ( + "addons/escoria-core/game/core-scripts/esc/esc_event_manager.gd", + "background_event_finished", + "return_code", + ): "Execution result returned by the background event.", + ( + "addons/escoria-core/game/core-scripts/esc/esc_event_manager.gd", + "background_event_finished", + "event_name", + ): "Name of the background event that finished.", + ( + "addons/escoria-core/game/core-scripts/esc/esc_event_manager.gd", + "background_event_finished", + "channel_name", + ): "Background channel where the event finished.", + ( + "addons/escoria-core/game/core-scripts/esc/types/esc_statement.gd", + "finished", + "event", + ): "`ESCStatement` representing the event whose execution completed.", + ( + "addons/escoria-core/game/core-scripts/esc/types/esc_statement.gd", + "finished", + "statement", + ): "`ESCStatement` that was running when the signal fired.", + ( + "addons/escoria-core/game/core-scripts/esc/types/esc_statement.gd", + "finished", + "return_code", + ): "Execution result code produced by the statement.", + ( + "addons/escoria-core/game/core-scripts/esc/types/esc_statement.gd", + "interrupted", + "event", + ): "`ESCStatement` representing the event whose execution was interrupted.", + ( + "addons/escoria-core/game/core-scripts/esc/types/esc_statement.gd", + "interrupted", + "statement", + ): "`ESCStatement` that was executing when the interruption occurred.", + ( + "addons/escoria-core/game/core-scripts/esc/types/esc_statement.gd", + "interrupted", + "return_code", + ): "Execution result code describing the interruption outcome.", + ( + "addons/escoria-core/tools/logging/esc_logger.gd", + "error_message_signal", + "message", + ): "Error or warning message emitted through the logger.", +} + + +def get_param_override( + context_type: str, + path: Path, + context_name: str, + param_name: str, + signature_text: Optional[str] = None, +) -> Optional[str]: + path_key = path.as_posix() + if context_type == "function": + if signature_text: + key = (path_key, signature_text, param_name) + if key in PARAM_OVERRIDES_BY_SIGNATURE: + return PARAM_OVERRIDES_BY_SIGNATURE[key] + key = (path_key, context_name, param_name) + if key in PARAM_OVERRIDES_BY_FUNCTION: + return PARAM_OVERRIDES_BY_FUNCTION[key] + elif context_type == "command": + key = (path_key, context_name, param_name) + if key in PARAM_OVERRIDES_BY_COMMAND: + return PARAM_OVERRIDES_BY_COMMAND[key] + elif context_type == "signal": + key = (path_key, context_name, param_name) + if key in PARAM_OVERRIDES_BY_SIGNAL: + return PARAM_OVERRIDES_BY_SIGNAL[key] + return None + + +def strip_doc_prefix(line: str, indent: str) -> str: + if not line.startswith(indent + "##"): + return "" + content = line[len(indent) + 2 :] + if content.startswith(" "): + content = content[1:] + return content.rstrip("\n") + + +def strip_trailing_br(text: str) -> str: + if not text: + return text + new_text = text + while new_text.endswith("[br]"): + new_text = new_text[: -4].rstrip() + return new_text.strip() + + +def parse_param_bullet(raw_line: str) -> Optional[Tuple[str, str]]: + stripped_original = raw_line.lstrip() + if stripped_original.startswith("##"): + content = stripped_original[2:] + else: + content = stripped_original + while content.startswith("#"): + content = content[1:] + content = content.lstrip() + if not content or not content.startswith(("-", "*")): + return None + line = strip_trailing_br(content) + if not line: + return None + line = line.lstrip("-* \t") + if not line: + return None + if ":" not in line: + return None + name_part, desc_part = line.split(":", 1) + name = name_part.strip().strip("*").strip("`") + desc = desc_part.strip() + if not name: + return None + return name, desc + + +def format_type_cell(type_name: Optional[str]) -> str: + type_str = (type_name or "").strip() + if not type_str: + type_str = "Variant" + # Remove surrounding backticks if present + if type_str.startswith("`") and type_str.endswith("`"): + type_str = type_str[1:-1] + parts = [part.strip() for part in type_str.split("|") if part.strip()] + if not parts: + parts = ["Variant"] + return " or ".join(f"`{part}`" for part in parts) + + +def is_table_structure_line(raw_line: str) -> bool: + text = strip_trailing_br(raw_line.strip()) + if not text.startswith("|"): + return False + inner = [part.strip() for part in text.strip().strip("|").split("|")] + if not inner: + return False + first = inner[0].lower() + return first in {"name", ":-----"} + + +def parse_table_row(raw_line: str) -> Optional[Dict[str, str]]: + text = strip_trailing_br(raw_line.strip()) + if not text.startswith("|"): + return None + inner = [part.strip() for part in text.strip().strip("|").split("|")] + if len(inner) != 4: + return None + if inner[0].lower() in {"name", ":-----"}: + return None + name_cell = inner[0].lstrip("\\").strip() + return { + "name": name_cell, + "type": inner[1].strip("`"), + "desc": inner[2], + "required": inner[3].lower(), + } + + +def extract_inline_return_type(text: str) -> Tuple[str, Optional[str]]: + stripped = text.strip() + match = re.search(r"\(`([^`]+)`\)\.?$", stripped) + if not match: + return text, None + type_name = match.group(1) + prefix = stripped[: match.start()].rstrip() + if prefix.endswith("."): + prefix = prefix[:-1].rstrip() + return prefix, type_name + + +def normalize_whitespace(text: str) -> str: + return re.sub(r"\s+", " ", text).strip() + + +def parse_function_signature(lines: List[str]) -> Tuple[str, str, List[Dict], str]: + signature_text = " ".join(line.strip() for line in lines) + signature_text = normalize_whitespace(signature_text) + match = re.match(r"(?:static\s+)?func\s+([A-Za-z0-9_]+)\s*\((.*)\)\s*(?:->\s*([^:]+))?:", signature_text) + func_name = "" + params: List[Dict] = [] + return_type = "void" + if match: + func_name = match.group(1) + raw_params = match.group(2).strip() + return_type = match.group(3).strip() if match.group(3) else "void" + if raw_params: + params = parse_parameter_list(raw_params) + return func_name, signature_text, params, return_type + + +def parse_parameter_list(params_fragment: str) -> List[Dict]: + params: List[Dict] = [] + current = "" + depth = 0 + for ch in params_fragment: + if ch in "([{": + depth += 1 + current += ch + continue + if ch in ")]}": + depth = max(depth - 1, 0) + current += ch + continue + if ch == "," and depth == 0: + token = current.strip() + if token: + params.append(parse_parameter_token(token)) + current = "" + continue + current += ch + token = current.strip() + if token: + params.append(parse_parameter_token(token)) + return params + + +def parse_parameter_token(token: str) -> Dict: + required = True + name_part = token + if "=" in token: + name_part, _ = token.split("=", 1) + required = False + param_type = "Variant" + name = name_part.strip() + if ":" in name_part: + name_bits = name_part.split(":", 1) + name = name_bits[0].strip() + type_candidate = name_bits[1].strip() + if type_candidate: + param_type = type_candidate + if "=" in param_type: + param_type = param_type.split("=", 1)[0].strip() + if "=" in name: + name = name.split("=", 1)[0].strip() + required = False + name = name.lstrip("\\").strip() + if not name: + name = "param" + return {"name": name, "type": param_type, "required": required} + + +def sanitize_description(text: str) -> str: + text = strip_trailing_br(text) + lowered = text.strip().lower() + if lowered in {"none", "none."}: + return "" + return text + + +def clean_desc_text(text: str) -> str: + if not text: + return text + cleaned = re.sub(r"(?:\s*None\.)+$", "", text).strip() + return cleaned + + +def append_extra_text(current: str, extra: str) -> str: + extra = strip_trailing_br(extra.strip()) + extra = extra.lstrip("-* \t") + if not extra: + return current + if not current: + return extra + return f"{current} {extra}" + + +def merge_extra_rows(rows: List[Tuple[str, str, str, str]], extras: List[Tuple[str, str]]) -> List[Tuple[str, str, str, str]]: + if not extras: + return rows + merged = list(rows) + for name, desc in extras: + name = name.strip() + desc = desc.strip() + if not name and not desc: + continue + text = " ".join(filter(None, [name, desc])) + if merged: + last_name, last_type, last_desc, last_required = merged[-1] + merged[-1] = ( + last_name, + last_type, + append_extra_text(last_desc, text), + last_required, + ) + else: + merged.append((name or "Extra", format_type_cell("Variant"), desc or DEFAULT_PARAM_DESC, "yes")) + return merged + + +def reformat_function_docstring( + block_lines: List[str], + indent: str, + following_lines: List[str], + path: Path, +) -> Optional[List[str]]: + content_lines = [strip_doc_prefix(line, indent) for line in block_lines] + # Collect signature lines + signature_lines: List[str] = [] + for line in following_lines: + signature_lines.append(line) + if line.strip().endswith(":"): + break + func_name, signature_text, params_info, return_type = parse_function_signature(signature_lines) + + desc_lines: List[str] = [] + params: List[Dict[str, str]] = [] + param_buffer: Optional[Dict[str, str]] = None + return_lines: List[str] = [] + + for raw_line in content_lines: + stripped = raw_line.strip() + if not stripped: + continue + if stripped.startswith("@ESC"): + break + lowered = stripped.lower() + if lowered in {"[br]", "##"}: + continue + if lowered.startswith("#### parameters") or lowered.startswith("**parameters"): + param_buffer = None + continue + if lowered.startswith("parameters"): + param_buffer = None + continue + if lowered.startswith("#### returns") or lowered.startswith("**returns"): + param_buffer = None + continue + if lowered.startswith("*returns*"): + text = stripped[len("*Returns*") :].strip(" :-") + text = strip_trailing_br(text) + if text: + return_lines.append(text) + param_buffer = None + continue + if lowered.startswith("returns "): + text = stripped[len("returns ") :].strip() + text = strip_trailing_br(text) + if text: + return_lines.append(text) + param_buffer = None + continue + if is_table_structure_line(raw_line): + continue + table_entry = parse_table_row(raw_line) + if table_entry: + entry = {"name": table_entry["name"], "desc": table_entry["desc"]} + params.append(entry) + param_buffer = entry + continue + bullet = parse_param_bullet(raw_line) + if bullet: + name, desc = bullet + entry = {"name": name, "desc": desc} + params.append(entry) + param_buffer = entry + continue + if param_buffer: + param_buffer["desc"] = append_extra_text(param_buffer.get("desc", ""), stripped) + continue + if return_lines: + return_lines[-1] = append_extra_text(return_lines[-1], stripped) + continue + desc_lines.append(sanitize_description(raw_line)) + + desc_text = " ".join(filter(None, [strip_trailing_br(line) for line in desc_lines])).strip() + desc_text = clean_desc_text(desc_text) + if not desc_text and return_lines: + desc_text = return_lines[0] + if desc_text and desc_text[0].islower(): + desc_text = desc_text[0].upper() + desc_text[1:] + if not desc_text: + desc_text = "No description provided." + + # Prepare parameter rows following signature order + param_map = {entry["name"]: entry.get("desc", "") for entry in params} + rows: List[Tuple[str, str, str, str]] = [] + for param in params_info: + name = param["name"] + type_name = format_type_cell(param["type"]) + required = "yes" if param["required"] else "no" + desc = param_map.pop(name, "").strip() + desc_key = desc.strip() + if not desc_key or desc_key.startswith(DEFAULT_PARAM_DESC): + override = get_param_override("function", path, func_name, name, signature_text) + if override: + desc = override.strip() + desc_key = desc + if not desc_key: + desc = DEFAULT_PARAM_DESC + else: + desc = desc_key + rows.append((name, type_name, desc, required)) + rows = merge_extra_rows(rows, list(param_map.items())) + + raw_return_desc = " ".join(return_lines).strip() + cleaned_return_desc, inline_type = extract_inline_return_type(raw_return_desc) + return_desc = cleaned_return_desc.strip() + effective_declared_type = return_type + if effective_declared_type == "void" and inline_type: + test_desc = re.sub(r"[\.\s]+$", "", cleaned_return_desc.strip().lower()) + if test_desc not in {"", "returns nothing", "nothing"}: + effective_declared_type = inline_type + inferred_return_type = infer_return_type(effective_declared_type, return_desc or raw_return_desc) + if not return_desc: + if inferred_return_type == "void": + return_desc = "Returns nothing." + else: + return_desc = f"Returns a `{inferred_return_type}` value." + else: + lowered = return_desc.lower() + if lowered.startswith("returns "): + return_desc = return_desc[8:].strip() + elif lowered.startswith("return "): + return_desc = return_desc[7:].strip() + if return_desc and not return_desc.endswith("."): + return_desc += "." + return_desc = f"Returns {normalize_return_sentence(return_desc)}" + if inferred_return_type and inferred_return_type != "void": + return_desc = f"{return_desc} (`{inferred_return_type}`)" + + new_block: List[str] = [] + new_block.append(f"{indent}## {desc_text}[br]") + new_block.append(f"{indent}## [br]") + new_block.append(f"{indent}## #### Parameters[br]") + new_block.append(f"{indent}## [br]") + if rows: + new_block.append(f"{indent}## | Name | Type | Description | Required? |[br]") + new_block.append(f"{indent}## |:-----|:-----|:------------|:----------|[br]") + for name, type_name, description, required in rows: + new_block.append(f"{indent}## |{name}|{type_name}|{description}|{required}|[br]") + new_block.append(f"{indent}## [br]") + else: + new_block.append(f"{indent}## None.") + new_block.append(f"{indent}## [br]") + new_block.append(f"{indent}## #### Returns[br]") + new_block.append(f"{indent}## [br]") + new_block.append(f"{indent}## {return_desc}") + return new_block + + +def parse_command_signature(first_line: str) -> Tuple[str, List[Dict]]: + command_name = "" + fragment = "" + signature_match = re.match(r"`\s*([A-Za-z0-9_]+)\s*\((.*)\)`", first_line.strip()) + if signature_match: + command_name = signature_match.group(1) + fragment = signature_match.group(2).strip() + else: + match = re.search(r"`[^`]*\((.*)\)`", first_line) + if match: + fragment = match.group(1).strip() + if not fragment: + return command_name, [] + params: List[Dict] = [] + current = "" + optional_depth = 0 + for ch in fragment: + if ch == "[": + token = current.strip() + if token: + params.append(parse_command_token(token, optional_depth > 0)) + current = "" + optional_depth += 1 + continue + if ch == "]": + token = current.strip() + if token: + params.append(parse_command_token(token, optional_depth > 0)) + current = "" + optional_depth = max(optional_depth - 1, 0) + continue + if ch == ",": + token = current.strip() + if token: + params.append(parse_command_token(token, optional_depth > 0)) + current = "" + continue + current += ch + token = current.strip() + if token: + params.append(parse_command_token(token, optional_depth > 0)) + return command_name, params + + +def parse_command_token(token: str, is_optional: bool) -> Dict: + token = token.strip() + param_type = "Variant" + name = token + if ":" in token: + name_part, type_part = token.split(":", 1) + name = name_part.strip() + type_candidate = type_part.strip() + if type_candidate: + param_type = type_candidate + if "=" in param_type: + param_type = param_type.split("=", 1)[0].strip() + if "=" in name: + name = name.split("=", 1)[0].strip() + is_optional = True + return {"name": name, "type": param_type, "required": not is_optional} + + +def reformat_command_docstring( + block_lines: List[str], + indent: str, + path: Path, +) -> Optional[List[str]]: + content_lines = [strip_doc_prefix(line, indent) for line in block_lines] + if not content_lines: + return None + + signature_line = content_lines[0].strip() + command_name, params_info = parse_command_signature(signature_line) + + desc_lines: List[str] = [] + params: List[Dict[str, str]] = [] + param_buffer: Optional[Dict[str, str]] = None + extra_lines: List[str] = [] + in_param_section = False + + for raw_line in content_lines[1:]: + stripped = raw_line.strip() + lowered = stripped.lower() + if not stripped or lowered == "[br]": + continue + if stripped.startswith("@ESC"): + break + if lowered.startswith("#### parameters") or lowered.startswith("**parameters"): + in_param_section = True + param_buffer = None + continue + if lowered.startswith("parameters"): + in_param_section = True + param_buffer = None + continue + if in_param_section and is_table_structure_line(raw_line): + continue + if in_param_section: + table_entry = parse_table_row(raw_line) + if table_entry: + entry = {"name": table_entry["name"], "desc": table_entry["desc"]} + params.append(entry) + param_buffer = entry + continue + bullet = parse_param_bullet(raw_line) + if bullet and in_param_section: + name, desc = bullet + entry = {"name": name, "desc": desc} + params.append(entry) + param_buffer = entry + continue + if in_param_section and param_buffer: + param_buffer["desc"] = append_extra_text(param_buffer.get("desc", ""), stripped) + continue + if in_param_section: + extra_entry = strip_trailing_br(stripped) + if extra_entry and extra_entry.strip().lower() != "none.": + extra_lines.append(extra_entry) + continue + desc_lines.append(strip_trailing_br(stripped)) + + desc_text = " ".join(filter(None, desc_lines)).strip() + desc_text = clean_desc_text(desc_text) + param_map = {entry["name"]: entry.get("desc", "") for entry in params} + rows: List[Tuple[str, str, str, str]] = [] + for param in params_info: + name = param["name"] + type_name = format_type_cell(param["type"]) + required = "yes" if param["required"] else "no" + desc = param_map.pop(name, "").strip() + desc_key = desc.strip() + if not desc_key or desc_key.startswith(DEFAULT_PARAM_DESC): + override = get_param_override("command", path, command_name, name) + if override: + desc = override.strip() + desc_key = desc + if not desc_key: + desc = DEFAULT_PARAM_DESC + else: + desc = desc_key + rows.append((name, type_name, desc, required)) + rows = merge_extra_rows(rows, list(param_map.items())) + + new_block: List[str] = [] + new_block.append(f"{indent}## {signature_line}") + new_block.append(f"{indent}##") + if desc_text: + new_block.append(f"{indent}## {desc_text}[br]") + else: + new_block.append(f"{indent}## [br]") + new_block.append(f"{indent}## [br]") + new_block.append(f"{indent}## #### Parameters[br]") + new_block.append(f"{indent}## [br]") + if rows: + new_block.append(f"{indent}## | Name | Type | Description | Required? |[br]") + new_block.append(f"{indent}## |:-----|:-----|:------------|:----------|[br]") + for name, type_name, description, required in rows: + new_block.append(f"{indent}## |{name}|{type_name}|{description}|{required}|[br]") + new_block.append(f"{indent}## [br]") + for line in extra_lines: + if line: + new_block.append(f"{indent}## {line}[br]") + else: + new_block.append(f"{indent}## None.") + new_block.append(f"{indent}## [br]") + for line in extra_lines: + if line: + new_block.append(f"{indent}## {line}[br]") + return new_block + + +def normalize_return_sentence(text: str) -> str: + text = text.strip() + if not text: + return text + first_word = text.split(" ", 1)[0].lower() + if first_word in {"the", "a", "an"} and len(text) > 1: + text = text[0].lower() + text[1:] + return text + + +def infer_return_type(declared_type: str, return_desc: str) -> str: + declared_type = (declared_type or "").strip() + if declared_type and declared_type != "void": + return declared_type + if not return_desc: + return declared_type or "void" + lowered = return_desc.lower() + if "nothing" in lowered or "no value" in lowered: + return "void" + if "true" in lowered or "false" in lowered: + return "bool" + if "array" in lowered: + return "Array" + if "dictionary" in lowered: + return "Dictionary" + if "string" in lowered: + return "String" + if "float" in lowered: + return "float" + if "int" in lowered: + return "int" + if "vector2" in lowered: + return "Vector2" + if "vector3" in lowered: + return "Vector3" + return "Variant" + + +def parse_signal_signature(signal_line: str) -> Tuple[str, List[str]]: + match = re.match(r"\s*signal\s+([A-Za-z0-9_]+)\s*(?:\((.*)\))?", signal_line) + if not match: + return "", [] + signal_name = match.group(1) + params_fragment = (match.group(2) or "").strip() + if not params_fragment: + return signal_name, [] + params = [] + current = "" + depth = 0 + for ch in params_fragment: + if ch == "," and depth == 0: + token = current.strip() + if token: + params.append(token) + current = "" + continue + if ch in "([{": + depth += 1 + elif ch in ")]}": + depth = max(depth - 1, 0) + current += ch + token = current.strip() + if token: + params.append(token) + names = [] + for token in params: + if ":" in token: + token = token.split(":", 1)[0].strip() + if "=" in token: + token = token.split("=", 1)[0].strip() + names.append(token) + return signal_name, names + + +def reformat_signal_docstring( + block_lines: List[str], + indent: str, + signal_line: str, + path: Path, +) -> Optional[List[str]]: + content_lines = [strip_doc_prefix(line, indent) for line in block_lines] + if not content_lines: + return None + + desc_lines: List[str] = [] + params: List[Dict[str, str]] = [] + param_buffer: Optional[Dict[str, str]] = None + in_param_section = False + + for raw_line in content_lines: + stripped = raw_line.strip() + lowered = stripped.lower() + if not stripped or lowered == "[br]": + continue + if lowered.startswith("#### parameters") or lowered.startswith("**parameters"): + in_param_section = True + param_buffer = None + continue + if lowered.startswith("parameters"): + in_param_section = True + param_buffer = None + continue + if lowered.startswith("#### returns") or lowered.startswith("**returns"): + in_param_section = False + continue + if in_param_section and is_table_structure_line(raw_line): + continue + if in_param_section: + table_entry = parse_table_row(raw_line) + if table_entry: + entry = {"name": table_entry["name"], "desc": table_entry["desc"]} + params.append(entry) + param_buffer = entry + continue + bullet = parse_param_bullet(raw_line) + if bullet and in_param_section: + name, desc = bullet + entry = {"name": name, "desc": desc} + params.append(entry) + param_buffer = entry + continue + if in_param_section and param_buffer: + param_buffer["desc"] = append_extra_text(param_buffer.get("desc", ""), stripped) + continue + if in_param_section: + continue + desc_lines.append(strip_trailing_br(stripped)) + + desc_text = " ".join(filter(None, desc_lines)).strip() + desc_text = clean_desc_text(desc_text) + if not desc_text: + desc_text = "No description provided." + + signal_name, signature_params = parse_signal_signature(signal_line) + param_map = {entry["name"]: entry.get("desc", "") for entry in params} + rows: List[Tuple[str, str, str, str]] = [] + for param_name in signature_params: + desc = param_map.pop(param_name, "").strip() + desc_key = desc.strip() + if not desc_key or desc_key.startswith(DEFAULT_PARAM_DESC): + override = get_param_override("signal", path, signal_name, param_name) + if override: + desc = override.strip() + desc_key = desc + if not desc_key: + desc = DEFAULT_PARAM_DESC + else: + desc = desc_key + rows.append((param_name, format_type_cell(None), desc, "yes")) + for extra_name, desc in param_map.items(): + name = extra_name.strip() + if not name: + continue + cleaned_desc = desc.strip() + if not cleaned_desc or cleaned_desc.startswith(DEFAULT_PARAM_DESC): + override = get_param_override("signal", path, signal_name, name) + if override: + cleaned_desc = override + if not cleaned_desc: + cleaned_desc = DEFAULT_PARAM_DESC + rows.append((name, format_type_cell(None), cleaned_desc, "yes")) + + new_block: List[str] = [] + new_block.append(f"{indent}## {desc_text}[br]") + new_block.append(f"{indent}## [br]") + new_block.append(f"{indent}## #### Parameters[br]") + new_block.append(f"{indent}## [br]") + if rows: + new_block.append(f"{indent}## | Name | Type | Description | Required? |[br]") + new_block.append(f"{indent}## |:-----|:-----|:------------|:----------|[br]") + for name, type_name, description, required in rows: + new_block.append(f"{indent}## |{name}|{type_name}|{description}|{required}|[br]") + new_block.append(f"{indent}## [br]") + else: + new_block.append(f"{indent}## None.") + new_block.append(f"{indent}## [br]") + return new_block + + +def process_file(path: Path) -> Tuple[str, bool]: + text = path.read_text() + lines = text.splitlines() + i = 0 + changed = False + while i < len(lines): + line = lines[i] + stripped = line.lstrip() + if not stripped.startswith("##") or stripped.startswith("## @"): + i += 1 + continue + indent_match = re.match(r"^(\s*)##", line) + if not indent_match: + i += 1 + continue + indent = indent_match.group(1) + block_start = i + block_end = i + while block_end < len(lines): + stripped_block = lines[block_end].lstrip() + if not stripped_block.startswith("##") or stripped_block.startswith("## @"): + break + block_end += 1 + block_lines = lines[block_start:block_end] + + # Determine context + k = block_end + signature_lines: List[str] = [] + while k < len(lines): + candidate = lines[k].strip() + if candidate == "": + k += 1 + continue + if candidate.startswith("## @"): + k += 1 + continue + if candidate.startswith("##"): + break + break + if k >= len(lines): + i = block_end + continue + next_line = lines[k] + + if re.match(r"\s*(?:static\s+)?func\b", next_line): + signature_lines = [] + sig_index = k + while sig_index < len(lines): + signature_lines.append(lines[sig_index].strip()) + if lines[sig_index].strip().endswith(":"): + break + sig_index += 1 + new_block = reformat_function_docstring(block_lines, indent, signature_lines, path) + if new_block: + lines[block_start:block_end] = new_block + changed = True + block_end = block_start + len(new_block) + i = block_end + continue + elif "extends ESCBaseCommand" in next_line: + new_block = reformat_command_docstring(block_lines, indent, path) + if new_block: + lines[block_start:block_end] = new_block + changed = True + block_end = block_start + len(new_block) + i = block_end + continue + elif next_line.strip().startswith("signal "): + new_block = reformat_signal_docstring(block_lines, indent, next_line.strip(), path) + if new_block: + lines[block_start:block_end] = new_block + changed = True + block_end = block_start + len(new_block) + i = block_end + continue + i = block_end + new_text = "\n".join(lines) + if text.endswith("\n"): + new_text += "\n" + return new_text, changed + + +def main() -> None: + total_changed = 0 + for path in ROOT.rglob("*.gd"): + new_text, changed = process_file(path) + if changed: + path.write_text(new_text) + total_changed += 1 + + +if __name__ == "__main__": + main() |
