badness_parser/bib/
semantic.rs1pub mod builder;
17pub mod entry;
18pub mod signature;
19
20pub use entry::{Entry, StringDef, StringUse};
21pub use signature::{BibFieldDb, EntrySig, FieldCategory, FieldSig, RequiredField, builtin};
22
23pub use builder::MONTH_MACROS;
24
25use crate::bib::syntax::SyntaxNode;
26
27#[derive(Debug, Default, PartialEq, Eq)]
33pub struct Model {
34 pub(crate) entries: Vec<Entry>,
35 pub(crate) string_defs: Vec<StringDef>,
36 pub(crate) string_uses: Vec<StringUse>,
37}
38
39impl Model {
40 pub fn build(root: &SyntaxNode) -> Self {
42 builder::build(root)
43 }
44
45 pub fn entries(&self) -> &[Entry] {
47 &self.entries
48 }
49
50 pub fn string_defs(&self) -> &[StringDef] {
52 &self.string_defs
53 }
54
55 pub fn string_uses(&self) -> &[StringUse] {
57 &self.string_uses
58 }
59
60 pub fn duplicate_keys(&self) -> impl Iterator<Item = &Entry> {
66 self.entries.iter().filter(|entry| entry.duplicate)
67 }
68
69 pub fn undefined_string_uses(&self) -> impl Iterator<Item = &StringUse> {
75 self.string_uses.iter().filter(|u| !u.resolved)
76 }
77
78 pub fn unused_string_defs(&self) -> impl Iterator<Item = &StringDef> {
87 let used: std::collections::HashSet<&str> =
88 self.string_uses.iter().map(|u| u.name.as_str()).collect();
89 self.string_defs
90 .iter()
91 .filter(move |d| !used.contains(d.name.as_str()))
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use crate::bib::parse;
99
100 fn model_of(src: &str) -> Model {
101 Model::build(&parse(src).syntax())
102 }
103
104 #[test]
105 fn collects_entry_type_and_key() {
106 let model = model_of("@article{knuth1984, title = {X}}\n");
107 assert_eq!(model.entries().len(), 1);
108 assert_eq!(model.entries()[0].entry_type, "article");
109 assert_eq!(model.entries()[0].key, "knuth1984");
110 assert!(!model.entries()[0].duplicate);
111 }
112
113 #[test]
114 fn caches_cleaned_title_and_author() {
115 let model =
116 model_of("@article{k, title = {The {\\TeX}book}, author = {Knuth, Donald E.}}\n");
117 let e = &model.entries()[0];
118 assert_eq!(e.title.as_deref(), Some("The {\\TeX}book"));
119 assert_eq!(e.authors.as_deref(), Some("Knuth, Donald E."));
120 }
121
122 #[test]
123 fn authors_falls_back_to_editor() {
124 let model = model_of("@book{k, title = {T}, editor = {Lamport, Leslie}}\n");
125 let e = &model.entries()[0];
126 assert_eq!(e.authors.as_deref(), Some("Lamport, Leslie"));
127 }
128
129 #[test]
130 fn missing_title_and_author_stay_none() {
131 let model = model_of("@misc{k, year = {2020}}\n");
132 let e = &model.entries()[0];
133 assert!(e.title.is_none());
134 assert!(e.authors.is_none());
135 }
136
137 #[test]
138 fn entry_type_is_lowercased() {
139 let model = model_of("@InProceedings{k, title = {X}}\n");
140 assert_eq!(model.entries()[0].entry_type, "inproceedings");
141 }
142
143 #[test]
144 fn keyless_entry_skipped() {
145 let model = model_of("@misc{");
147 assert_eq!(model.entries().len(), 0);
148 }
149
150 #[test]
151 fn duplicate_keys_flagged_case_insensitively() {
152 let model = model_of("@misc{Key, t = {a}}\n@book{key, t = {b}}\n@misc{other, t = {c}}\n");
153 assert_eq!(model.entries().len(), 3);
154 assert!(!model.entries()[0].duplicate);
156 assert!(model.entries()[1].duplicate);
157 assert!(!model.entries()[2].duplicate);
158 let dups: Vec<_> = model.duplicate_keys().map(|e| e.key.as_str()).collect();
159 assert_eq!(dups, vec!["key"]);
160 }
161
162 #[test]
163 fn string_def_collected() {
164 let model = model_of("@string{cup = {Cambridge University Press}}\n");
165 assert_eq!(model.string_defs().len(), 1);
166 assert_eq!(model.string_defs()[0].name, "cup");
167 }
168
169 #[test]
170 fn string_use_resolved_by_in_file_def() {
171 let model = model_of("@string{cup = {C}}\n@book{k, publisher = cup}\n");
172 assert_eq!(model.string_uses().len(), 1);
173 assert_eq!(model.string_uses()[0].name, "cup");
174 assert!(model.string_uses()[0].resolved);
175 assert_eq!(model.undefined_string_uses().count(), 0);
176 }
177
178 #[test]
179 fn month_macro_use_is_resolved() {
180 let model = model_of("@article{k, month = jan}\n");
181 assert_eq!(model.string_uses().len(), 1);
182 assert!(model.string_uses()[0].resolved);
183 }
184
185 #[test]
186 fn undefined_string_use_reported() {
187 let model = model_of("@book{k, publisher = nope}\n");
188 assert_eq!(model.undefined_string_uses().count(), 1);
189 assert_eq!(model.string_uses()[0].name, "nope");
190 }
191
192 #[test]
193 fn number_value_is_not_a_string_use() {
194 let model = model_of("@article{k, year = 2020}\n");
195 assert_eq!(model.string_uses().len(), 0);
196 }
197
198 #[test]
199 fn unused_string_def_reported() {
200 let model =
201 model_of("@string{cup = {C}}\n@string{used = {U}}\n@book{k, publisher = used}\n");
202 let unused: Vec<_> = model
203 .unused_string_defs()
204 .map(|d| d.name.as_str())
205 .collect();
206 assert_eq!(unused, vec!["cup"]);
207 }
208
209 #[test]
210 fn all_strings_used_reports_none() {
211 let model = model_of("@string{cup = {C}}\n@book{k, publisher = cup}\n");
212 assert_eq!(model.unused_string_defs().count(), 0);
213 }
214
215 #[test]
216 fn string_use_in_concatenation() {
217 let model = model_of("@book{k, publisher = pub # { Press}}\n@string{pub = {Foo}}\n");
219 let uses: Vec<_> = model
220 .string_uses()
221 .iter()
222 .map(|u| u.name.as_str())
223 .collect();
224 assert_eq!(uses, vec!["pub"]);
225 assert!(model.string_uses()[0].resolved);
226 }
227}