Add code sample language parity check to make_rst.py
This commit is contained in:
parent
9e65c5c0f4
commit
bba3c70722
|
@ -141,6 +141,9 @@ class State:
|
||||||
self.classes: OrderedDict[str, ClassDef] = OrderedDict()
|
self.classes: OrderedDict[str, ClassDef] = OrderedDict()
|
||||||
self.current_class: str = ""
|
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:
|
def parse_class(self, class_root: ET.Element, filepath: str) -> None:
|
||||||
class_name = class_root.attrib["name"]
|
class_name = class_root.attrib["name"]
|
||||||
self.current_class = class_name
|
self.current_class = class_name
|
||||||
|
@ -543,6 +546,9 @@ class ClassDef(DefinitionBase):
|
||||||
def __init__(self, name: str) -> None:
|
def __init__(self, name: str) -> None:
|
||||||
super().__init__("class", name)
|
super().__init__("class", name)
|
||||||
|
|
||||||
|
self.class_group = "variant"
|
||||||
|
self.editor_class = self._is_editor_class()
|
||||||
|
|
||||||
self.constants: OrderedDict[str, ConstantDef] = OrderedDict()
|
self.constants: OrderedDict[str, ConstantDef] = OrderedDict()
|
||||||
self.enums: OrderedDict[str, EnumDef] = OrderedDict()
|
self.enums: OrderedDict[str, EnumDef] = OrderedDict()
|
||||||
self.properties: OrderedDict[str, PropertyDef] = OrderedDict()
|
self.properties: OrderedDict[str, PropertyDef] = OrderedDict()
|
||||||
|
@ -560,6 +566,65 @@ class ClassDef(DefinitionBase):
|
||||||
# Used to match the class with XML source for output filtering purposes.
|
# Used to match the class with XML source for output filtering purposes.
|
||||||
self.filepath: str = ""
|
self.filepath: str = ""
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
# 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))
|
||||||
|
|
||||||
|
|
||||||
# Entry point for the RST generator.
|
# Entry point for the RST generator.
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
|
@ -590,6 +655,11 @@ def main() -> None:
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="If passed, no output will be generated and XML files are only checked for errors.",
|
help="If passed, no output will be generated and XML files are only checked for errors.",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--verbose",
|
||||||
|
action="store_true",
|
||||||
|
help="If passed, enables verbose printing.",
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
should_color = args.color or (hasattr(sys.stdout, "isatty") and sys.stdout.isatty())
|
should_color = args.color or (hasattr(sys.stdout, "isatty") and sys.stdout.isatty())
|
||||||
|
@ -684,15 +754,15 @@ def main() -> None:
|
||||||
if args.filter and not pattern.search(class_def.filepath):
|
if args.filter and not pattern.search(class_def.filepath):
|
||||||
continue
|
continue
|
||||||
state.current_class = class_name
|
state.current_class = class_name
|
||||||
|
|
||||||
|
class_def.update_class_group(state)
|
||||||
make_rst_class(class_def, state, args.dry_run, args.output)
|
make_rst_class(class_def, state, args.dry_run, args.output)
|
||||||
|
|
||||||
group_name = get_class_group(class_def, state)
|
if class_def.class_group not in grouped_classes:
|
||||||
|
grouped_classes[class_def.class_group] = []
|
||||||
|
grouped_classes[class_def.class_group].append(class_name)
|
||||||
|
|
||||||
if group_name not in grouped_classes:
|
if class_def.editor_class:
|
||||||
grouped_classes[group_name] = []
|
|
||||||
grouped_classes[group_name].append(class_name)
|
|
||||||
|
|
||||||
if is_editor_class(class_def):
|
|
||||||
if "editor" not in grouped_classes:
|
if "editor" not in grouped_classes:
|
||||||
grouped_classes["editor"] = []
|
grouped_classes["editor"] = []
|
||||||
grouped_classes["editor"].append(class_name)
|
grouped_classes["editor"].append(class_name)
|
||||||
|
@ -704,6 +774,26 @@ def main() -> None:
|
||||||
|
|
||||||
print("")
|
print("")
|
||||||
|
|
||||||
|
# Print out checks.
|
||||||
|
|
||||||
|
if state.script_language_parity_check.hit_count > 0:
|
||||||
|
if not args.verbose:
|
||||||
|
print(
|
||||||
|
f'{STYLES["yellow"]}{state.script_language_parity_check.hit_count} code samples failed parity check. Use --verbose to get more information.{STYLES["reset"]}'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f'{STYLES["yellow"]}{state.script_language_parity_check.hit_count} code samples failed parity check:{STYLES["reset"]}'
|
||||||
|
)
|
||||||
|
|
||||||
|
for class_name in state.script_language_parity_check.hit_map.keys():
|
||||||
|
class_hits = state.script_language_parity_check.hit_map[class_name]
|
||||||
|
print(f'{STYLES["yellow"]}- {len(class_hits)} hits in class "{class_name}"{STYLES["reset"]}')
|
||||||
|
|
||||||
|
for context, error in class_hits:
|
||||||
|
print(f" - {error} in {format_context_name(context)}")
|
||||||
|
print("")
|
||||||
|
|
||||||
# Print out warnings and errors, or lack thereof, and exit with an appropriate code.
|
# Print out warnings and errors, or lack thereof, and exit with an appropriate code.
|
||||||
|
|
||||||
if state.num_warnings >= 2:
|
if state.num_warnings >= 2:
|
||||||
|
@ -760,46 +850,6 @@ def get_git_branch() -> str:
|
||||||
return "master"
|
return "master"
|
||||||
|
|
||||||
|
|
||||||
def get_class_group(class_def: ClassDef, state: State) -> str:
|
|
||||||
group_name = "variant"
|
|
||||||
class_name = class_def.name
|
|
||||||
|
|
||||||
if class_name.startswith("@"):
|
|
||||||
group_name = "global"
|
|
||||||
elif class_def.inherits:
|
|
||||||
inherits = class_def.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
|
|
||||||
|
|
||||||
return group_name
|
|
||||||
|
|
||||||
|
|
||||||
def is_editor_class(class_def: ClassDef) -> bool:
|
|
||||||
class_name = class_def.name
|
|
||||||
|
|
||||||
if class_name.startswith("Editor"):
|
|
||||||
return True
|
|
||||||
if class_name in EDITOR_CLASSES:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
# Generator methods.
|
# Generator methods.
|
||||||
|
|
||||||
|
|
||||||
|
@ -1642,7 +1692,7 @@ def parse_link_target(link_target: str, state: State, context_name: str) -> List
|
||||||
|
|
||||||
def format_text_block(
|
def format_text_block(
|
||||||
text: str,
|
text: str,
|
||||||
context: Union[DefinitionBase, None],
|
context: DefinitionBase,
|
||||||
state: State,
|
state: State,
|
||||||
) -> str:
|
) -> str:
|
||||||
# Linebreak + tabs in the XML should become two line breaks unless in a "codeblock"
|
# Linebreak + tabs in the XML should become two line breaks unless in a "codeblock"
|
||||||
|
@ -1692,6 +1742,9 @@ def format_text_block(
|
||||||
inside_code_tabs = False
|
inside_code_tabs = False
|
||||||
ignore_code_warnings = False
|
ignore_code_warnings = False
|
||||||
|
|
||||||
|
has_codeblocks_gdscript = False
|
||||||
|
has_codeblocks_csharp = False
|
||||||
|
|
||||||
pos = 0
|
pos = 0
|
||||||
tag_depth = 0
|
tag_depth = 0
|
||||||
while True:
|
while True:
|
||||||
|
@ -1759,6 +1812,17 @@ def format_text_block(
|
||||||
|
|
||||||
elif tag_state.name == "codeblocks":
|
elif tag_state.name == "codeblocks":
|
||||||
if tag_state.closing:
|
if tag_state.closing:
|
||||||
|
if not has_codeblocks_gdscript or not has_codeblocks_csharp:
|
||||||
|
state.script_language_parity_check.add_hit(
|
||||||
|
state.current_class,
|
||||||
|
context,
|
||||||
|
"Only one script language sample found in [codeblocks]",
|
||||||
|
state,
|
||||||
|
)
|
||||||
|
|
||||||
|
has_codeblocks_gdscript = False
|
||||||
|
has_codeblocks_csharp = False
|
||||||
|
|
||||||
tag_depth -= 1
|
tag_depth -= 1
|
||||||
tag_text = ""
|
tag_text = ""
|
||||||
inside_code_tabs = False
|
inside_code_tabs = False
|
||||||
|
@ -1776,6 +1840,8 @@ def format_text_block(
|
||||||
f"{state.current_class}.xml: GDScript code block is used outside of [codeblocks] in {context_name}.",
|
f"{state.current_class}.xml: GDScript code block is used outside of [codeblocks] in {context_name}.",
|
||||||
state,
|
state,
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
has_codeblocks_gdscript = True
|
||||||
tag_text = "\n .. code-tab:: gdscript\n"
|
tag_text = "\n .. code-tab:: gdscript\n"
|
||||||
elif tag_state.name == "csharp":
|
elif tag_state.name == "csharp":
|
||||||
if not inside_code_tabs:
|
if not inside_code_tabs:
|
||||||
|
@ -1783,8 +1849,17 @@ def format_text_block(
|
||||||
f"{state.current_class}.xml: C# code block is used outside of [codeblocks] in {context_name}.",
|
f"{state.current_class}.xml: C# code block is used outside of [codeblocks] in {context_name}.",
|
||||||
state,
|
state,
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
has_codeblocks_csharp = True
|
||||||
tag_text = "\n .. code-tab:: csharp\n"
|
tag_text = "\n .. code-tab:: csharp\n"
|
||||||
else:
|
else:
|
||||||
|
state.script_language_parity_check.add_hit(
|
||||||
|
state.current_class,
|
||||||
|
context,
|
||||||
|
"Code sample is formatted with [codeblock] where [codeblocks] should be used",
|
||||||
|
state,
|
||||||
|
)
|
||||||
|
|
||||||
tag_text = "\n::\n"
|
tag_text = "\n::\n"
|
||||||
|
|
||||||
inside_code = True
|
inside_code = True
|
||||||
|
|
Loading…
Reference in New Issue