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

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: Lossless syntax tree (LST) node implementation backed by tree-sitter.""" 

8 

9import sys 

10from typing import Any, Self, cast 

11 

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 

15 

16IRRELEVANT_PROPS = {"source_code", "end_point", "start_point", "location", "type"} 

17IRRELEVANT_NODE = {"comment"} 

18 

19 

20class LSTNode: 

21 """AI: Lossless syntax tree (LST) node implementation backed by tree-sitter.""" 

22 

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" 

40 

41 self.parser_kind = node_type 

42 self.semantic_kind = TREE_SITTER_KIND_MAP.get(node_type, SemanticKind.NODE) 

43 

44 self.is_implicit = True 

45 self.show_props = False 

46 self.indent = "" 

47 

48 self.is_statement = node_type == "Expr" 

49 self.referenced_by = [] 

50 self.references = [] 

51 

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 

59 

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 

64 

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 ) 

73 

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))) 

77 

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) 

82 

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)) 

86 

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 

91 

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) 

96 

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) 

101 

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", "") 

106 

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()) 

112 

113 @property 

114 def node(self): 

115 """AI: Return this node itself.""" 

116 return self 

117 

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 # ) 

130 

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 

134 

135 

136class LST: 

137 """AI: Hold the root node of a language-syntax tree.""" 

138 

139 def __init__(self, root: LSTNode) -> None: 

140 """AI: Hold the root node of a language-syntax tree.""" 

141 self.root = root