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

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 

6 

7"""AI: Processor that coordinates parsing, pattern matching, and rewriting of a single AST.""" 

8 

9from collections.abc import Callable, Iterator, Sequence 

10from pathlib import Path 

11from typing import Any 

12 

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 

21 

22 

23class ASTProcessor: 

24 """AI: Processor that coordinates parsing, pattern matching, and rewriting of a single AST.""" 

25 

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 

38 

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 

43 

44 @property 

45 def node(self) -> ASTNode: 

46 """AI: Return the root AST node this processor operates on.""" 

47 return self.__root_node 

48 

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() 

53 

54 @property 

55 def root(self) -> ASTNode: 

56 """AI: Return the root AST node this processor operates on.""" 

57 return self.__root_node 

58 

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) 

68 

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) 

77 

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) 

87 

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) 

97 

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) 

101 

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) 

105 

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 ) 

113 

114 def has_changed(self) -> bool: 

115 """AI: Return whether any rewrites have been queued.""" 

116 return self.__rewriter.has_changed() 

117 

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() 

121 

122 def commit(self) -> ASTProcessor: 

123 """Commit the current changes to the AST (Abstract Syntax Tree) and return a new ASTProcessor instance. 

124 

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. 

128 

129 Returns: 

130 ASTProcessor: A new instance of ASTProcessor with the updated AST. 

131 

132 Raises: 

133 IOError: If there is an error writing to the file. 

134 

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) 

140 

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) 

152 

153 

154# main 

155if __name__ == "__main__": 

156 

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 

162 

163 test("key", str)