Skip to main content

quarb_code/
lib.rs

1//! Source-code AST adapter for the Quarb query engine.
2//!
3//! The trait is named `AstAdapter`; this adapter takes the name
4//! literally: a source file parses (tree-sitter) into its syntax
5//! tree, and the tree is the arbor. Node kinds are the edge names
6//! (`//function_item`, `//call_expression`), a node's value
7//! (`::`) is its source text, and tree-sitter's *fields* become
8//! properties — `::name` on a `function_item` is the function's
9//! name, `::body` its block — so
10//! `//function_item[::name = "main"]` reads as it should.
11//!
12//! Only *named* nodes appear (punctuation and keywords are
13//! syntax, not structure). Metadata: `::;kind`, `::;field` (this
14//! node's field name in its parent), `::;start-line` /
15//! `::;end-line` (1-based), `::;n-children`. Languages: Rust,
16//! Python, JavaScript, by extension (`rs`, `py`, `js`); the
17//! grammar set is compile-time and easily grown.
18//!
19//! Composed (`qua --descend`), source files graft like JSON does
20//! — `//function_item::name` over a whole directory tree is one
21//! query across every parsed file.
22
23use quarb::{AstAdapter, NodeId, Value};
24
25/// An error parsing a source file.
26#[derive(Debug, thiserror::Error)]
27pub enum CodeError {
28    #[error("code: {0}")]
29    Io(#[from] std::io::Error),
30    #[error("code: no grammar for extension {0:?}")]
31    Language(String),
32    #[error("code: parse produced no tree")]
33    Parse,
34}
35
36struct Node {
37    kind: &'static str,
38    field: Option<&'static str>,
39    parent: Option<NodeId>,
40    children: Vec<NodeId>,
41    /// Byte range into the source.
42    start: usize,
43    end: usize,
44    start_line: usize,
45    end_line: usize,
46    /// Field name → child index, for `::field` properties.
47    fields: Vec<(&'static str, usize)>,
48}
49
50/// A parsed source file, exposed as its syntax tree.
51pub struct CodeAdapter {
52    source: String,
53    nodes: Vec<Node>,
54}
55
56/// The grammar for a file extension.
57fn language(ext: &str) -> Option<tree_sitter::Language> {
58    match ext {
59        "rs" => Some(tree_sitter_rust::LANGUAGE.into()),
60        "py" => Some(tree_sitter_python::LANGUAGE.into()),
61        "js" | "mjs" | "cjs" | "jsx" => Some(tree_sitter_javascript::LANGUAGE.into()),
62        _ => None,
63    }
64}
65
66/// Whether an extension has a grammar (for dispatch and grafting).
67pub fn supported(ext: &str) -> bool {
68    language(ext).is_some()
69}
70
71impl CodeAdapter {
72    /// Parse source text as `ext`'s language.
73    pub fn parse(text: &str, ext: &str) -> Result<Self, CodeError> {
74        let lang = language(ext).ok_or_else(|| CodeError::Language(ext.to_string()))?;
75        let mut parser = tree_sitter::Parser::new();
76        parser
77            .set_language(&lang)
78            .map_err(|_| CodeError::Language(ext.to_string()))?;
79        let tree = parser.parse(text, None).ok_or(CodeError::Parse)?;
80        let mut nodes = Vec::new();
81        build(&mut nodes, tree.root_node());
82        Ok(CodeAdapter {
83            source: text.to_string(),
84            nodes,
85        })
86    }
87
88    /// Parse a file, language by extension.
89    pub fn open(path: &std::path::Path) -> Result<Self, CodeError> {
90        let ext = path
91            .extension()
92            .and_then(|e| e.to_str())
93            .unwrap_or("")
94            .to_ascii_lowercase();
95        let text = std::fs::read_to_string(path)?;
96        Self::parse(&text, &ext)
97    }
98
99    /// A human-readable locator: `/kind[start-line]` chain.
100    pub fn locator(&self, node: NodeId) -> String {
101        let mut parts = Vec::new();
102        let mut cur = Some(node);
103        while let Some(n) = cur {
104            let nd = &self.nodes[n.0 as usize];
105            if nd.parent.is_some() {
106                parts.push(format!("{}:{}", nd.kind, nd.start_line));
107            }
108            cur = nd.parent;
109        }
110        parts.reverse();
111        format!("/{}", parts.join("/"))
112    }
113
114    fn text_of(&self, n: &Node) -> &str {
115        &self.source[n.start.min(self.source.len())..n.end.min(self.source.len())]
116    }
117}
118
119/// Intern the named nodes. Uses an explicit stack rather than
120/// recursion so a deeply nested source file (thousands of levels)
121/// can't overflow the call stack. Nodes are interned in the same
122/// pre-order the recursive walk produced.
123fn build(nodes: &mut Vec<Node>, root: tree_sitter::Node<'_>) {
124    // (tree-sitter node, parent id, this node's field in its parent)
125    let mut stack: Vec<(tree_sitter::Node<'_>, Option<NodeId>, Option<&'static str>)> =
126        vec![(root, None, None)];
127    while let Some((ts, parent, field)) = stack.pop() {
128        let id = NodeId(nodes.len() as u64);
129        nodes.push(Node {
130            kind: ts.kind(),
131            field,
132            parent,
133            children: Vec::new(),
134            start: ts.start_byte(),
135            end: ts.end_byte(),
136            start_line: ts.start_position().row + 1,
137            end_line: ts.end_position().row + 1,
138            fields: Vec::new(),
139        });
140        // Record this node with its parent, preserving child order
141        // and the field-name → child-index map.
142        if let Some(p) = parent {
143            let pnode = &mut nodes[p.0 as usize];
144            if let Some(f) = field {
145                pnode.fields.push((f, pnode.children.len()));
146            }
147            pnode.children.push(id);
148        }
149        // Collect named children, then push them reversed so they
150        // pop — and get interned — in source order (a pre-order DFS,
151        // matching the previous recursive numbering).
152        let mut cursor = ts.walk();
153        let mut kids = Vec::new();
154        if cursor.goto_first_child() {
155            loop {
156                let child = cursor.node();
157                if child.is_named() {
158                    kids.push((child, cursor.field_name()));
159                }
160                if !cursor.goto_next_sibling() {
161                    break;
162                }
163            }
164        }
165        for (child, f) in kids.into_iter().rev() {
166            stack.push((child, Some(id), f));
167        }
168    }
169}
170
171impl AstAdapter for CodeAdapter {
172    fn root(&self) -> NodeId {
173        NodeId(0)
174    }
175
176    fn children(&self, node: NodeId) -> Vec<NodeId> {
177        self.nodes[node.0 as usize].children.clone()
178    }
179
180    /// The node kind (`function_item`, `identifier`, ...); the
181    /// root (source_file/module/program) stays unnamed so `/`
182    /// starts at its children.
183    fn name(&self, node: NodeId) -> Option<String> {
184        let n = &self.nodes[node.0 as usize];
185        n.parent.map(|_| n.kind.to_string())
186    }
187
188    fn parent(&self, node: NodeId) -> Option<NodeId> {
189        self.nodes[node.0 as usize].parent
190    }
191
192    /// The kind, as a trait too (`<function_item>` where a trait
193    /// filter reads better than a name step).
194    fn traits(&self, node: NodeId) -> Vec<String> {
195        vec![self.nodes[node.0 as usize].kind.to_string()]
196    }
197
198    /// Tree-sitter fields: `::name` on a function is the name
199    /// child's source text.
200    fn property(&self, node: NodeId, name: &str) -> Option<Value> {
201        let n = &self.nodes[node.0 as usize];
202        let (_, idx) = n.fields.iter().find(|(f, _)| *f == name)?;
203        let child = &self.nodes[n.children[*idx].0 as usize];
204        Some(Value::Str(self.text_of(child).to_string()))
205    }
206
207    /// A node's source text.
208    fn default_value(&self, node: NodeId) -> Option<Value> {
209        Some(Value::Str(
210            self.text_of(&self.nodes[node.0 as usize]).to_string(),
211        ))
212    }
213
214    fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
215        let n = &self.nodes[node.0 as usize];
216        match key {
217            "kind" => Some(Value::Str(n.kind.to_string())),
218            "field" => n.field.map(|f| Value::Str(f.to_string())),
219            "start-line" => Some(Value::Int(n.start_line as i64)),
220            "end-line" => Some(Value::Int(n.end_line as i64)),
221            "n-children" => Some(Value::Int(n.children.len() as i64)),
222            _ => None,
223        }
224    }
225}