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
« 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."""
3from pathlib import Path
4from typing import Self
6import libcst
7from libcst import BaseCompoundStatement, BaseSmallStatement, ClassDef, CSTNode, FunctionDef, MetadataWrapper
8from libcst.metadata import WhitespaceInclusivePositionProvider
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
16class PythonCstTranslationUnit:
17 """AI: Parse Python source into a libcst tree with position lookups for AST-node wrapping."""
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)
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
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
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 ""
47class PythonCstNode:
48 """AI: ASTNode implementation backed by libcst's concrete syntax tree."""
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
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)
64 self.children: list[Self] = [PythonCstNode(node, translation_unit, self) for node in node.children]
65 self.properties = {}
67 # for shower
68 self.is_implicit = True
69 self.show_props = False
71 # for rewriter
72 self.text = self.signature
74 def __str__(self):
75 """AI: Return the string representation of the wrapped CST node."""
76 return str(self.node)
78 def __repr__(self):
79 """AI: Return the repr of the wrapped CST node."""
80 return repr(self.node)
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)
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)
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
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)
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
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()
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)
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)
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))
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