Coverage for src/renaissance/syntax_tree/ast_finder.py: 69%
45 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: Helpers for finding AST nodes matching a predicate or semantic kind."""
9import re
10from collections.abc import Callable, Iterator, Sequence
12from renaissance.utils.ast_utils import traverse
14from .ast_node import ASTNode
15from .node_protocol import NodeProtocol
16from .semantic_kind import SemanticKind
19class ASTFinder:
20 """AI: Static helpers for finding AST nodes matching a predicate or semantic kind."""
22 KIND_MATCH = re.compile(r"[\W_]+")
24 @staticmethod
25 def find_all(ast_node: ASTNode, function: Callable[[ASTNode], Iterator[ASTNode] | bool]) -> Sequence[ASTNode]:
26 """AI: Return all descendant nodes of ast_node for which function returns a truthy value or child iterator."""
27 return list(ASTFinder.__find_all(ast_node, function))
29 # @staticmethod
30 # def find_kind(ast_node: ASTNode, kind: str | re.Pattern[str]) -> Sequence[ASTNode]:
31 # return list(ASTFinder.__matches_kind(ast_node, kind))
33 @staticmethod
34 def find(ast_node: ASTNode, kind: str | re.Pattern[str]) -> Sequence[ASTNode]:
35 """AI: Return all descendant nodes of ast_node whose kind matches the given kind pattern."""
36 return list(ASTFinder.__matches_kind(ast_node, kind))
38 @staticmethod
39 def matches_kind(ast_node: ASTNode | None, kind: str | re.Pattern[str]) -> bool:
40 """AI: Return whether ast_node's kind matches the given kind pattern."""
41 # compare kind with the ast_node kind only using word characters
42 # get kind of the ast_node with only word characters
43 if ast_node is None:
44 return False
45 ast_kind = ASTFinder.KIND_MATCH.sub("", ast_node.kind).lower()
46 pattern = kind if isinstance(kind, re.Pattern) else re.compile(kind, re.IGNORECASE)
47 return pattern.fullmatch(ast_kind) is not None
49 @staticmethod
50 def __find_all(ast_node: ASTNode, function: Callable[[ASTNode], Iterator[ASTNode] | bool]) -> Iterator[ASTNode]:
51 result = function(ast_node)
52 if isinstance(result, bool) and result:
53 yield ast_node
54 elif isinstance(result, Iterator):
55 yield from result
56 for child in ast_node.children:
57 yield from ASTFinder.__find_all(child, function)
59 @staticmethod
60 def __matches_kind(ast_node: ASTNode, kind: str | re.Pattern[str]) -> Iterator[ASTNode]:
61 pattern = kind if isinstance(kind, re.Pattern) else re.compile(kind, re.IGNORECASE)
62 node_kind = ast_node.kind or ""
63 ast_kind = ASTFinder.KIND_MATCH.sub("", node_kind).lower()
65 if pattern.fullmatch(ast_kind):
66 yield ast_node
67 for child in ast_node.children:
68 # assert isinstance(child, type(ast_node)), f'Expected {type(ast_node)} but got {type(child)}'
69 yield from ASTFinder.__matches_kind(child, pattern)
72def find_nodes(ast_node: NodeProtocol, predicate) -> Sequence[NodeProtocol]:
73 """AI: Return all descendants (and ast_node itself) matching predicate, via a full traversal."""
74 return [node for node in traverse(ast_node) if predicate(node)]
77def matches_node(ast_node: NodeProtocol, predicate) -> bool:
78 """AI: Return True if ast_node itself satisfies predicate."""
79 return predicate(ast_node)
82def find_semantic_kind(ast_node: NodeProtocol, kind: SemanticKind) -> Sequence[NodeProtocol]:
83 """AI: Return all nodes under ast_node whose semantic kind matches the given kind."""
84 return find_nodes(ast_node, lambda node: node.semantic_kind is kind)