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
« 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
7"""AI: ASTNode implementation backed by libcst's concrete syntax tree."""
9from pathlib import Path
10from typing import Self
12import libcst
13from libcst import BaseCompoundStatement, BaseSmallStatement, ClassDef, CSTNode, FunctionDef, MetadataWrapper
14from libcst.metadata import WhitespaceInclusivePositionProvider
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
22class PythonCstTranslationUnit:
23 """AI: Parse Python source into a libcst tree with position lookups for AST-node wrapping."""
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)
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
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
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 ""
54class PythonCstNode:
55 """AI: ASTNode implementation backed by libcst's concrete syntax tree."""
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
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)
71 self.children: list[Self] = [PythonCstNode(node, translation_unit, self) for node in node.children]
72 self.properties = {}
74 # for shower
75 self.is_implicit = True
76 self.show_props = False
78 # for rewriter
79 self.text = self.signature
81 def __str__(self) -> str:
82 """AI: Return the string representation of the wrapped CST node."""
83 return str(self.node)
85 def __repr__(self) -> str:
86 """AI: Return the repr of the wrapped CST node."""
87 return repr(self.node)
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)
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)
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
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)
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
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
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()
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)
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)
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))
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