badness_parser/bib/semantic/
builder.rs1use std::collections::HashSet;
9
10use smol_str::SmolStr;
11
12use crate::bib::ast;
13use crate::bib::semantic::Model;
14use crate::bib::semantic::entry::{Entry, StringDef, StringUse};
15use crate::bib::syntax::{SyntaxKind, SyntaxNode};
16
17pub const MONTH_MACROS: [&str; 12] = [
20 "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec",
21];
22
23pub fn build(root: &SyntaxNode) -> Model {
25 let mut model = Model::default();
26 for node in root.descendants() {
27 match node.kind() {
28 SyntaxKind::ENTRY => collect_entry(&node, &mut model),
29 SyntaxKind::STRING_ENTRY => collect_string(&node, &mut model),
30 _ => {}
31 }
32 }
33 resolve(&mut model);
34 model
35}
36
37fn collect_entry(entry: &SyntaxNode, model: &mut Model) {
39 if let Some((key, key_range)) = ast::cite_key(entry) {
40 let entry_type = ast::entry_type(entry).unwrap_or_default().to_lowercase();
41 let title = field_cleaned(entry, "title");
42 let authors = field_cleaned(entry, "author").or_else(|| field_cleaned(entry, "editor"));
43 model.entries.push(Entry {
44 entry_type: SmolStr::new(entry_type),
45 key: SmolStr::new(key),
46 title,
47 authors,
48 key_range,
49 range: entry.text_range(),
50 duplicate: false,
51 });
52 }
53 collect_uses(entry, model);
54}
55
56fn field_cleaned(entry: &SyntaxNode, want: &str) -> Option<SmolStr> {
59 ast::fields(entry)
60 .filter(|f| ast::field_name(f).is_some_and(|n| n.eq_ignore_ascii_case(want)))
61 .find_map(|f| ast::field_value(&f))
62 .map(|v| ast::value_text_cleaned(&v))
63 .filter(|s| !s.is_empty())
64 .map(SmolStr::new)
65}
66
67fn collect_string(string_entry: &SyntaxNode, model: &mut Model) {
69 if let Some((name, range)) = ast::string_def_name(string_entry) {
70 model.string_defs.push(StringDef {
71 name: SmolStr::new(name.to_lowercase()),
72 range,
73 });
74 }
75 collect_uses(string_entry, model);
76}
77
78fn collect_uses(node: &SyntaxNode, model: &mut Model) {
80 for field in ast::fields(node) {
81 let Some(value) = ast::field_value(&field) else {
82 continue;
83 };
84 for (name, range) in ast::value_macro_uses(&value) {
85 model.string_uses.push(StringUse {
86 name: SmolStr::new(name.to_lowercase()),
87 range,
88 resolved: false,
89 });
90 }
91 }
92}
93
94fn resolve(model: &mut Model) {
96 let mut seen: HashSet<SmolStr> = HashSet::new();
98 for entry in &mut model.entries {
99 let folded = SmolStr::new(entry.key.to_lowercase());
100 if !seen.insert(folded) {
101 entry.duplicate = true;
102 }
103 }
104
105 let mut defined: HashSet<SmolStr> = model.string_defs.iter().map(|d| d.name.clone()).collect();
108 defined.extend(MONTH_MACROS.iter().map(|m| SmolStr::new(*m)));
109 for string_use in &mut model.string_uses {
110 string_use.resolved = defined.contains(&string_use.name);
111 }
112}