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