Coverage for src/renaissance/integrations/python/ast/cst_node.py: 99%

88 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: ASTNode implementation backed by libcst's concrete syntax tree.""" 

8 

9from pathlib import Path 

10from typing import Self 

11 

12import libcst 

13from libcst import BaseCompoundStatement, BaseSmallStatement, ClassDef, CSTNode, FunctionDef, MetadataWrapper 

14from libcst.metadata import WhitespaceInclusivePositionProvider 

15 

16from renaissance.integrations.python.ast.kinds import PYTHON_KIND_MAP 

17from renaissance.integrations.python.ast.util import convert 

18from renaissance.syntax_tree.semantic_kind import SemanticKind 

19from renaissance.utils.ast_utils import next_sibling, preceding_sibling 

20 

21 

22class PythonCstTranslationUnit: 

23 """AI: Parse Python source into a libcst tree with position lookups for AST-node wrapping.""" 

24 

25 def __init__(self, content, file_name: str) -> None: 

26 """AI: Parse Python source into a libcst tree with position lookups for AST-node wrapping.""" 

27 # TODO: see docs/developer/architecture/position-consistency.md - exposes no buffer, so the rewriter derives its own. 

28 self.content = content 

29 self.lines = content.splitlines() 

30 self.file_name = file_name 

31 self.references_initialized = False 

32 self.wrapper = MetadataWrapper(libcst.parse_module(content)) 

33 self.atu = self.wrapper.module 

34 self.spans = self.wrapper.resolve(WhitespaceInclusivePositionProvider) 

35 

36 def start_of(self, node: CSTNode) -> int: 

37 """AI: Return the character offset where node begins in the source text.""" 

38 span = self.spans.get(node) 

39 return convert(self.lines, span.start.line, span.start.column) if span else 0 

40 

41 def end_of(self, node: CSTNode) -> int: 

42 """AI: Return the character offset where node ends in the source text.""" 

43 span = self.spans.get(node) 

44 return convert(self.lines, span.end.line, span.end.column) if span else 0 

45 

46 def signature_of(self, node: CSTNode) -> str: 

47 """AI: Return the source code text corresponding to node, or an empty string on failure.""" 

48 try: 

49 return self.atu.code_for_node(node) 

50 except Exception: 

51 return "" 

52 

53 

54class PythonCstNode: 

55 """AI: ASTNode implementation backed by libcst's concrete syntax tree.""" 

56 

57 def __init__(self, node: CSTNode, translation_unit: PythonCstTranslationUnit, parent=None) -> None: 

58 """AI: Wrap a libcst node as an AST node within the given translation unit.""" 

59 self.parent = parent 

60 if parent and parent.root: 

61 self.root = parent.root 

62 else: 

63 self.root = self 

64 self.translation_unit = translation_unit 

65 self.node = node 

66 

67 self.is_statement = isinstance(self.node, (BaseSmallStatement, BaseCompoundStatement)) 

68 self.parser_kind = type(node).__name__ 

69 self.semantic_kind = PYTHON_KIND_MAP.get(self.parser_kind, SemanticKind.NODE) 

70 

71 self.children: list[Self] = [PythonCstNode(node, translation_unit, self) for node in node.children] 

72 self.properties = {} 

73 

74 # for shower 

75 self.is_implicit = True 

76 self.show_props = False 

77 

78 # for rewriter 

79 self.text = self.signature 

80 

81 def __str__(self) -> str: 

82 """AI: Return the string representation of the wrapped CST node.""" 

83 return str(self.node) 

84 

85 def __repr__(self) -> str: 

86 """AI: Return the repr of the wrapped CST node.""" 

87 return repr(self.node) 

88 

89 @property 

90 def signature(self): 

91 """AI: Return the source code text of the wrapped CST node.""" 

92 return self.translation_unit.signature_of(self.node) 

93 

94 @property 

95 def offset(self): 

96 """AI: Return the character offset where this node begins in the source text.""" 

97 return self.translation_unit.start_of(self.node) 

98 

99 @property 

100 def length(self): 

101 """AI: Return the length in characters of this node's source text.""" 

102 return self.end_offset - self.offset 

103 

104 @property 

105 def end_offset(self): 

106 """AI: Return the character offset where this node ends in the source text.""" 

107 return self.translation_unit.end_of(self.node) 

108 

109 @property 

110 def extended_end_offset(self) -> int: 

111 """AI: Return the end offset; libcst attaches trailing trivia to the node itself, so there is nothing to extend.""" 

112 return self.end_offset 

113 

114 @property 

115 def filename(self): 

116 """AI: Return the source file name of this node's translation unit.""" 

117 return self.translation_unit.file_name 

118 

119 @property 

120 def name(self): 

121 """AI: Return this node's class/function name, or an empty string if not applicable.""" 

122 if isinstance(self.node, (ClassDef, FunctionDef)): 

123 return self.node.name.value 

124 return "" 

125 self.name = "" # self._derive_name() 

126 

127 @property 

128 def next_sibling(self) -> Self | None: 

129 """AI: Return the sibling node immediately following this one, or None.""" 

130 return next_sibling(self) 

131 

132 @property 

133 def preceding_sibling(self) -> Self | None: 

134 """AI: Return the sibling node immediately preceding this one, or None.""" 

135 return preceding_sibling(self) 

136 

137 @staticmethod 

138 def load(file_path: Path) -> PythonCstNode: 

139 """AI: Parse the Python source file at file_path into a PythonCstNode tree.""" 

140 with Path(file_path).open() as file: 

141 content = file.read() 

142 return PythonCstNode.load_from_text(content, str(file_path)) 

143 

144 @staticmethod 

145 def load_from_text( 

146 text: str, 

147 file_name: str = "cst_snippet.py", 

148 ) -> PythonCstNode: 

149 """AI: Parse Python source text into a PythonCstNode tree, attributed to file_name.""" 

150 translation_unit = PythonCstTranslationUnit(text, file_name=str(file_name)) 

151 root_node = PythonCstNode(translation_unit.atu, translation_unit) 

152 return root_node