Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add code sample language parity check to make_rst.py #86971

Merged
merged 1 commit into from
Jan 16, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 122 additions & 47 deletions doc/tools/make_rst.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ def __init__(self) -> None:
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:
class_name = class_root.attrib["name"]
self.current_class = class_name
Expand Down Expand Up @@ -543,6 +546,9 @@ 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()
Expand All @@ -560,6 +566,65 @@ def __init__(self, name: str) -> None:
# Used to match the class with XML source for output filtering purposes.
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.
def main() -> None:
Expand Down Expand Up @@ -590,6 +655,11 @@ def main() -> None:
action="store_true",
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()

should_color = args.color or (hasattr(sys.stdout, "isatty") and sys.stdout.isatty())
Expand Down Expand Up @@ -684,15 +754,15 @@ def main() -> None:
if args.filter and not pattern.search(class_def.filepath):
continue
state.current_class = class_name
make_rst_class(class_def, state, args.dry_run, args.output)

group_name = get_class_group(class_def, state)
class_def.update_class_group(state)
make_rst_class(class_def, state, args.dry_run, args.output)

if group_name not in grouped_classes:
grouped_classes[group_name] = []
grouped_classes[group_name].append(class_name)
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 is_editor_class(class_def):
if class_def.editor_class:
if "editor" not in grouped_classes:
grouped_classes["editor"] = []
grouped_classes["editor"].append(class_name)
Expand All @@ -704,6 +774,26 @@ def main() -> None:

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.

if state.num_warnings >= 2:
Expand Down Expand Up @@ -760,46 +850,6 @@ def get_git_branch() -> str:
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.


Expand Down Expand Up @@ -1642,7 +1692,7 @@ def parse_link_target(link_target: str, state: State, context_name: str) -> List

def format_text_block(
text: str,
context: Union[DefinitionBase, None],
context: DefinitionBase,
state: State,
) -> str:
# Linebreak + tabs in the XML should become two line breaks unless in a "codeblock"
Expand Down Expand Up @@ -1692,6 +1742,9 @@ def format_text_block(
inside_code_tabs = False
ignore_code_warnings = False

has_codeblocks_gdscript = False
has_codeblocks_csharp = False

pos = 0
tag_depth = 0
while True:
Expand Down Expand Up @@ -1759,6 +1812,17 @@ def format_text_block(

elif tag_state.name == "codeblocks":
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_text = ""
inside_code_tabs = False
Expand All @@ -1776,15 +1840,26 @@ def format_text_block(
f"{state.current_class}.xml: GDScript code block is used outside of [codeblocks] in {context_name}.",
state,
)
else:
has_codeblocks_gdscript = True
tag_text = "\n .. code-tab:: gdscript\n"
elif tag_state.name == "csharp":
if not inside_code_tabs:
print_error(
f"{state.current_class}.xml: C# code block is used outside of [codeblocks] in {context_name}.",
state,
)
else:
has_codeblocks_csharp = True
tag_text = "\n .. code-tab:: csharp\n"
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"

inside_code = True
Expand Down
Loading