Skip to main content

semantic/parser/
syntax_index.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Compact syntax index derived from a tree-sitter parse tree.
3
4use std::ops::Range;
5
6use tree_sitter::Node;
7
8use super::{
9    parser_language::Language,
10    parser_types::{FunctionDef, Import, ImportKind},
11};
12
13/// Compact Heddle-owned syntax data for one parsed source file.
14#[derive(Debug)]
15pub struct SyntaxIndex {
16    functions: Vec<IndexedFunction>,
17    imports: Vec<IndexedImport>,
18    line_offsets: Vec<usize>,
19}
20
21/// Borrowed view of a function indexed in a [`SyntaxIndex`].
22#[derive(Clone, Copy, Debug)]
23pub struct FunctionRef<'a> {
24    inner: &'a IndexedFunction,
25    source: &'a str,
26}
27
28/// Borrowed view of an import indexed in a [`SyntaxIndex`].
29#[derive(Clone, Copy, Debug)]
30pub struct ImportRef<'a> {
31    inner: &'a IndexedImport,
32    source: &'a str,
33}
34
35#[derive(Debug)]
36struct IndexedFunction {
37    name: String,
38    signature: String,
39    start_line: usize,
40    end_line: usize,
41    content: Range<usize>,
42}
43
44#[derive(Debug)]
45struct IndexedImport {
46    raw: Range<usize>,
47    kind: ImportKind,
48}
49
50impl SyntaxIndex {
51    pub(super) fn build(language: Language, source: &str, root: Node<'_>) -> Self {
52        let mut index = Self {
53            functions: Vec::new(),
54            imports: Vec::new(),
55            line_offsets: line_offsets(source),
56        };
57
58        let mut stack = vec![root];
59        while let Some(node) = stack.pop() {
60            if is_function_node(&node, language)
61                && let Some(name) = function_name(&node, source)
62            {
63                index.functions.push(IndexedFunction {
64                    name: name.to_string(),
65                    signature: function_signature(&node, source),
66                    start_line: node.start_position().row,
67                    end_line: node.end_position().row,
68                    content: node.byte_range(),
69                });
70            }
71
72            push_children_reverse(node, &mut stack);
73        }
74
75        let mut cursor = root.walk();
76        for child in root.children(&mut cursor) {
77            match language {
78                Language::Rust => match child.kind() {
79                    "use_declaration" => index.imports.push(IndexedImport {
80                        raw: child.byte_range(),
81                        kind: ImportKind::Use,
82                    }),
83                    "extern_crate_declaration" => index.imports.push(IndexedImport {
84                        raw: child.byte_range(),
85                        kind: ImportKind::ExternCrate,
86                    }),
87                    _ => {}
88                },
89                Language::Python => {
90                    if matches!(child.kind(), "import_statement" | "import_from_statement") {
91                        index.imports.push(IndexedImport {
92                            raw: child.byte_range(),
93                            kind: ImportKind::Import,
94                        });
95                    }
96                }
97                Language::JavaScript | Language::TypeScript => {
98                    if child.kind() == "import_statement" {
99                        index.imports.push(IndexedImport {
100                            raw: child.byte_range(),
101                            kind: ImportKind::Import,
102                        });
103                    }
104                }
105                Language::Go | Language::Java => {
106                    if child.kind() == "import_declaration" {
107                        index.imports.push(IndexedImport {
108                            raw: child.byte_range(),
109                            kind: ImportKind::Import,
110                        });
111                    }
112                }
113                // Zig has no top-level import statement node — `@import` is a
114                // builtin call bound in a `variable_declaration`, so there is
115                // no dedicated import kind to collect here.
116                Language::C | Language::Cpp | Language::Zig | Language::Unknown => {}
117            }
118        }
119
120        index
121    }
122
123    pub fn functions<'a>(&'a self, source: &'a str) -> impl Iterator<Item = FunctionRef<'a>> + 'a {
124        self.functions
125            .iter()
126            .map(move |inner| FunctionRef { inner, source })
127    }
128
129    pub fn imports<'a>(&'a self, source: &'a str) -> impl Iterator<Item = ImportRef<'a>> + 'a {
130        self.imports
131            .iter()
132            .map(move |inner| ImportRef { inner, source })
133    }
134
135    /// Byte offsets where each line starts. The first entry is always `0`.
136    pub fn line_offsets(&self) -> &[usize] {
137        &self.line_offsets
138    }
139}
140
141impl FunctionRef<'_> {
142    pub fn name(&self) -> &str {
143        &self.inner.name
144    }
145
146    pub fn signature(&self) -> &str {
147        &self.inner.signature
148    }
149
150    pub fn start_line(&self) -> usize {
151        self.inner.start_line
152    }
153
154    pub fn end_line(&self) -> usize {
155        self.inner.end_line
156    }
157
158    pub fn content(&self) -> &str {
159        &self.source[self.inner.content.clone()]
160    }
161
162    pub fn to_owned(self) -> FunctionDef {
163        FunctionDef {
164            name: self.name().to_string(),
165            signature: self.signature().to_string(),
166            start_line: self.start_line(),
167            end_line: self.end_line(),
168            content: self.content().to_string(),
169        }
170    }
171}
172
173impl ImportRef<'_> {
174    pub fn raw(&self) -> &str {
175        &self.source[self.inner.raw.clone()]
176    }
177
178    pub fn kind(&self) -> ImportKind {
179        self.inner.kind
180    }
181
182    pub fn to_owned(self) -> Import {
183        Import {
184            raw: self.raw().to_string(),
185            kind: self.kind(),
186        }
187    }
188}
189
190pub(super) fn is_function_kind(kind: &str, language: Language) -> bool {
191    match language {
192        Language::Rust => {
193            kind == "function_item" || kind == "method_declaration" || kind == "closure_expression"
194        }
195        Language::Python => kind == "function_definition",
196        Language::JavaScript | Language::TypeScript => {
197            kind == "function_declaration"
198                || kind == "method_definition"
199                || kind == "generator_function_declaration"
200                || kind == "variable_declarator"
201        }
202        Language::Go => kind == "function_declaration" || kind == "method_declaration",
203        Language::C | Language::Cpp => kind == "function_definition",
204        Language::Java => kind == "method_declaration" || kind == "constructor_declaration",
205        Language::Zig => kind == "function_declaration",
206        Language::Unknown => false,
207    }
208}
209
210fn is_function_node(node: &Node<'_>, language: Language) -> bool {
211    match language {
212        Language::JavaScript | Language::TypeScript => {
213            matches!(
214                node.kind(),
215                "function_declaration" | "method_definition" | "generator_function_declaration"
216            ) || (node.kind() == "variable_declarator"
217                && node
218                    .child_by_field_name("value")
219                    .is_some_and(|value| is_javascript_function_value(value.kind())))
220        }
221        _ => is_function_kind(node.kind(), language),
222    }
223}
224
225fn function_name<'a>(node: &Node<'_>, source: &'a str) -> Option<&'a str> {
226    if let Some(name) = node.child_by_field_name("name") {
227        return Some(&source[name.byte_range()]);
228    }
229    if let Some(declarator) = node.child_by_field_name("declarator") {
230        if let Some(name) = c_function_name(declarator, source) {
231            return Some(name);
232        }
233        if let Some(name) = first_identifier_in_subtree(declarator, source) {
234            return Some(name);
235        }
236    }
237
238    let mut cursor = node.walk();
239    for child in node.children(&mut cursor) {
240        if matches!(
241            child.kind(),
242            "identifier" | "field_identifier" | "type_identifier" | "property_identifier"
243        ) {
244            return Some(&source[child.byte_range()]);
245        }
246    }
247    None
248}
249
250fn c_function_name<'a>(function_declarator: Node<'_>, source: &'a str) -> Option<&'a str> {
251    let mut current = function_declarator.child_by_field_name("declarator")?;
252    for _ in 0..32 {
253        match current.kind() {
254            "identifier"
255            | "field_identifier"
256            | "type_identifier"
257            | "property_identifier"
258            | "operator_name"
259            | "destructor_name" => return Some(&source[current.byte_range()]),
260            "qualified_identifier" | "template_function" => {
261                current = current.child_by_field_name("name")?;
262            }
263            "pointer_declarator"
264            | "reference_declarator"
265            | "function_declarator"
266            | "parenthesized_declarator" => {
267                current = current.child_by_field_name("declarator")?;
268            }
269            _ => return None,
270        }
271    }
272    None
273}
274
275fn first_identifier_in_subtree<'a>(node: Node<'_>, source: &'a str) -> Option<&'a str> {
276    let mut stack = vec![node];
277    while let Some(current) = stack.pop() {
278        if matches!(
279            current.kind(),
280            "identifier" | "field_identifier" | "type_identifier" | "property_identifier"
281        ) {
282            return Some(&source[current.byte_range()]);
283        }
284        push_children_reverse(current, &mut stack);
285    }
286    None
287}
288
289fn function_signature(node: &Node<'_>, source: &str) -> String {
290    if node.kind() == "variable_declarator" {
291        return variable_function_signature(node, source);
292    }
293
294    let mut signature_parts = Vec::new();
295    let mut cursor = node.walk();
296    for child in node.children(&mut cursor) {
297        let kind = child.kind();
298        if matches!(
299            kind,
300            "identifier"
301                | "field_identifier"
302                | "type_identifier"
303                | "property_identifier"
304                | "parameters"
305                | "formal_parameters"
306                | "parameter_list"
307                | "function_declarator"
308                | "type_parameters"
309                | "type_arguments"
310                | "return_type"
311                | "type_annotation"
312                | "result"
313        ) {
314            signature_parts.push(&source[child.byte_range()]);
315        }
316        if matches!(
317            kind,
318            "block" | "compound_statement" | "statement_block" | "suite"
319        ) {
320            break;
321        }
322    }
323
324    signature_parts.join(" ")
325}
326
327fn variable_function_signature(node: &Node<'_>, source: &str) -> String {
328    let Some(name) = node.child_by_field_name("name") else {
329        return String::new();
330    };
331    let Some(value) = node.child_by_field_name("value") else {
332        return source[name.byte_range()].to_string();
333    };
334
335    let mut signature_parts = vec![&source[name.byte_range()]];
336    let mut cursor = value.walk();
337    for child in value.children(&mut cursor) {
338        if matches!(child.kind(), "formal_parameters" | "parameters") {
339            signature_parts.push(&source[child.byte_range()]);
340        }
341        if matches!(child.kind(), "statement_block" | "body") {
342            break;
343        }
344    }
345    signature_parts.join(" ")
346}
347
348fn line_offsets(source: &str) -> Vec<usize> {
349    let mut offsets =
350        Vec::with_capacity(source.as_bytes().iter().filter(|&&b| b == b'\n').count() + 1);
351    offsets.push(0);
352    for (index, byte) in source.bytes().enumerate() {
353        if byte == b'\n' && index + 1 < source.len() {
354            offsets.push(index + 1);
355        }
356    }
357    offsets
358}
359
360fn is_javascript_function_value(kind: &str) -> bool {
361    matches!(
362        kind,
363        "arrow_function" | "function_expression" | "generator_function"
364    )
365}
366
367fn push_children_reverse<'tree>(node: Node<'tree>, stack: &mut Vec<Node<'tree>>) {
368    let child_count = node.child_count();
369    for index in (0..child_count).rev() {
370        if let Some(child) = node.child(index as u32) {
371            stack.push(child);
372        }
373    }
374}