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