Coverage for src/renaissance/syntax_tree/ast_refactor_actions.py: 88%
60 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: Higher-level refactoring actions built on top of pattern matching and rewriting."""
9from collections.abc import Callable, Sequence
10from functools import cache
11from typing import TYPE_CHECKING
13from .ast_finder import matches_node
14from .ast_node import ASTNode
15from .ast_processor import ASTProcessor
16from .match_finder import MatchFinder, PatternMatch
17from .semantic_kind import SemanticKind
19if TYPE_CHECKING:
20 from renaissance.integrations.clang.c_pattern_factory import CPPPatternFactory
23def _kind_predicate(kind):
24 if kind is None:
25 return lambda _node: False
26 if callable(kind):
27 return kind
28 if isinstance(kind, SemanticKind):
29 return lambda node: node.semantic_kind is kind
30 raise TypeError("kind must be a SemanticKind or a node predicate")
33class ASTRefactorActions:
34 """AI: Higher-level refactoring actions (replace, insert, remove) built on top of pattern matching and rewriting."""
36 def __init__(self, processor: ASTProcessor, pattern_factory: CPPPatternFactory) -> None:
37 """AI: Provide pattern-based refactoring actions (replace, insert, remove) over an AST."""
38 self.processor = processor
39 self.pattern_factory = pattern_factory
40 self.replaced: set[int] = set()
42 def replace_expr(self, name: str, replacement: str, kind: SemanticKind | Callable[[ASTNode], bool]) -> None:
43 """AI: Replace occurrences of the named expression matching kind with replacement."""
44 kind_predicate = _kind_predicate(kind)
46 def test(n: ASTNode):
47 if (kind and matches_node(n, kind_predicate)) and n.name == name:
48 yield n
50 [self.processor.replace(found.text.replace(found.name, replacement, 1), found) for found in self.processor.find_all(test)]
52 def replace_name(
53 self,
54 name: str,
55 replacement: str,
56 kind: SemanticKind | Callable[[ASTNode], bool] | None = None,
57 skip_kind: SemanticKind | Callable[[ASTNode], bool] | None = None,
58 ) -> None:
59 """AI: Replace occurrences of the named node (matching kind, excluding skip_kind) with replacement."""
60 kind_predicate = _kind_predicate(kind)
61 skip_kind_predicate = _kind_predicate(skip_kind)
63 def matches_name(n1: ASTNode) -> bool:
64 return (
65 (kind is None or matches_node(n1, kind_predicate))
66 and (skip_kind is None or not matches_node(n1, skip_kind_predicate))
67 and n1
68 and n1.name == name
69 )
71 found_nodes = self.processor.find_all(matches_name)
72 [self.replaced.add(found.offset) for found in found_nodes if found.offset not in self.replaced]
73 for n in found_nodes:
74 self.processor.replace(n.text.replace(n.name, replacement, 1), n)
76 def replace_text(
77 self,
78 text: str,
79 replacement: str,
80 kind: SemanticKind | Callable[[ASTNode], bool] | None = None,
81 skip_kind: SemanticKind | Callable[[ASTNode], bool] | None = None,
82 ) -> None:
83 """AI: Replace nodes whose text equals text (matching kind, excluding skip_kind) with replacement."""
84 kind_predicate = _kind_predicate(kind)
85 skip_kind_predicate = _kind_predicate(skip_kind)
87 def matches_text(n: ASTNode) -> bool:
88 return (
89 (kind is None or matches_node(n, kind_predicate))
90 and (skip_kind is None or not matches_node(n, skip_kind_predicate))
91 and n is not None
92 and n.text == text
93 )
95 found_nodes = self.processor.find_all(matches_text)
96 [self.replaced.add(found.offset) for found in found_nodes if found.offset not in self.replaced]
98 [self.processor.replace(n.text.replace(n.name, replacement, 1), n) for n in found_nodes]
100 def replace_declaration(self, declaration: str, replacement: str) -> None:
101 """AI: Replace every match of declaration with replacement."""
102 for match in self.find_declaration(declaration):
103 self.processor.replace(replacement, match)
105 def _replace_patterns(
106 self,
107 node: ASTNode,
108 replacement: str,
109 patterns: Sequence[Sequence[ASTNode]],
110 matches: Sequence[PatternMatch],
111 ) -> None:
112 if not patterns:
113 self.processor.replace(replacement, matches)
114 return
115 [
116 self._replace_patterns(m.nodes[0], replacement, patterns[1:], list(matches) + [m])
117 for m in MatchFinder.find_all([node], patterns[0])
118 ]
120 @cache
121 def find_declaration(self, decl_pattern: str):
122 """AI: Return the matches of decl_pattern parsed as a declaration pattern."""
123 pattern = self.pattern_factory.create_declaration(decl_pattern)
124 return self.processor.find_match(pattern)
126 @cache
127 def collect(self, pattern: str, pattern_kind: str):
128 """AI: Return the matches of pattern parsed as the given pattern_kind."""
129 root = self.pattern_factory.create(pattern, pattern_kind)
131 return self.processor.find_match(root)