1use std::{
2 collections::{BTreeMap, BTreeSet},
3 convert::TryFrom,
4 sync::LazyLock,
5 vec::IntoIter,
6};
7
8use miette::Diagnostic;
9use thiserror::Error;
10
11use crate::model::{Lint, lint};
12
13#[derive(Debug, Eq, PartialEq, Clone)]
15pub struct Lints {
16 lints: BTreeSet<Lint>,
17}
18
19static AVAILABLE: LazyLock<Lints> = LazyLock::new(|| {
21 let set = Lint::all_lints().collect();
22 Lints::new(set)
23});
24
25impl Lints {
26 #[must_use]
37 pub const fn new(lints: BTreeSet<Lint>) -> Self {
38 Self { lints }
39 }
40
41 #[must_use]
52 pub fn available() -> &'static Self {
53 &AVAILABLE
54 }
55
56 #[must_use]
67 pub fn names(self) -> Vec<&'static str> {
68 self.lints.iter().map(|lint| lint.name()).collect()
69 }
70
71 #[must_use]
82 pub fn config_keys(self) -> Vec<String> {
83 self.lints.iter().map(|lint| lint.config_key()).collect()
84 }
85
86 #[must_use]
98 pub fn merge(&self, other: &Self) -> Self {
99 Self::new(self.lints.union(&other.lints).copied().collect())
100 }
101
102 #[must_use]
114 pub fn subtract(&self, other: &Self) -> Self {
115 Self::new(self.lints.difference(&other.lints).copied().collect())
116 }
117}
118
119impl IntoIterator for Lints {
120 type Item = Lint;
121 type IntoIter = IntoIter<Lint>;
122
123 fn into_iter(self) -> Self::IntoIter {
124 self.lints.into_iter().collect::<Vec<_>>().into_iter()
125 }
126}
127
128impl TryFrom<Lints> for String {
129 type Error = Error;
130
131 fn try_from(lints: Lints) -> Result<Self, Self::Error> {
132 let enabled: Vec<_> = lints.into();
133
134 let config: BTreeMap<Self, bool> = Lint::all_lints()
135 .map(|x| (x, enabled.contains(&x)))
136 .fold(BTreeMap::new(), |mut acc, (lint, state)| {
137 acc.insert(lint.to_string(), state);
138 acc
139 });
140
141 let mut inner: BTreeMap<Self, BTreeMap<Self, bool>> = BTreeMap::new();
142 inner.insert("lint".into(), config);
143 let mut output: BTreeMap<Self, BTreeMap<Self, BTreeMap<Self, bool>>> = BTreeMap::new();
144 output.insert("mit".into(), inner);
145
146 Ok(toml::to_string(&output)?)
147 }
148}
149
150impl From<Vec<Lint>> for Lints {
151 fn from(lints: Vec<Lint>) -> Self {
152 Self::new(lints.into_iter().collect())
153 }
154}
155
156impl From<Lints> for Vec<Lint> {
157 fn from(lints: Lints) -> Self {
158 lints.into_iter().collect()
159 }
160}
161
162impl TryFrom<Vec<&str>> for Lints {
163 type Error = Error;
164
165 fn try_from(value: Vec<&str>) -> Result<Self, Self::Error> {
166 let lints = value
167 .into_iter()
168 .try_fold(
169 vec![],
170 |lints: Vec<Lint>, item_name| -> Result<Vec<Lint>, Error> {
171 let lint = Lint::try_from(item_name)?;
172
173 Ok([lints, vec![lint]].concat())
174 },
175 )
176 .map(Vec::into_iter)?;
177
178 Ok(Self::new(lints.collect()))
179 }
180}
181
182#[derive(Error, Debug, Diagnostic)]
184pub enum Error {
185 #[error(transparent)]
187 #[diagnostic(transparent)]
188 LintNameUnknown(#[from] lint::Error),
189 #[error("Failed to parse lint config file: {0}")]
191 #[diagnostic(
192 code(mit_lint::model::lints::error::toml_parse),
193 url(docsrs),
194 help("is it valid toml?")
195 )]
196 TomlParse(#[from] toml::de::Error),
197 #[error("Failed to convert config to toml: {0}")]
199 #[diagnostic(code(mit_lint::model::lints::error::toml_serialize), url(docsrs))]
200 TomlSerialize(#[from] toml::ser::Error),
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 use std::{
208 borrow::Borrow,
209 collections::{BTreeMap, BTreeSet},
210 convert::{TryFrom, TryInto},
211 };
212
213 use quickcheck::TestResult;
214
215 use crate::model::{
216 Lint,
217 lint::Lint::{
218 BodyWiderThan72Characters, DuplicatedTrailers, JiraIssueKeyMissing,
219 PivotalTrackerIdMissing, SubjectLongerThan72Characters, SubjectNotSeparateFromBody,
220 },
221 };
222
223 #[allow(clippy::needless_pass_by_value)]
224 #[quickcheck]
225 fn it_returns_an_error_if_one_of_the_names_is_wrong(lints: Vec<String>) -> TestResult {
226 if lints.is_empty() {
227 return TestResult::discard();
228 }
229
230 let actual: Result<Lints, Error> = lints
231 .iter()
232 .map(Borrow::borrow)
233 .collect::<Vec<&str>>()
234 .try_into();
235
236 TestResult::from_bool(actual.is_err())
237 }
238
239 #[allow(clippy::needless_pass_by_value)]
240 #[allow(unused_must_use)]
241 #[quickcheck]
242 fn no_lint_segfaults(lint: Lint, commit: String) -> TestResult {
243 lint.lint(&commit.into());
244
245 TestResult::passed()
246 }
247
248 #[test]
249 fn example_it_returns_an_error_if_one_of_the_names_is_wrong() {
250 let lints = vec![
251 "pivotal-tracker-id-missing",
252 "broken",
253 "jira-issue-key-missing",
254 ];
255 let actual: Result<Lints, Error> = lints.try_into();
256
257 actual.unwrap_err();
258 }
259
260 #[quickcheck]
261 fn it_can_construct_itself_from_names(lints: Vec<Lint>) -> bool {
262 let lint_names: Vec<&str> = lints.clone().into_iter().map(Lint::name).collect();
263
264 let expected_lints = lints.into_iter().collect::<BTreeSet<Lint>>();
265 let expected = Lints::new(expected_lints);
266
267 let actual: Lints = lint_names.try_into().expect("Lints to have been parsed");
268
269 expected == actual
270 }
271
272 #[test]
273 fn example_it_can_construct_itself_from_names() {
274 let lints = vec!["pivotal-tracker-id-missing", "jira-issue-key-missing"];
275
276 let mut expected_lints = BTreeSet::new();
277 expected_lints.insert(PivotalTrackerIdMissing);
278 expected_lints.insert(JiraIssueKeyMissing);
279
280 let expected = Lints::new(expected_lints);
281 let actual: Lints = lints.try_into().expect("Lints to have been parsed");
282
283 assert_eq!(expected, actual);
284 }
285
286 #[quickcheck]
287 fn it_can_give_me_an_into_iterator(lint_vec: Vec<Lint>) -> bool {
288 let lints = lint_vec.into_iter().collect::<BTreeSet<_>>();
289 let input = Lints::new(lints.clone());
290
291 let expected = lints.into_iter().collect::<Vec<_>>();
292 let actual = input.into_iter().collect::<Vec<_>>();
293
294 expected == actual
295 }
296
297 #[test]
298 fn example_it_can_give_me_an_into_iterator() {
299 let mut lints = BTreeSet::new();
300 lints.insert(PivotalTrackerIdMissing);
301 lints.insert(JiraIssueKeyMissing);
302 let input = Lints::new(lints);
303
304 let expected = vec![PivotalTrackerIdMissing, JiraIssueKeyMissing];
305 let actual = input.into_iter().collect::<Vec<_>>();
306
307 assert_eq!(expected, actual);
308 }
309
310 #[quickcheck]
311 fn it_can_convert_into_a_vec(lint_vec: Vec<Lint>) -> bool {
312 let lints = lint_vec.into_iter().collect::<BTreeSet<_>>();
313 let input = Lints::new(lints.clone());
314
315 let expected = lints.into_iter().collect::<Vec<_>>();
316 let actual: Vec<_> = input.into();
317
318 expected == actual
319 }
320
321 #[test]
322 fn example_it_can_convert_into_a_vec() {
323 let mut lints = BTreeSet::new();
324 lints.insert(PivotalTrackerIdMissing);
325 lints.insert(JiraIssueKeyMissing);
326 let input = Lints::new(lints);
327
328 let expected = vec![PivotalTrackerIdMissing, JiraIssueKeyMissing];
329 let actual: Vec<Lint> = input.into();
330
331 assert_eq!(expected, actual);
332 }
333
334 #[quickcheck]
335 fn it_can_give_me_the_names(lints: BTreeSet<Lint>) -> bool {
336 let lint_names: Vec<&str> = lints.clone().into_iter().map(Lint::name).collect();
337 let actual = Lints::from(lints.into_iter().collect::<Vec<Lint>>()).names();
338
339 lint_names == actual
340 }
341
342 #[test]
343 fn example_it_can_give_me_the_names() {
344 let mut lints = BTreeSet::new();
345 lints.insert(PivotalTrackerIdMissing);
346 lints.insert(JiraIssueKeyMissing);
347 let input = Lints::new(lints);
348
349 let expected = vec![PivotalTrackerIdMissing.name(), JiraIssueKeyMissing.name()];
350 let actual = input.names();
351
352 assert_eq!(expected, actual);
353 }
354
355 #[quickcheck]
356 fn it_can_give_me_the_config_keys(lints: BTreeSet<Lint>) -> bool {
357 let lint_names: Vec<String> = lints.clone().into_iter().map(Lint::config_key).collect();
358 let actual = Lints::from(lints.into_iter().collect::<Vec<Lint>>()).config_keys();
359
360 lint_names == actual
361 }
362
363 #[test]
364 fn example_it_can_give_me_the_config_keys() {
365 let mut lints = BTreeSet::new();
366 lints.insert(PivotalTrackerIdMissing);
367 lints.insert(JiraIssueKeyMissing);
368 let input = Lints::new(lints);
369
370 let expected = vec![
371 PivotalTrackerIdMissing.config_key(),
372 JiraIssueKeyMissing.config_key(),
373 ];
374 let actual = input.config_keys();
375
376 assert_eq!(expected, actual);
377 }
378
379 #[test]
380 fn can_get_all() {
381 let actual = Lints::available();
382 let lints = Lint::all_lints().collect();
383 let expected = &Lints::new(lints);
384
385 assert_eq!(
386 expected, actual,
387 "Expected all the lints to be {expected:?}, instead got {actual:?}"
388 );
389 }
390
391 #[test]
392 fn example_can_get_all() {
393 let actual = Lints::available();
394 let lints = Lint::all_lints().collect();
395 let expected = &Lints::new(lints);
396
397 assert_eq!(
398 expected, actual,
399 "Expected all the lints to be {expected:?}, instead got {actual:?}"
400 );
401 }
402
403 #[allow(clippy::needless_pass_by_value)]
404 #[quickcheck]
405 fn get_toml(expected: BTreeMap<Lint, bool>) -> bool {
406 let toml = String::try_from(Lints::new(
407 expected
408 .iter()
409 .filter(|(_, enabled)| **enabled)
410 .map(|(lint, _)| *lint)
411 .collect(),
412 ))
413 .expect("To be able to convert lints to toml");
414 let full: BTreeMap<String, BTreeMap<String, BTreeMap<String, bool>>> =
415 toml::from_str(toml.as_str()).unwrap();
416 let actual: BTreeMap<Lint, bool> = full
417 .get("mit")
418 .and_then(|x| x.get("lint"))
419 .expect("To have successfully removed the wrapping keys")
420 .iter()
421 .map(|(lint, enabled)| (Lint::try_from(lint.as_str()).unwrap(), *enabled))
422 .collect();
423
424 actual.iter().all(|(actual_key, actual_enabled)| {
425 expected
426 .get(actual_key)
427 .map_or(!*actual_enabled, |expected_enabled| {
428 expected_enabled == actual_enabled
429 })
430 })
431 }
432
433 #[test]
434 fn example_get_toml() {
435 let mut lints_on = BTreeSet::new();
436 lints_on.insert(DuplicatedTrailers);
437 lints_on.insert(SubjectNotSeparateFromBody);
438 lints_on.insert(SubjectLongerThan72Characters);
439 lints_on.insert(BodyWiderThan72Characters);
440 lints_on.insert(PivotalTrackerIdMissing);
441 let actual = String::try_from(Lints::new(lints_on)).expect("Failed to serialise");
442 let expected = "[mit.lint]
443body-wider-than-72-characters = true
444duplicated-trailers = true
445github-id-missing = false
446jira-issue-key-missing = false
447not-conventional-commit = false
448not-emoji-log = false
449pivotal-tracker-id-missing = true
450subject-line-ends-with-period = false
451subject-line-not-capitalized = false
452subject-longer-than-72-characters = true
453subject-not-separated-from-body = true
454";
455
456 assert_eq!(
457 expected, actual,
458 "Expected the list of lint identifiers to be {expected:?}, instead got {actual:?}"
459 );
460 }
461
462 #[allow(clippy::needless_pass_by_value)]
463 #[quickcheck]
464 fn two_sets_of_lints_can_be_merged(
465 set_a_lints: BTreeSet<Lint>,
466 set_b_lints: BTreeSet<Lint>,
467 ) -> bool {
468 let set_a = Lints::new(set_a_lints.clone());
469 let set_b = Lints::new(set_b_lints.clone());
470
471 let actual = set_a.merge(&set_b);
472
473 let expected = Lints::new(set_a_lints.union(&set_b_lints).copied().collect());
474
475 expected == actual
476 }
477
478 #[test]
479 fn example_two_sets_of_lints_can_be_merged() {
480 let mut set_a_lints = BTreeSet::new();
481 set_a_lints.insert(PivotalTrackerIdMissing);
482
483 let mut set_b_lints = BTreeSet::new();
484 set_b_lints.insert(DuplicatedTrailers);
485
486 let set_a = Lints::new(set_a_lints);
487 let set_b = Lints::new(set_b_lints);
488
489 let actual = set_a.merge(&set_b);
490
491 let mut lints = BTreeSet::new();
492 lints.insert(DuplicatedTrailers);
493 lints.insert(PivotalTrackerIdMissing);
494 let expected = Lints::new(lints);
495
496 assert_eq!(
497 expected, actual,
498 "Expected the list of lint identifiers to be {expected:?}, instead got {actual:?}"
499 );
500 }
501
502 #[allow(clippy::needless_pass_by_value)]
503 #[quickcheck]
504 fn we_can_subtract_lints_from_the_lint_list(
505 set_a_lints: BTreeSet<Lint>,
506 set_b_lints: BTreeSet<Lint>,
507 ) -> bool {
508 let total = Lints::new(set_a_lints.union(&set_b_lints).copied().collect());
509 let set_a = Lints::new(set_a_lints.difference(&set_b_lints).copied().collect());
510 let expected = Lints::new(set_b_lints);
511
512 let actual = total.subtract(&set_a);
513
514 expected == actual
515 }
516
517 #[test]
518 fn example_we_can_subtract_lints_from_the_lint_list() {
519 let mut set_a_lints = BTreeSet::new();
520 set_a_lints.insert(JiraIssueKeyMissing);
521 set_a_lints.insert(PivotalTrackerIdMissing);
522
523 let mut set_b_lints = BTreeSet::new();
524 set_b_lints.insert(DuplicatedTrailers);
525 set_b_lints.insert(PivotalTrackerIdMissing);
526
527 let set_a = Lints::new(set_a_lints);
528 let set_b = Lints::new(set_b_lints);
529
530 let actual = set_a.subtract(&set_b);
531
532 let mut lints = BTreeSet::new();
533 lints.insert(JiraIssueKeyMissing);
534 let expected = Lints::new(lints);
535
536 assert_eq!(
537 expected, actual,
538 "Expected the list of lint identifiers to be {expected:?}, instead got {actual:?}"
539 );
540 }
541
542 #[test]
543 fn example_when_merging_overlapping_does_not_lead_to_duplication() {
544 let mut set_a_lints = BTreeSet::new();
545 set_a_lints.insert(PivotalTrackerIdMissing);
546
547 let mut set_b_lints = BTreeSet::new();
548 set_b_lints.insert(DuplicatedTrailers);
549 set_b_lints.insert(PivotalTrackerIdMissing);
550
551 let set_a = Lints::new(set_a_lints);
552 let set_b = Lints::new(set_b_lints);
553
554 let actual = set_a.merge(&set_b);
555
556 let mut lints = BTreeSet::new();
557 lints.insert(DuplicatedTrailers);
558 lints.insert(PivotalTrackerIdMissing);
559 let expected = Lints::new(lints);
560
561 assert_eq!(
562 expected, actual,
563 "Expected the list of lint identifiers to be {expected:?}, instead got {actual:?}"
564 );
565 }
566}