Coverage for src/renaissance/utils/ast_utils.py: 95%
64 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-15 10:34 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-15 10:34 +0000
1# Copyright (C) 2024-2026 TNO
2#
3# Licensed under the terms specified in the LICENSE file in the root directory
4# of this repository, also available at:
5# https://github.com/TNO/Renaissance.Py/blob/main/LICENSE
7"""AI: Low-level AST utility helpers: sibling lookup, placeholder handling, and node matching."""
9from collections import deque
10from contextlib import contextmanager
11from contextvars import ContextVar
13from renaissance.integrations import MATCH_ALL, MATCH_ONE
15DISPLAY_PARSER_KIND: ContextVar[bool] = ContextVar("display_parser_kind", default=False)
18@contextmanager
19def display_context(display_parser_kind: bool):
20 """AI: Temporarily set whether node display uses parser kind instead of semantic kind."""
21 token = DISPLAY_PARSER_KIND.set(display_parser_kind)
22 try:
23 yield
24 finally:
25 DISPLAY_PARSER_KIND.reset(token)
28def replace_dollar(text: str) -> str:
29 """Replace dollar-sign placeholders in the given text with their corresponding match symbols.
31 Two literal dollar-sign characters (`$$`) are replaced with MATCH_ALL; a single literal dollar-sign
32 character (`$`) is replaced with MATCH_ONE.
33 """
34 return text.replace("$$", MATCH_ALL).replace("$", MATCH_ONE)
37def use_dollar(text: str) -> str:
38 """Replace match symbols in the given text with their corresponding dollar-sign placeholders.
40 MATCH_ALL is replaced with two literal dollar-sign characters (`$$`); MATCH_ONE is replaced with a
41 single literal dollar-sign character (`$`).
42 """
43 return text.replace(MATCH_ALL, "$$").replace(MATCH_ONE, "$")
46def detect_placeholder(signature: str, original_node_type: str) -> tuple[bool, str, str]:
47 """Detect if the given signature represents a placeholder symbol.
49 Returns:
50 (is_placeholder, coerced_node_type, placeholder_name_or_signature)
52 """
53 if not signature:
54 return False, original_node_type, ""
55 if signature.startswith((MATCH_ALL, "$$")) and " " not in signature and "(" not in signature:
56 # legacy compatibility
57 return True, MATCH_ALL, signature
58 if signature.startswith((MATCH_ONE, "$")) and " " not in signature and "(" not in signature:
59 return True, MATCH_ONE, signature
60 return False, original_node_type, "-"
63# duplicate of ast node process
64def traverse(node):
65 """AI: Yield node and all its descendants in breadth-first order."""
66 todo = deque([node])
67 while todo:
68 node = todo.popleft()
69 if hasattr(node, "children"):
70 todo.extend(node.children)
71 yield node
74def process_node(node, action) -> None:
75 """AI: Recursively apply action to node and all its descendants."""
76 action(node)
77 if node.children:
78 for child in node.children:
79 process_node(child, action)
82def preceding_sibling(node):
83 """AI: Return the sibling immediately before node in its parent's children, or None."""
84 parent = node.parent
85 if not parent:
86 return None
87 siblings = parent.children
88 preceding_index = siblings.index(node) - 1
89 return siblings[preceding_index] if preceding_index >= 0 else None
92def next_sibling(self):
93 """AI: Return the sibling immediately after self in its parent's children, or None."""
94 parent = self.parent
95 if not parent:
96 return None
97 siblings = parent.children
98 next_index = siblings.index(self) + 1
99 return siblings[next_index] if next_index < len(siblings) else None
102def match_props(mine, other, irrelevant_props) -> bool:
103 """AI: Return True if mine and other agree on all properties except the irrelevant ones."""
104 all_keys = (mine.keys() | other.keys()) - irrelevant_props
105 return all(mine.get(n) == other.get(n) for n in all_keys)
108def match_children(mine, other, irrelevant_kinds) -> bool:
109 """AI: Return True if every child in other matches the corresponding child in mine (or has an irrelevant kind)."""
110 if mine is None or other is None:
111 return mine == other
112 return all((i < len(mine) and mine[i] == child) or child.parser_kind in irrelevant_kinds for i, child in enumerate(other))
115def format_node(node) -> str:
116 """AI: Return a human-readable, indented display string for a single AST node."""
117 raw_lines = node.signature.splitlines()
118 properties_text = "" if not node.show_props else node.properties
119 prefix = " " if len(raw_lines) < 2 else f"\n {node.indent}"
120 formatted_lines = [f"{prefix}|{line}|" for line in raw_lines]
121 semantic_name = node.semantic_kind.value
122 kind = node.parser_kind if DISPLAY_PARSER_KIND.get() or semantic_name == "node" else semantic_name
123 return (
124 f"{node.indent}({kind}, {node.name}, "
125 f"{node.filename}[{node.offset}:{node.offset + node.length}])"
126 f"{properties_text}:{''.join(formatted_lines)}\n"
127 )