import ast import sys class ImGuiScopeLinter: def __init__(self): self.pairs = { 'begin': 'end', 'begin_child': 'end_child', 'begin_group': 'end_group', 'begin_combo': 'end_combo', 'begin_main_menu_bar': 'end_main_menu_bar', 'begin_menu_bar': 'end_menu_bar', 'begin_menu': 'end_menu', 'begin_tooltip': 'end_tooltip', 'begin_popup': 'end_popup', 'begin_popup_modal': 'end_popup', 'begin_popup_context_item': 'end_popup', 'begin_popup_context_window': 'end_popup', 'begin_popup_context_void': 'end_popup', 'begin_table': 'end_table', 'begin_tab_bar': 'end_tab_bar', 'begin_tab_item': 'end_tab_item', 'push_style_var': 'pop_style_var', 'push_style_color': 'pop_style_color', 'push_font': 'pop_font', 'push_id': 'pop_id', 'push_item_width': 'pop_item_width', 'push_text_wrap_pos': 'pop_text_wrap_pos', 'push_clip_rect': 'pop_clip_rect', 'tree_node': 'tree_pop', 'tree_push': 'tree_pop', 'indent': 'unindent', } self.starts = set(self.pairs.keys()) self.ends = set(self.pairs.values()) def check_source(self, source_code: str) -> list[str]: try: tree = ast.parse(source_code) except SyntaxError as e: return [f"Syntax error: {e}"] visitor = ImGuiVisitor(self.pairs) visitor.visit(tree) return visitor.errors class ImGuiVisitor(ast.NodeVisitor): def __init__(self, pairs): self.pairs = pairs self.stack = [] self.errors = [] self.starts = set(pairs.keys()) self.ends = set(pairs.values()) def visit_FunctionDef(self, node): saved_stack = self.stack self.stack = [] self.generic_visit(node) while self.stack: start_type, lineno = self.stack.pop() self.errors.append(f"Function '{node.name}': Unclosed scope '{start_type}' started at line {lineno}") self.stack = saved_stack def visit_AsyncFunctionDef(self, node): self.visit_FunctionDef(node) def visit_Call(self, node): func_name = self._get_func_name(node.func) if func_name: parts = func_name.split('.') if len(parts) >= 2 and parts[0] in ('imgui', 'ed', 'imgui_node_editor'): method = parts[-1] if method in self.starts: self.stack.append((method, node.lineno)) elif method in self.ends: if not self.stack: self.errors.append(f"Extra '{method}' at line {node.lineno} (no matching start)") else: start_type, start_lineno = self.stack[-1] expected_end = self.pairs.get(start_type) if expected_end == method: self.stack.pop() else: self.stack.pop() self.errors.append(f"Mismatched scope: '{method}' at line {node.lineno} does not match '{start_type}' at line {start_lineno}") self.generic_visit(node) def _get_func_name(self, node): if isinstance(node, ast.Name): return node.id elif isinstance(node, ast.Attribute): value = self._get_func_name(node.value) if value: return f"{value}.{node.attr}" return None def main(): if len(sys.argv) < 2: print("Usage: python check_imgui_scopes.py ...") return linter = ImGuiScopeLinter() for path in sys.argv[1:]: try: with open(path, "r", encoding="utf-8") as f: source = f.read() errors = linter.check_source(source) if errors: print(f"Errors in {path}:") for err in errors: print(f" {err}") else: print(f"{path}: OK") except Exception as e: print(f"Error reading {path}: {e}") if __name__ == "__main__": main()