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

85 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-10-02 13:12 +0000

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

2 

3from pathlib import Path 

4from typing import Self 

5 

6import libcst 

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

8from libcst.metadata import WhitespaceInclusivePositionProvider 

9 

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

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

12from renaissance.syntax_tree.semantic_kind import SemanticKind 

13from renaissance.utils.ast_utils import next_sibling, preceding_sibling 

14 

15 

16class PythonCstTranslationUnit: 

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

18 

19 def __init__(self, content, file_name: str): 

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

21 self.content = content 

22 self.lines = content.splitlines() 

23 self.file_name = file_name 

24 self.references_initialized = False 

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

26 self.atu = self.wrapper.module 

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

28 

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

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

31 span = self.spans.get(node) 

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

33 

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

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

36 span = self.spans.get(node) 

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

38 

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

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

41 try: 

42 return self.atu.code_for_node(node) 

43 except Exception: 

44 return "" 

45 

46 

47class PythonCstNode: 

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

49 

50 def __init__(self, node: CSTNode, translation_unit: PythonCstTranslationUnit, parent=None): 

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

52 self.parent = parent 

53 if parent and parent.root: 

54 self.root = parent.root 

55 else: 

56 self.root = self 

57 self.translation_unit = translation_unit 

58 self.node = node 

59 

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

61 self.parser_kind = type(node).__name__ 

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

63 

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

65 self.properties = {} 

66 

67 # for shower 

68 self.is_implicit = True 

69 self.show_props = False 

70 

71 # for rewriter 

72 self.text = self.signature 

73 

74 def __str__(self): 

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

76 return str(self.node) 

77 

78 def __repr__(self): 

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

80 return repr(self.node) 

81 

82 @property 

83 def signature(self): 

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

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

86 

87 @property 

88 def offset(self): 

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

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

91 

92 @property 

93 def length(self): 

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

95 return self.end_offset - self.offset 

96 

97 @property 

98 def end_offset(self): 

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

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

101 

102 @property 

103 def filename(self): 

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

105 return self.translation_unit.file_name 

106 

107 @property 

108 def name(self): 

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

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

111 return self.node.name.value 

112 return "" 

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

114 

115 @property 

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

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

118 return next_sibling(self) 

119 

120 @property 

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

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

123 return preceding_sibling(self) 

124 

125 @staticmethod 

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

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

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

129 content = file.read() 

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

131 

132 @staticmethod 

133 def load_from_text( 

134 text: str, 

135 file_name: str = "cst_snippet.py", 

136 ) -> PythonCstNode: 

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

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

139 root_node = PythonCstNode(translation_unit.atu, translation_unit) 

140 return root_node