Skip to main content

badness_parser/bib/
ast.rs

1//! A typed AST layer over the BibTeX CST — the bib analog of [`crate::ast`]. Thin,
2//! read-only wrappers ([`AstNode`]) giving nodes a typed identity and syntactic
3//! accessors (an entry's type and cite key, its fields' names and values, an
4//! `@string` definition's name, the bare macro *uses* inside a value).
5//!
6//! Purely syntactic: they know nothing about what a field or entry *means* (AGENTS.md
7//! decisions #2, #10), so the semantic layer, formatter, and linter build on them
8//! without meaning leaking downward.
9//!
10//! The free functions below are thin, kind-agnostic shims over the wrapper methods,
11//! kept so existing `&SyntaxNode`-based call sites compile unchanged.
12
13pub mod nodes;
14
15pub use nodes::{Entry, EntryType, Field, FieldName, Key, StringEntry, Value};
16
17use rowan::TextRange;
18
19use crate::bib::syntax::{SyntaxKind, SyntaxNode};
20
21/// A typed wrapper over a bib CST *node* of a single [`SyntaxKind`]. Mirrors
22/// rust-analyzer's `AstNode` (and [`crate::ast::AstNode`]): `cast` succeeds iff
23/// `can_cast(node.kind())`. Re-declared here rather than shared with the LaTeX side
24/// because the two CSTs have distinct `SyntaxKind` enums and `Language` markers.
25pub trait AstNode {
26    fn can_cast(kind: SyntaxKind) -> bool
27    where
28        Self: Sized;
29    fn cast(syntax: SyntaxNode) -> Option<Self>
30    where
31        Self: Sized;
32    fn syntax(&self) -> &SyntaxNode;
33}
34
35/// The first child node castable to `N`.
36pub fn child<N: AstNode>(parent: &SyntaxNode) -> Option<N> {
37    parent.children().find_map(N::cast)
38}
39
40/// All child nodes castable to `N`, in source order.
41pub fn children<N: AstNode>(parent: &SyntaxNode) -> impl Iterator<Item = N> {
42    parent.children().filter_map(N::cast)
43}
44
45// --- Free-function shims (kind-agnostic; see module docs) ---------------------
46
47/// The entry type of an `ENTRY` / `STRING_ENTRY` / … node — the word following `@`
48/// (e.g. `"article"`, `"string"`). `None` for a malformed entry with no `ENTRY_TYPE`
49/// child. Case is preserved; callers normalize.
50pub fn entry_type(entry: &SyntaxNode) -> Option<String> {
51    child::<EntryType>(entry).and_then(|t| t.text())
52}
53
54/// The cite key of a regular `ENTRY` and the byte range of its `KEY` node. `None` when
55/// the entry has no key (a recovery case) or the key is empty.
56pub fn cite_key(entry: &SyntaxNode) -> Option<(String, TextRange)> {
57    let key = child::<Key>(entry)?;
58    key.text().map(|text| (text, key.syntax().text_range()))
59}
60
61/// The macro name defined by a `STRING_ENTRY` (`@string{ name = value }`) and the
62/// byte range of its `FIELD_NAME` node. `None` for a malformed `@string` with no
63/// `name = …` field.
64pub fn string_def_name(string_entry: &SyntaxNode) -> Option<(String, TextRange)> {
65    let name = child::<Field>(string_entry)?.name_node()?;
66    name.text().map(|text| (text, name.syntax().text_range()))
67}
68
69/// The `FIELD` children of an entry, in source order.
70pub fn fields(entry: &SyntaxNode) -> impl Iterator<Item = SyntaxNode> {
71    children::<Field>(entry).map(|f| f.syntax().clone())
72}
73
74/// The name of a `FIELD` (the text of its `FIELD_NAME`), or `None` if absent.
75pub fn field_name(field: &SyntaxNode) -> Option<String> {
76    child::<FieldName>(field).and_then(|n| n.text())
77}
78
79/// The `VALUE` node of a `FIELD` (the right-hand side of `=`), or `None` if absent.
80pub fn field_value(field: &SyntaxNode) -> Option<SyntaxNode> {
81    child::<Value>(field).map(|v| v.syntax().clone())
82}
83
84/// A field `VALUE` as plain display text: the node text, trimmed, with one layer of
85/// surrounding `{…}`/`"…"` removed and interior whitespace collapsed. Shared by the
86/// LSP hover/completion cards and the semantic model's cached title/author facts.
87pub fn value_text_cleaned(value: &SyntaxNode) -> String {
88    let raw = value.text().to_string();
89    let trimmed = raw.trim();
90    let inner = trimmed
91        .strip_prefix('{')
92        .and_then(|s| s.strip_suffix('}'))
93        .or_else(|| trimmed.strip_prefix('"').and_then(|s| s.strip_suffix('"')))
94        .unwrap_or(trimmed);
95    inner.split_whitespace().collect::<Vec<_>>().join(" ")
96}
97
98/// The bare-macro *uses* inside a `VALUE`: each `LITERAL` piece whose single token is
99/// a `WORD` (an unquoted, unbraced name) is an `@string` reference. A `LITERAL`
100/// wrapping a `NUMBER` is a literal number, not a macro use, and is skipped. Yields
101/// `(name, range)` with the range of the `LITERAL` piece.
102pub fn value_macro_uses(value: &SyntaxNode) -> impl Iterator<Item = (String, TextRange)> {
103    nodes::macro_uses_of(value)
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use crate::bib::parse;
110
111    /// The first node of `kind` in a freshly parsed `src`.
112    fn node(src: &str, kind: SyntaxKind) -> SyntaxNode {
113        parse(src)
114            .syntax()
115            .descendants()
116            .find(|n| n.kind() == kind)
117            .unwrap_or_else(|| panic!("a {kind:?} node"))
118    }
119
120    #[test]
121    fn entry_type_reads_word() {
122        let entry = node("@article{k, title = {Hi}}\n", SyntaxKind::ENTRY);
123        assert_eq!(entry_type(&entry).as_deref(), Some("article"));
124    }
125
126    #[test]
127    fn cite_key_reassembles_colon_key() {
128        let entry = node("@book{westfahl:space, title = {X}}\n", SyntaxKind::ENTRY);
129        let (key, _range) = cite_key(&entry).expect("a key");
130        assert_eq!(key, "westfahl:space");
131    }
132
133    #[test]
134    fn cite_key_none_without_key() {
135        let entry = node("@misc{", SyntaxKind::ENTRY);
136        assert_eq!(cite_key(&entry), None);
137    }
138
139    #[test]
140    fn string_def_name_reads_field_name() {
141        let s = node("@string{jan = \"January\"}\n", SyntaxKind::STRING_ENTRY);
142        let (name, _range) = string_def_name(&s).expect("a name");
143        assert_eq!(name, "jan");
144    }
145
146    #[test]
147    fn fields_and_names() {
148        let entry = node("@misc{k, a = {x}, b = 3}\n", SyntaxKind::ENTRY);
149        let names: Vec<_> = fields(&entry).filter_map(|f| field_name(&f)).collect();
150        assert_eq!(names, vec!["a", "b"]);
151    }
152
153    #[test]
154    fn value_macro_uses_finds_word_not_number() {
155        // `t = pub # {x} # 2020`: only `pub` is a macro use; `2020` is a number.
156        let field = node("@misc{k, t = pub # {x} # 2020}\n", SyntaxKind::FIELD);
157        let value = field_value(&field).expect("a value");
158        let uses: Vec<_> = value_macro_uses(&value).map(|(n, _)| n).collect();
159        assert_eq!(uses, vec!["pub"]);
160    }
161
162    // --- Wrapper-native tests --------------------------------------------------
163
164    #[test]
165    fn cast_is_kind_exact() {
166        let entry = node("@article{k, title = {Hi}}\n", SyntaxKind::ENTRY);
167        assert!(Entry::cast(entry.clone()).is_some());
168        assert!(Field::cast(entry).is_none());
169    }
170
171    #[test]
172    fn entry_wrapper_reads_type_key_and_fields() {
173        let entry = Entry::cast(node("@article{k, a = {x}, b = 3}\n", SyntaxKind::ENTRY)).unwrap();
174        assert_eq!(entry.entry_type().as_deref(), Some("article"));
175        assert_eq!(entry.cite_key().map(|(k, _)| k).as_deref(), Some("k"));
176        let names: Vec<_> = entry.fields().filter_map(|f| f.name()).collect();
177        assert_eq!(names, vec!["a", "b"]);
178    }
179}