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

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: Higher-level refactoring actions built on top of pattern matching and rewriting.""" 

8 

9from collections.abc import Callable, Sequence 

10from functools import cache 

11from typing import TYPE_CHECKING 

12 

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 

18 

19if TYPE_CHECKING: 

20 from renaissance.integrations.clang.c_pattern_factory import CPPPatternFactory 

21 

22 

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

31 

32 

33class ASTRefactorActions: 

34 """AI: Higher-level refactoring actions (replace, insert, remove) built on top of pattern matching and rewriting.""" 

35 

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

41 

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) 

45 

46 def test(n: ASTNode): 

47 if (kind and matches_node(n, kind_predicate)) and n.name == name: 

48 yield n 

49 

50 [self.processor.replace(found.text.replace(found.name, replacement, 1), found) for found in self.processor.find_all(test)] 

51 

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) 

62 

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 ) 

70 

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) 

75 

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) 

86 

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 ) 

94 

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] 

97 

98 [self.processor.replace(n.text.replace(n.name, replacement, 1), n) for n in found_nodes] 

99 

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) 

104 

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 ] 

119 

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) 

125 

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) 

130 

131 return self.processor.find_match(root)