Browse Source

Add code sample language parity check to make_rst.py

Yuri Sizov 1 year ago
parent
commit
bba3c70722
1 changed files with 122 additions and 47 deletions
  1. 122 47
      doc/tools/make_rst.py

+ 122 - 47
doc/tools/make_rst.py

@@ -141,6 +141,9 @@ class State:
         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
@@ -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()
@@ -560,6 +566,65 @@ class ClassDef(DefinitionBase):
         # 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:
@@ -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())
@@ -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)
@@ -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:
@@ -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.
 
 
@@ -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"
@@ -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:
@@ -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
@@ -1776,6 +1840,8 @@ 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:
@@ -1783,8 +1849,17 @@ def format_text_block(
                             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