Coverage for src/renaissance/integrations/tree_sitter/lst.py: 91%
67 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: Lossless syntax tree (LST) node implementation backed by tree-sitter."""
9import sys
10from typing import Any, Self, cast
12from renaissance.integrations.tree_sitter.kinds import TREE_SITTER_KIND_MAP
13from renaissance.syntax_tree.semantic_kind import SemanticKind
14from renaissance.utils.ast_utils import format_node, match_children, match_props, next_sibling, preceding_sibling
16IRRELEVANT_PROPS = {"source_code", "end_point", "start_point", "location", "type"}
17IRRELEVANT_NODE = {"comment"}
20class LSTNode:
21 """AI: Lossless syntax tree (LST) node implementation backed by tree-sitter."""
23 def __init__(
24 self,
25 node_type: str,
26 properties: dict[str, Any],
27 signature: str,
28 offset: int = 0,
29 children: list[Self] | None = None,
30 parent: Self | None = None,
31 root: Self | None = None,
32 ) -> None:
33 """AI: Wrap a tree-sitter node as a language-syntax-tree node with derived semantic kind."""
34 self.root = root or self
35 self.parent = parent
36 self.children = [] if children is None else children
37 self.properties = properties
38 if node_type == "string" and signature.startswith("f"):
39 node_type = "FormattedString"
41 self.parser_kind = node_type
42 self.semantic_kind = TREE_SITTER_KIND_MAP.get(node_type, SemanticKind.NODE)
44 self.is_implicit = True
45 self.show_props = False
46 self.indent = ""
48 self.is_statement = node_type == "Expr"
49 self.referenced_by = []
50 self.references = []
52 self.signature = signature
53 self.text = signature
54 self.filename = "unknown"
55 self.length = len(signature)
56 self.offset = offset
57 self.end_offset = self.offset + self.length
58 self.extended_end_offset = self.end_offset
60 @property
61 def kind_key(self) -> SemanticKind | str:
62 """AI: Return the semantic kind, or the raw parser kind when no semantic kind applies."""
63 return self.semantic_kind if self.semantic_kind is not SemanticKind.NODE else self.parser_kind
65 def __eq__(self, other) -> bool:
66 """AI: Return whether this node is structurally equal to `other`, ignoring irrelevant properties/children."""
67 return (
68 isinstance(other, type(self))
69 and self.kind_key == other.kind_key
70 and match_props(self.properties, other.properties, IRRELEVANT_PROPS)
71 and match_children(self.children, other.children, IRRELEVANT_NODE)
72 )
74 def __hash__(self) -> int:
75 """AI: Return a hash based on the node's kind key, properties, and children."""
76 return hash((self.kind_key, frozenset(self.properties.items()), tuple(self.children)))
78 def match_props(self, properties) -> bool:
79 """AI: Return whether this node's properties match the given properties, ignoring irrelevant ones."""
80 all_keys = (self.properties.keys() | properties.keys()) - IRRELEVANT_PROPS
81 return all(self.properties.get(n) == properties.get(n) for n in all_keys)
83 def match_children(self, children):
84 """AI: Return whether this node's children match the given children at each corresponding index."""
85 return all(i < len(self.children) and self.children[i] == child for i, child in enumerate(children))
87 def add_child(self, child) -> None: # LSTNode):
88 """AI: Append child to this node's children and set its parent to this node."""
89 self.children.append(child)
90 child.parent = self
92 @property
93 def preceding_sibling(self) -> Self | None:
94 """AI: Return the sibling node immediately preceding this one, or None."""
95 return preceding_sibling(self)
97 @property
98 def next_sibling(self) -> Self | None:
99 """AI: Return the sibling node immediately following this one, or None."""
100 return next_sibling(self)
102 @property
103 def name(self) -> str:
104 """AI: Return this node's "name" property, or an empty string if absent."""
105 return self.properties.get("name", "")
107 def binary_file_content(self):
108 """AI: Return this node's source code encoded as bytes using the filesystem encoding."""
109 # TODO: see docs/developer/architecture/position-consistency.md - builds a per-node buffer, not the root's.
110 src = cast("str", self.properties.get("source_code"))
111 return src.encode(sys.getfilesystemencoding())
113 @property
114 def node(self):
115 """AI: Return this node itself."""
116 return self
118 def __repr__(self) -> str:
119 """AI: Return the formatted node representation."""
120 return format_node(self)
121 # raw_lines = self.signature.splitlines()
122 # properties_text = "" if not self.show_props else self.properties
123 # prefix = " " if len(raw_lines) < 2 else f"\n {self.indent}"
124 # formatted_lines = [f"{prefix}|{line}|" for line in raw_lines]
125 # return (
126 # f"{self.indent}({self.kind}, {self.name},"
127 # f" {self.filename}[{self.offset}:{self.offset + self.length}])"
128 # f"{properties_text}:{''.join(formatted_lines)}\n"
129 # )
131 def is_part_of_translation_unit(self):
132 """AI: Return whether this node belongs to a translation unit (has a root)."""
133 return self.root is not None
136class LST:
137 """AI: Hold the root node of a language-syntax tree."""
139 def __init__(self, root: LSTNode) -> None:
140 """AI: Hold the root node of a language-syntax tree."""
141 self.root = root