Skip to main content

semantic/parser/
parser_core.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Core parsing implementation.
3
4use std::sync::{Arc, OnceLock};
5
6use objects::object::{ContentHash, OccurrenceRole, ScopeEntry, ScopeKind};
7use tree_sitter::{Node, Tree as TSTree};
8
9use super::{
10    parser_language::Language,
11    parser_pool::parse_fresh,
12    parser_types::{CallSite, FunctionDef, Import},
13    syntax_index::{FunctionRef, ImportRef, SyntaxIndex},
14};
15
16/// A parsed file with its tree-sitter AST and Heddle-owned syntax index.
17#[derive(Debug)]
18pub struct ParsedFile {
19    pub language: Language,
20    pub source: Arc<str>,
21    content_hash: ContentHash,
22    tree: TSTree,
23    index: OnceLock<SyntaxIndex>,
24}
25
26impl ParsedFile {
27    /// Parse a file's contents.
28    pub fn parse(source: impl AsRef<str>, language: Language) -> Option<Self> {
29        let source = Arc::<str>::from(source.as_ref());
30        let content_hash = ContentHash::compute(source.as_bytes());
31        Self::parse_with_hash(source, language, content_hash)
32    }
33
34    /// Parse already-owned contents without copying the source string.
35    pub fn parse_owned(source: String, language: Language) -> Option<Self> {
36        let content_hash = ContentHash::compute(source.as_bytes());
37        Self::parse_with_hash(Arc::<str>::from(source), language, content_hash)
38    }
39
40    pub(crate) fn parse_with_hash(
41        source: Arc<str>,
42        language: Language,
43        content_hash: ContentHash,
44    ) -> Option<Self> {
45        let tree = parse_fresh(source.as_bytes(), language)?;
46        if tree.root_node().has_error() {
47            return None;
48        }
49
50        Some(Self {
51            language,
52            source,
53            content_hash,
54            tree,
55            index: OnceLock::new(),
56        })
57    }
58
59    /// Stable content identity for caches and sidecars.
60    pub fn content_hash(&self) -> ContentHash {
61        self.content_hash
62    }
63
64    /// Borrow the source text without cloning.
65    pub fn source(&self) -> &str {
66        &self.source
67    }
68
69    /// Get the root node of the AST.
70    pub fn root_node(&self) -> Node<'_> {
71        self.tree.root_node()
72    }
73
74    /// Compact Heddle-owned syntax data derived from the AST.
75    pub fn syntax_index(&self) -> &SyntaxIndex {
76        self.index.get_or_init(|| {
77            SyntaxIndex::build(self.language, self.source.as_ref(), self.root_node())
78        })
79    }
80
81    /// Borrow indexed function definitions.
82    pub fn functions(&self) -> impl Iterator<Item = FunctionRef<'_>> + '_ {
83        self.syntax_index().functions(self.source.as_ref())
84    }
85
86    /// Borrow indexed imports.
87    pub fn imports(&self) -> impl Iterator<Item = ImportRef<'_>> + '_ {
88        self.syntax_index().imports(self.source.as_ref())
89    }
90
91    /// Extract function definitions from the file.
92    pub fn extract_functions(&self) -> Vec<FunctionDef> {
93        self.functions().map(FunctionRef::to_owned).collect()
94    }
95
96    /// Call expressions from the syntax index. Comments and string
97    /// literals are not call nodes, so they do not appear here.
98    pub fn extract_calls(&self) -> Vec<CallSite> {
99        self.syntax_index()
100            .occurrences()
101            .iter()
102            .filter(|occurrence| occurrence.role == OccurrenceRole::Call)
103            .map(|occurrence| CallSite {
104                name: occurrence.name.clone(),
105                qualifier: occurrence.qualifier.clone(),
106            })
107            .collect()
108    }
109
110    /// Calls owned by the outermost function in this parse.
111    /// Nested function and closure bodies are excluded.
112    pub fn extract_own_calls(&self) -> Vec<CallSite> {
113        let index = self.syntax_index();
114        let scopes = index.semantic_scopes();
115        let Some(owner) = outermost_function_scope(scopes) else {
116            return Vec::new();
117        };
118        index
119            .occurrences()
120            .iter()
121            .filter(|occurrence| {
122                occurrence.role == OccurrenceRole::Call
123                    && nearest_function_scope(scopes, occurrence.scope) == Some(owner)
124            })
125            .map(|occurrence| CallSite {
126                name: occurrence.name.clone(),
127                qualifier: occurrence.qualifier.clone(),
128            })
129            .collect()
130    }
131
132    /// Extract imports from the file.
133    pub fn extract_imports(&self) -> Vec<Import> {
134        self.imports().map(ImportRef::to_owned).collect()
135    }
136
137    /// Check if a node kind string represents a function definition in the given language.
138    pub fn is_function_kind(kind: &str, language: Language) -> bool {
139        super::syntax_index::is_function_kind(kind, language)
140    }
141}
142
143fn outermost_function_scope(scopes: &[ScopeEntry]) -> Option<u32> {
144    scopes.iter().find_map(|scope| {
145        if scope.kind != ScopeKind::Function {
146            return None;
147        }
148        let mut parent = scope.parent;
149        while let Some(id) = parent {
150            let entry = scopes.get(id as usize)?;
151            if entry.kind == ScopeKind::Function {
152                return None;
153            }
154            parent = entry.parent;
155        }
156        Some(scope.local_id)
157    })
158}
159
160fn nearest_function_scope(scopes: &[ScopeEntry], scope: u32) -> Option<u32> {
161    let mut current = Some(scope);
162    while let Some(id) = current {
163        let entry = scopes.get(id as usize)?;
164        if entry.kind == ScopeKind::Function {
165            return Some(id);
166        }
167        current = entry.parent;
168    }
169    None
170}