Files
game-cards/addons/gut/documentation/class_ref/godot_classes.py
2026-05-29 09:16:10 +08:00

624 lines
23 KiB
Python

from collections import OrderedDict
import xml.etree.ElementTree as ET
from typing import Any, Dict, List, Optional, TextIO, Tuple, Union
from godot_consts import *
import logger as lgr
import bitwes
class State:
def __init__(self) -> None:
self.num_errors = 0
self.num_warnings = 0
self.classes: OrderedDict[str, ClassDef] = OrderedDict()
self.current_class: str = ""
# Additional content and structure checks and validators.
self.script_language_parity_check: ScriptLanguageParityCheck = ScriptLanguageParityCheck()
def parse_class(self, class_root: ET.Element, filepath: str) -> None:
# -bitwes: remove quotes from class names, these appear when the script
# does not have a class_name. This prevents the quotes from appearing
# in TOC and allows linking to scripts by path without having to use
# quotes.
class_name = class_root.attrib["name"].replace('"', "")
self.current_class = class_name
class_def = ClassDef(class_name)
self.classes[class_name] = class_def
class_def.filepath = filepath
inherits = class_root.get("inherits")
if inherits is not None:
class_def.inherits = inherits
class_def.deprecated = class_root.get("deprecated")
class_def.experimental = class_root.get("experimental")
brief_desc = class_root.find("brief_description")
if brief_desc is not None and brief_desc.text:
class_def.brief_description = brief_desc.text
desc = class_root.find("description")
if desc is not None and desc.text:
class_def.description = desc.text
keywords = class_root.get("keywords")
if keywords is not None:
class_def.keywords = keywords
properties = class_root.find("members")
if properties is not None:
for property in properties:
assert property.tag == "member"
property_name = property.attrib["name"]
if property_name in class_def.properties:
lgr.print_error(f'{class_name}.xml: Duplicate property "{property_name}".', self)
continue
type_name = TypeName.from_element(property)
setter = property.get("setter") or None # Use or None so '' gets turned into None.
getter = property.get("getter") or None
default_value = property.get("default") or None
if default_value is not None:
default_value = f"``{default_value}``"
overrides = property.get("overrides") or None
property_def = PropertyDef(
property_name, type_name, setter, getter, property.text, default_value, overrides
)
property_def.deprecated = property.get("deprecated")
property_def.experimental = property.get("experimental")
class_def.properties[property_name] = property_def
constructors = class_root.find("constructors")
if constructors is not None:
for constructor in constructors:
assert constructor.tag == "constructor"
method_name = constructor.attrib["name"]
qualifiers = constructor.get("qualifiers")
return_element = constructor.find("return")
if return_element is not None:
return_type = TypeName.from_element(return_element)
else:
return_type = TypeName("void")
params = self.parse_params(constructor, "constructor")
desc_element = constructor.find("description")
method_desc = None
if desc_element is not None:
method_desc = desc_element.text
method_def = MethodDef(method_name, return_type, params, method_desc, qualifiers)
method_def.definition_name = "constructor"
method_def.deprecated = constructor.get("deprecated")
method_def.experimental = constructor.get("experimental")
if method_name not in class_def.constructors:
class_def.constructors[method_name] = []
class_def.constructors[method_name].append(method_def)
methods = class_root.find("methods")
if methods is not None:
for method in methods:
assert method.tag == "method"
method_name = method.attrib["name"]
qualifiers = method.get("qualifiers")
return_element = method.find("return")
if return_element is not None:
return_type = TypeName.from_element(return_element)
else:
return_type = TypeName("void")
params = self.parse_params(method, "method")
desc_element = method.find("description")
method_desc = None
if desc_element is not None:
method_desc = desc_element.text
method_def = MethodDef(method_name, return_type, params, method_desc, qualifiers)
method_def.deprecated = method.get("deprecated")
method_def.experimental = method.get("experimental")
if method_name not in class_def.methods:
class_def.methods[method_name] = []
class_def.methods[method_name].append(method_def)
operators = class_root.find("operators")
if operators is not None:
for operator in operators:
assert operator.tag == "operator"
method_name = operator.attrib["name"]
qualifiers = operator.get("qualifiers")
return_element = operator.find("return")
if return_element is not None:
return_type = TypeName.from_element(return_element)
else:
return_type = TypeName("void")
params = self.parse_params(operator, "operator")
desc_element = operator.find("description")
method_desc = None
if desc_element is not None:
method_desc = desc_element.text
method_def = MethodDef(method_name, return_type, params, method_desc, qualifiers)
method_def.definition_name = "operator"
method_def.deprecated = operator.get("deprecated")
method_def.experimental = operator.get("experimental")
if method_name not in class_def.operators:
class_def.operators[method_name] = []
class_def.operators[method_name].append(method_def)
constants = class_root.find("constants")
if constants is not None:
for constant in constants:
assert constant.tag == "constant"
constant_name = constant.attrib["name"]
value = constant.attrib["value"]
enum = constant.get("enum")
is_bitfield = constant.get("is_bitfield") == "true"
constant_def = ConstantDef(constant_name, value, constant.text, is_bitfield)
constant_def.deprecated = constant.get("deprecated")
constant_def.experimental = constant.get("experimental")
if enum is None:
if constant_name in class_def.constants:
lgr.print_error(f'{class_name}.xml: Duplicate constant "{constant_name}".', self)
continue
class_def.constants[constant_name] = constant_def
else:
if enum in class_def.enums:
enum_def = class_def.enums[enum]
else:
enum_def = EnumDef(enum, TypeName("int", enum), is_bitfield)
class_def.enums[enum] = enum_def
enum_def.values[constant_name] = constant_def
annotations = class_root.find("annotations")
if annotations is not None:
for annotation in annotations:
assert annotation.tag == "annotation"
annotation_name = annotation.attrib["name"]
qualifiers = annotation.get("qualifiers")
params = self.parse_params(annotation, "annotation")
desc_element = annotation.find("description")
annotation_desc = None
if desc_element is not None:
annotation_desc = desc_element.text
annotation_def = AnnotationDef(annotation_name, params, annotation_desc, qualifiers)
if annotation_name not in class_def.annotations:
class_def.annotations[annotation_name] = []
class_def.annotations[annotation_name].append(annotation_def)
signals = class_root.find("signals")
if signals is not None:
for signal in signals:
assert signal.tag == "signal"
signal_name = signal.attrib["name"]
if signal_name in class_def.signals:
lgr.print_error(f'{class_name}.xml: Duplicate signal "{signal_name}".', self)
continue
params = self.parse_params(signal, "signal")
desc_element = signal.find("description")
signal_desc = None
if desc_element is not None:
signal_desc = desc_element.text
signal_def = SignalDef(signal_name, params, signal_desc)
signal_def.deprecated = signal.get("deprecated")
signal_def.experimental = signal.get("experimental")
class_def.signals[signal_name] = signal_def
theme_items = class_root.find("theme_items")
if theme_items is not None:
for theme_item in theme_items:
assert theme_item.tag == "theme_item"
theme_item_name = theme_item.attrib["name"]
theme_item_data_name = theme_item.attrib["data_type"]
theme_item_id = "{}_{}".format(theme_item_data_name, theme_item_name)
if theme_item_id in class_def.theme_items:
lgr.print_error(
f'{class_name}.xml: Duplicate theme property "{theme_item_name}" of type "{theme_item_data_name}".',
self,
)
continue
default_value = theme_item.get("default") or None
if default_value is not None:
default_value = f"``{default_value}``"
theme_item_def = ThemeItemDef(
theme_item_name,
TypeName.from_element(theme_item),
theme_item_data_name,
theme_item.text,
default_value,
)
class_def.theme_items[theme_item_name] = theme_item_def
tutorials = class_root.find("tutorials")
if tutorials is not None:
for link in tutorials:
assert link.tag == "link"
if link.text is not None:
class_def.tutorials.append((link.text.strip(), link.get("title", "")))
self.current_class = ""
def parse_params(self, root: ET.Element, context: str) -> List["ParameterDef"]:
param_elements = root.findall("param")
params: Any = [None] * len(param_elements)
for param_index, param_element in enumerate(param_elements):
param_name = param_element.attrib["name"]
index = int(param_element.attrib["index"])
type_name = TypeName.from_element(param_element)
default = param_element.get("default")
if param_name.strip() == "" or param_name.startswith("_unnamed_arg"):
lgr.print_error(
f'{self.current_class}.xml: Empty argument name in {context} "{root.attrib["name"]}" at position {param_index}.',
self,
)
params[index] = ParameterDef(param_name, type_name, default)
cast: List[ParameterDef] = params
return cast
def sort_classes(self) -> None:
self.classes = OrderedDict(sorted(self.classes.items(), key=lambda t: t[0].lower()))
class TypeName:
def __init__(self, type_name: str, enum: Optional[str] = None, is_bitfield: bool = False) -> None:
self.type_name = type_name
self.enum = enum
self.is_bitfield = is_bitfield
def to_rst(self, state: State) -> str:
if self.enum is not None:
return make_enum(self.enum, self.is_bitfield, state)
elif self.type_name == "void":
return "|void|"
else:
return make_type(self.type_name, state)
@classmethod
def from_element(cls, element: ET.Element) -> "TypeName":
return cls(element.attrib["type"], element.get("enum"), element.get("is_bitfield") == "true")
class DefinitionBase:
def __init__(
self,
definition_name: str,
name: str,
) -> None:
self.definition_name = definition_name
self.name = name
self.deprecated: Optional[str] = None
self.experimental: Optional[str] = None
self.description: Optional[str] = None
# Checks the description for an annotation, returns if it is there,
# optionally replaces the annotation in the description with replace_text.
def desc_annotation(self, ann_text, replace_text=None):
exists = False
if(self.description != None):
exists = ann_text in self.description
if(exists and replace_text != None):
self.description = self.description.replace(ann_text, replace_text)
return exists
def is_description_empty(self):
return self.description is None or self.description.strip() == ""
class PropertyDef(DefinitionBase):
def __init__(
self,
name: str,
type_name: TypeName,
setter: Optional[str],
getter: Optional[str],
text: Optional[str],
default_value: Optional[str],
overrides: Optional[str],
) -> None:
super().__init__("property", name)
self.type_name = type_name
self.setter = setter
self.getter = getter
self.text = "!! No longer used, use description !!"
self.default_value = default_value
self.overrides = overrides
self.description = text
self.ignore = self.desc_annotation("@ignore")
class ParameterDef(DefinitionBase):
def __init__(self, name: str, type_name: TypeName, default_value: Optional[str]) -> None:
super().__init__("parameter", name)
self.type_name = type_name
self.default_value = default_value
class SignalDef(DefinitionBase):
def __init__(self, name: str, parameters: List[ParameterDef], description: Optional[str]) -> None:
super().__init__("signal", name)
self.parameters = parameters
self.description = description
self.ignore = self.desc_annotation("@ignore")
class AnnotationDef(DefinitionBase):
def __init__(
self,
name: str,
parameters: List[ParameterDef],
description: Optional[str],
qualifiers: Optional[str],
) -> None:
super().__init__("annotation", name)
self.parameters = parameters
self.description = description
self.qualifiers = qualifiers
class MethodDef(DefinitionBase):
def __init__(
self,
name: str,
return_type: TypeName,
parameters: List[ParameterDef],
description: Optional[str],
qualifiers: Optional[str],
) -> None:
super().__init__("method", name)
self.return_type = return_type
self.parameters = parameters
self.description = description
self.qualifiers = qualifiers
self.internal = self.desc_annotation("@internal", "[b]Internal use only.[/b]")
self.ignore = self.desc_annotation("@ignore", None)
class ConstantDef(DefinitionBase):
def __init__(self, name: str, value: str, text: Optional[str], bitfield: bool) -> None:
super().__init__("constant", name)
self.value = value
self.text = text
self.is_bitfield = bitfield
class EnumDef(DefinitionBase):
def __init__(self, name: str, type_name: TypeName, bitfield: bool) -> None:
super().__init__("enum", name)
self.type_name = type_name
self.values: OrderedDict[str, ConstantDef] = OrderedDict()
self.is_bitfield = bitfield
class ThemeItemDef(DefinitionBase):
def __init__(
self, name: str, type_name: TypeName, data_name: str, text: Optional[str], default_value: Optional[str]
) -> None:
super().__init__("theme property", name)
self.type_name = type_name
self.data_name = data_name
self.text = text
self.default_value = default_value
class ClassDef(DefinitionBase):
def __init__(self, name: str) -> None:
super().__init__("class", name)
self.class_group = "variant"
self.editor_class = self._is_editor_class()
self.constants: OrderedDict[str, ConstantDef] = OrderedDict()
self.enums: OrderedDict[str, EnumDef] = OrderedDict()
self.properties: OrderedDict[str, PropertyDef] = OrderedDict()
self.constructors: OrderedDict[str, List[MethodDef]] = OrderedDict()
self.methods: OrderedDict[str, List[MethodDef]] = OrderedDict()
self.operators: OrderedDict[str, List[MethodDef]] = OrderedDict()
self.signals: OrderedDict[str, SignalDef] = OrderedDict()
self.annotations: OrderedDict[str, List[AnnotationDef]] = OrderedDict()
self.theme_items: OrderedDict[str, ThemeItemDef] = OrderedDict()
self.inherits: Optional[str] = None
self.brief_description: Optional[str] = None
self.description: Optional[str] = None
self.tutorials: List[Tuple[str, str]] = []
self.keywords: Optional[str] = None
# Used to match the class with XML source for output filtering purposes.
self.filepath: str = ""
self.ignore_uncommented = False
def _is_editor_class(self) -> bool:
if self.name.startswith("Editor"):
return True
if self.name in EDITOR_CLASSES:
return True
return False
def update_class_group(self, state: State) -> None:
group_name = "variant"
if self.name.startswith("@"):
group_name = "global"
elif self.inherits:
inherits = self.inherits.strip()
while inherits in state.classes:
if inherits == "Node":
group_name = "node"
break
if inherits == "Resource":
group_name = "resource"
break
if inherits == "Object":
group_name = "object"
break
inode = state.classes[inherits].inherits
if inode:
inherits = inode.strip()
else:
break
self.class_group = group_name
def _strip_private_props(self):
to_delete = []
for key in self.properties.keys():
if(key.startswith("_") and self.properties[key].is_description_empty()):
to_delete.append(key)
for del_me in to_delete:
del self.properties[del_me]
def _strip_private_methods(self):
to_delete = []
for key in self.methods.keys():
if(key.startswith("_") and self.methods[key][0].is_description_empty()):
to_delete.append(key)
for del_me in to_delete:
del self.methods[del_me]
def strip_privates(self):
self.ignore_uncommented = self.desc_annotation("@ignore-uncommented", "")
self._strip_private_props()
self._strip_private_methods()
# Checks if code samples have both GDScript and C# variations.
# For simplicity we assume that a GDScript example is always present, and ignore contexts
# which don't necessarily need C# examples.
class ScriptLanguageParityCheck:
def __init__(self) -> None:
self.hit_map: OrderedDict[str, List[Tuple[DefinitionBase, str]]] = OrderedDict()
self.hit_count = 0
def add_hit(self, class_name: str, context: DefinitionBase, error: str, state: State) -> None:
if class_name in ["@GDScript", "@GlobalScope"]:
return # We don't expect these contexts to have parity.
class_def = state.classes[class_name]
if class_def.class_group == "variant" and class_def.name != "Object":
return # Variant types are replaced with native types in C#, we don't expect parity.
self.hit_count += 1
if class_name not in self.hit_map:
self.hit_map[class_name] = []
self.hit_map[class_name].append((context, error))
def make_type(klass: str, state: State) -> str:
if klass.find("*") != -1: # Pointer, ignore
return f"``{klass}``"
link_type = klass
is_array = False
if link_type.endswith("[]"): # Typed array, strip [] to link to contained type.
link_type = link_type[:-2]
is_array = True
if link_type in state.classes:
type_rst = f":ref:`{link_type}<class_{link_type}>`"
if is_array:
type_rst = f":ref:`Array<class_Array>`\\[{type_rst}\\]"
return type_rst
# print_error(f'{state.current_class}.xml: Unresolved type "{link_type}".', state)
# type_rst = f"``{link_type}``"
type_rst = bitwes.make_type_link(link_type)
if is_array:
type_rst = f":ref:`Array<class_Array>`\\[{type_rst}\\]"
return type_rst
def make_enum(t: str, is_bitfield: bool, state: State) -> str:
p = t.find(".")
if p >= 0:
c = t[0:p]
e = t[p + 1 :]
# Variant enums live in GlobalScope but still use periods.
if c == "Variant":
c = "@GlobalScope"
e = "Variant." + e
else:
c = state.current_class
e = t
if c in state.classes and e not in state.classes[c].enums:
c = "@GlobalScope"
if c in state.classes and e in state.classes[c].enums:
if is_bitfield:
if not state.classes[c].enums[e].is_bitfield:
lgr.print_error(f'{state.current_class}.xml: Enum "{t}" is not bitfield.', state)
return f"|bitfield|\\[:ref:`{e}<enum_{c}_{e}>`\\]"
else:
return f":ref:`{e}<enum_{c}_{e}>`"
# Don't fail for `Vector3.Axis`, as this enum is a special case which is expected not to be resolved.
if f"{c}.{e}" != "Vector3.Axis":
return bitwes.make_type_link_for_part(c, e)
# lgr.print_error(f'{state.current_class}.xml: Unresolved enum "{t}".', state)
else:
return t