Coverage for src/renaissance/syntax_tree/ast_processor.py: 86%
69 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: Processor that coordinates parsing, pattern matching, and rewriting of a single AST."""
9from collections.abc import Callable, Iterator, Sequence
10from pathlib import Path
11from typing import Any
13import renaissance.syntax_tree.match_finder
14from renaissance.syntax_tree import ASTNode
15from renaissance.syntax_tree.ast_factory import ASTFactory
16from renaissance.syntax_tree.ast_finder import ASTFinder, find_semantic_kind
17from renaissance.syntax_tree.ast_rewriter import ASTRewriter
18from renaissance.syntax_tree.match_finder import PatternMatch
19from renaissance.syntax_tree.node_protocol import NodeProtocol
20from renaissance.syntax_tree.semantic_kind import SemanticKind
23class ASTProcessor:
24 """AI: Processor that coordinates parsing, pattern matching, and rewriting of a single AST."""
26 def __init__(
27 self,
28 root: ASTNode,
29 ast_factory: ASTFactory,
30 in_memory: bool = False,
31 ) -> None:
32 """AI: Prepare a processor that runs refactoring/analysis steps over the given AST."""
33 self.__root_node = root
34 self.__rewriter = ASTRewriter(root)
35 self.__ast_factory = ast_factory
36 self.in_memory = in_memory
37 self.repeat_step = 0
39 @property
40 def factory(self) -> ASTFactory:
41 """AI: Return the AST factory used to create nodes for this processor."""
42 return self.__ast_factory
44 @property
45 def node(self) -> ASTNode:
46 """AI: Return the root AST node this processor operates on."""
47 return self.__root_node
49 @property
50 def filename(self) -> str:
51 """AI: Return the filename of the AST this processor operates on."""
52 return self.__rewriter.get_filename()
54 @property
55 def root(self) -> ASTNode:
56 """AI: Return the root AST node this processor operates on."""
57 return self.__root_node
59 def replace(
60 self,
61 new_content: str,
62 target: ASTNode | Sequence[ASTNode] | PatternMatch | Sequence[PatternMatch],
63 include_whitespace: bool = True,
64 include_comments: bool = True,
65 ) -> None:
66 """AI: Queue a rewrite replacing target's source text with new_content."""
67 self.__rewriter.replace(new_content, target, include_whitespace, include_comments)
69 def remove(
70 self,
71 target: ASTNode | Sequence[ASTNode] | PatternMatch | Sequence[PatternMatch],
72 include_whitespace: bool = True,
73 include_comments: bool = True,
74 ) -> None:
75 """AI: Queue a rewrite removing target's source text."""
76 self.__rewriter.remove(target, include_whitespace, include_comments)
78 def insert_before(
79 self,
80 new_content: str,
81 target: ASTNode | Sequence[ASTNode] | PatternMatch | Sequence[PatternMatch],
82 include_whitespace: bool = True,
83 include_comments: bool = True,
84 ) -> None:
85 """AI: Queue a rewrite inserting new_content immediately before target's source text."""
86 self.__rewriter.insert_before(new_content, target, include_whitespace, include_comments)
88 def insert_after(
89 self,
90 new_content: str,
91 target: ASTNode | Sequence[ASTNode] | PatternMatch | Sequence[PatternMatch],
92 include_whitespace: bool = True,
93 include_comments: bool = True,
94 ) -> None:
95 """AI: Queue a rewrite inserting new_content immediately after target's source text."""
96 self.__rewriter.insert_after(new_content, target, include_whitespace, include_comments)
98 def find_all(self, function: Callable[[ASTNode], Iterator[ASTNode] | bool]) -> Sequence[ASTNode]:
99 """AI: Return all descendant nodes of the root for which function returns a truthy value or child iterator."""
100 return ASTFinder.find_all(self.__root_node, function)
102 def find_semantic_kind(self, kind: SemanticKind) -> Sequence[NodeProtocol]:
103 """AI: Return all descendant nodes of the root matching the given semantic kind."""
104 return find_semantic_kind(self.__root_node, kind)
106 def find_match(self, *patterns_list: Any, recursive: bool = True) -> Sequence[PatternMatch]:
107 """AI: Return matches of patterns_list against the root's children."""
108 return renaissance.syntax_tree.match_finder.find_all(
109 self.__root_node.children,
110 *patterns_list,
111 recursive=recursive,
112 )
114 def has_changed(self) -> bool:
115 """AI: Return whether any rewrites have been queued."""
116 return self.__rewriter.has_changed()
118 def apply_to_string(self) -> str:
119 """AI: Return the rewritten source as a string, applying all queued rewrites."""
120 return self.__rewriter.apply_to_string()
122 def commit(self) -> ASTProcessor:
123 """Commit the current changes to the AST (Abstract Syntax Tree) and return a new ASTProcessor instance.
125 This method applies the current changes to the source code and creates a new ASTProcessor instance
126 with the updated AST. If the changes are in-memory, it directly creates the new AST from the updated
127 code string. Otherwise, it writes the changes to the file, reloads the file, and then creates the new AST.
129 Returns:
130 ASTProcessor: A new instance of ASTProcessor with the updated AST.
132 Raises:
133 IOError: If there is an error writing to the file.
135 """
136 if not self.__rewriter.has_changed():
137 return self
138 self.__root_node, self.__rewriter = self._commit(self.__rewriter, self.__ast_factory, self.in_memory)
139 return ASTProcessor(self.__root_node, self.__ast_factory, self.in_memory)
141 @staticmethod
142 def _commit(rewriter: ASTRewriter, factory: ASTFactory, in_memory: bool = False) -> tuple[ASTNode[Any, Any], ASTRewriter]:
143 rewriter.apply_to_string()
144 if in_memory:
145 atu = factory.create_from_text(rewriter.apply_to_string(), rewriter.get_filename())
146 return atu, ASTRewriter(atu)
147 # save file first then reload it
148 with Path(rewriter.get_filename()).open("wb") as f:
149 f.write(rewriter.apply())
150 atu = factory.create(Path(rewriter.get_filename()))
151 return atu, ASTRewriter(atu)
154# main
155if __name__ == "__main__":
157 def test[T](_: str, factory: type[T]) -> T:
158 """AI: Smoke-test helper verifying a factory callable returns an instance of its own type."""
159 result = factory()
160 assert isinstance(result, factory)
161 return result
163 test("key", str)