1pub mod fetch;
8pub mod lints;
9pub mod modifiers;
10pub mod reporters;
11pub mod tree;
12
13use annotate_snippets::snippet::{Annotation, AnnotationType, Slice, Snippet};
14
15use comrak::arena_tree::Node;
16use comrak::nodes::Ast;
17use comrak::{Arena, ComrakExtensionOptions, ComrakOptions};
18
19use crate::lints::{Context, DefaultLint, Error as LintError, FetchContext, InnerContext, Lint};
20use crate::modifiers::{DefaultModifier, Modifier};
21use crate::reporters::Reporter;
22
23use educe::Educe;
24
25use tnipv_preamble::{Preamble, SplitError};
26
27use serde::{Deserialize, Serialize};
28
29use snafu::{ensure, ResultExt, Snafu};
30
31use std::cell::RefCell;
32use std::collections::hash_map::{self, HashMap};
33use std::path::{Path, PathBuf};
34
35#[derive(Snafu, Debug)]
36#[non_exhaustive]
37pub enum Error {
38 Lint {
39 #[snafu(backtrace)]
40 source: LintError,
41 origin: Option<PathBuf>,
42 },
43 #[snafu(context(false))]
44 Modifier {
45 #[snafu(backtrace)]
46 source: crate::modifiers::Error,
47 },
48 Io {
49 path: PathBuf,
50 source: std::io::Error,
51 },
52 SliceFetched {
53 lint: String,
54 origin: Option<PathBuf>,
55 },
56}
57
58#[doc(hidden)]
59pub fn default_modifiers_enum() -> Vec<DefaultModifier<&'static str>> {
61 vec![
62 DefaultModifier::SetDefaultAnnotation(modifiers::SetDefaultAnnotation {
63 name: "status",
64 value: "Stagnant",
65 annotation_type: AnnotationType::Warning,
66 }),
67 DefaultModifier::SetDefaultAnnotation(modifiers::SetDefaultAnnotation {
68 name: "status",
69 value: "Withdrawn",
70 annotation_type: AnnotationType::Warning,
71 }),
72 ]
73}
74
75pub fn default_modifiers() -> impl Iterator<Item = Box<dyn Modifier>> {
76 default_modifiers_enum().into_iter().map(|m| match m {
77 DefaultModifier::SetDefaultAnnotation(m) => Box::new(m) as Box<dyn Modifier>,
78 })
79}
80
81pub fn default_lints() -> impl Iterator<Item = (&'static str, Box<dyn Lint>)> {
82 default_lints_enum().map(|(name, lint)| (name, lint.boxed()))
83}
84
85#[doc(hidden)]
86pub fn default_lints_enum() -> impl Iterator<Item = (&'static str, DefaultLint<&'static str>)> {
88 use self::DefaultLint::*;
89 use lints::preamble::regex;
90 use lints::{markdown, preamble};
91
92 [
93 ("preamble-no-dup", PreambleNoDuplicates(preamble::NoDuplicates)),
97 ("preamble-trim", PreambleTrim(preamble::Trim)),
98 ("preamble-tnip", PreambleUint { name: preamble::Uint("tnip") }),
99 ("preamble-author", PreambleAuthor { name: preamble::Author("author") } ),
100 ("preamble-re-title", PreambleRegex(preamble::Regex {
101 name: "title",
102 mode: regex::Mode::Excludes,
103 pattern: r"(?i)standar\w*\b",
104 message: "preamble header `title` should not contain `standard` (or similar words.)",
105 })),
106 ("preamble-re-title-colon", PreambleRegex(preamble::Regex {
107 name: "title",
108 mode: regex::Mode::Excludes,
109 pattern: r":",
110 message: "preamble header `title` should not contain `:`",
111 })),
112 (
113 "preamble-refs-title",
114 PreambleProposalRef(preamble::ProposalRef {
115 name: "title",
116 prefix: "tnip-",
117 suffix: ".md",
118 }),
119 ),
120 (
121 "preamble-refs-description",
122 PreambleProposalRef(preamble::ProposalRef {
123 name: "description",
124 prefix: "tnip-",
125 suffix: ".md",
126 }),
127 ),
128 (
129 "preamble-re-title-tnip-dash",
130 PreambleRegex(preamble::Regex {
131 name: "title",
132 mode: regex::Mode::Excludes,
133 pattern: r"(?i)tnip[\s]*[0-9]+",
134 message: "proposals must be referenced with the form `TNIP-N` (not `TNIPN` or `TNIP N`)",
135 }),
136 ),
137 (
138 "preamble-re-description-tnip-dash",
139 PreambleRegex(preamble::Regex {
140 name: "description",
141 mode: regex::Mode::Excludes,
142 pattern: r"(?i)tnip[\s]*[0-9]+",
143 message: "proposals must be referenced with the form `TNIP-N` (not `TNIPN` or `TNIP N`)",
144 }),
145 ),
146 ("preamble-re-description", PreambleRegex(preamble::Regex {
147 name: "description",
148 mode: regex::Mode::Excludes,
149 pattern: r"(?i)standar\w*\b",
150 message: "preamble header `description` should not contain `standard` (or similar words.)",
151 })),
152 ("preamble-re-description-colon", PreambleRegex(preamble::Regex {
153 name: "description",
154 mode: regex::Mode::Excludes,
155 pattern: r":",
156 message: "preamble header `description` should not contain `:`",
157 })),
158 (
159 "preamble-discussions-to",
160 PreambleUrl { name: preamble::Url("discussions-to") },
161 ),
162 (
163 "preamble-re-discussions-to",
164 PreambleRegex(preamble::Regex {
165 name: "discussions-to",
166 mode: regex::Mode::Includes,
167 pattern: "^https://forum.telcoin.org/t/[^/]+/[0-9]+$",
168 message: concat!(
169 "preamble header `discussions-to` should ",
170 "point to a thread on forum.telcoin.org/tnips"
171 ),
172 }),
173 ),
174 ("preamble-list-author", PreambleList { name: preamble::List("author") }),
175 ("preamble-list-requires", PreambleList{name: preamble::List("requires")}),
176 (
177 "preamble-len-requires",
178 PreambleLength(preamble::Length {
179 name: "requires",
180 min: Some(1),
181 max: None,
182 }
183 ),
184 ),
185 (
186 "preamble-uint-requires",
187 PreambleUintList { name: preamble::UintList("requires") },
188 ),
189 (
190 "preamble-len-title",
191 PreambleLength(preamble::Length {
192 name: "title",
193 min: Some(2),
194 max: Some(44),
195 }
196 ),
197 ),
198 (
199 "preamble-len-description",
200 PreambleLength(preamble::Length {
201 name: "description",
202 min: Some(2),
203 max: Some(140),
204 }
205 ),
206 ),
207 (
208 "preamble-req",
209 PreambleRequired { names: preamble::Required(vec![
210 "tnip",
211 "title",
212 "description",
213 "author",
214 "discussions-to",
215 "status",
216 "type",
217 "created",
218 ])
219 },
220 ),
221 (
222 "preamble-order",
223 PreambleOrder { names: preamble::Order(vec![
224 "tnip",
225 "title",
226 "description",
227 "author",
228 "discussions-to",
229 "status",
230 "last-call-deadline",
231 "type",
232 "category",
233 "created",
234 "requires",
235 "withdrawal-reason",
236 ])
237 },
238 ),
239 ("preamble-date-created", PreambleDate { name: preamble::Date("created") } ),
240 (
241 "preamble-req-last-call-deadline",
242 PreambleRequiredIfEq(preamble::RequiredIfEq {
243 when: "status",
244 equals: "Last Call",
245 then: "last-call-deadline",
246 }
247 ),
248 ),
249 (
250 "preamble-date-last-call-deadline",
251 PreambleDate { name: preamble::Date("last-call-deadline") },
252 ),
253 (
254 "preamble-req-category",
255 PreambleRequiredIfEq(preamble::RequiredIfEq {
256 when: "type",
257 equals: "Standards Track",
258 then: "category",
259 }
260 ),
261 ),
262 (
263 "preamble-req-withdrawal-reason",
264 PreambleRequiredIfEq(preamble::RequiredIfEq {
265 when: "status",
266 equals: "Withdrawn",
267 then: "withdrawal-reason",
268 }
269 ),
270 ),
271 (
272 "preamble-enum-status",
273 PreambleOneOf(preamble::OneOf {
274 name: "status",
275 values: vec![
276 "Draft",
277 "Review",
278 "Last Call",
279 "Final",
280 "Stagnant",
281 "Withdrawn",
282 "Living",
283 ],
284 }
285 ),
286 ),
287 (
288 "preamble-enum-type",
289 PreambleOneOf(preamble::OneOf {
290 name: "type",
291 values: vec!["Standards Track", "Meta", "Informational"],
292 }
293 ),
294 ),
295 (
296 "preamble-enum-category",
297 PreambleOneOf(preamble::OneOf {
298 name: "category",
299 values: vec!["Core", "Networking", "Interface"],
300 }
301 ),
302 ),
303 (
304 "preamble-requires-status",
305 PreambleRequiresStatus(preamble::RequiresStatus {
306 requires: "requires",
307 status: "status",
308 prefix: "tnip-",
309 suffix: ".md",
310 flow: vec![
311 vec!["Draft", "Stagnant"],
312 vec!["Review"],
313 vec!["Last Call"],
314 vec!["Final", "Withdrawn", "Living"],
315 ]
316 }),
317 ),
318 (
319 "preamble-requires-ref-title",
320 PreambleRequireReferenced(preamble::RequireReferenced {
321 name: "title",
322 requires: "requires",
323 }),
324 ),
325 (
326 "preamble-requires-ref-description",
327 PreambleRequireReferenced(preamble::RequireReferenced {
328 name: "description",
329 requires: "requires",
330 }),
331 ),
332 (
333 "preamble-file-name",
334 PreambleFileName(preamble::FileName {
335 name: "tnip",
336 prefix: "tnip-",
337 suffix: ".md",
338 }),
339 ),
340 (
344 "markdown-refs",
345 MarkdownProposalRef(markdown::ProposalRef {
346 prefix: "tnip-",
347 suffix: ".md",
348 }),
349 ),
350 (
351 "markdown-html-comments",
352 MarkdownHtmlComments(markdown::HtmlComments {
353 name: "status",
354 warn_for: vec![
355 "Draft",
356 "Withdrawn",
357 ],
358 }
359 ),
360 ),
361 (
362 "markdown-req-section",
363 MarkdownSectionRequired { sections: markdown::SectionRequired(vec![
364 "Abstract",
365 "Specification",
366 "Rationale",
367 "Security Considerations",
368 "Copyright",
369 ])
370 },
371 ),
372 (
373 "markdown-order-section",
374 MarkdownSectionOrder {
375 sections: markdown::SectionOrder(vec![
376 "Abstract",
377 "Motivation",
378 "Specification",
379 "Rationale",
380 "Backwards Compatibility",
381 "Test Cases",
382 "Reference Implementation",
383 "Security Considerations",
384 "Copyright",
385 ])
386 },
387 ),
388 (
389 "markdown-re-tnip-dash",
390 MarkdownRegex(markdown::Regex {
391 mode: markdown::regex::Mode::Excludes,
392 pattern: r"(?i)tnip[\s]*[0-9]+",
393 message: "proposals must be referenced with the form `TNIP-N` (not `TNIPN` or `TNIP N`)",
394 }),
395 ),
396 (
397 "markdown-link-first",
398 MarkdownLinkFirst {
399 pattern: markdown::LinkFirst(r"(?i)(?:tnip)-[0-9]+"),
400 }
401 ),
402 ("markdown-rel-links", MarkdownRelativeLinks(markdown::RelativeLinks {
403 exceptions: vec![
404 "^https://(www\\.)?github\\.com/telcoin-association/consensus-specs/blob/[a-f0-9]{40}/.+$",
405 "^https://(www\\.)?github\\.com/telcoin-association/consensus-specs/commit/[a-f0-9]{40}$",
406
407 "^https://(www\\.)?github\\.com/telcoin-association/devp2p/blob/[0-9a-f]{40}/.+$",
408 "^https://(www\\.)?github\\.com/telcoin-association/devp2p/commit/[0-9a-f]{40}$",
409
410 "^https://(www\\.)?github\\.com/bitcoin/bips/blob/[0-9a-f]{40}/bip-[0-9]+\\.mediawiki$",
411
412 "^https://www\\.w3\\.org/TR/[0-9][0-9][0-9][0-9]/.*$",
413 "^https://[a-z]*\\.spec\\.whatwg\\.org/commit-snapshots/[0-9a-f]{40}/$",
414 "^https://www\\.rfc-editor\\.org/rfc/.*$",
415 ]
416 })),
417 (
418 "markdown-link-status",
419 MarkdownLinkStatus(markdown::LinkStatus {
420 prefix: "tnip-",
421 suffix: ".md",
422 status: "status",
423 flow: vec![
424 vec!["Draft", "Stagnant"],
425 vec!["Review"],
426 vec!["Last Call"],
427 vec!["Final", "Withdrawn", "Living"],
428 ]
429 }),
430 ),
431 (
432 "markdown-json-cite",
433 MarkdownJsonSchema(markdown::JsonSchema {
434 additional_schemas: vec![
435 (
436 "https://resource.citationstyles.org/schema/v1.0/input/json/csl-data.json",
437 include_str!("lints/markdown/json_schema/csl-data.json"),
438 ),
439 ],
440 schema: include_str!("lints/markdown/json_schema/citation.json"),
441 language: "csl-json",
442 help: concat!(
443 "see https://github.com/telcoin-association/tnipv/blob/",
444 "main/tnipv-lint/src/lints/markdown/",
445 "json_schema/citation.json",
446 ),
447 }),
448 ),
449 (
450 "markdown-headings-space",
451 MarkdownHeadingsSpace(markdown::HeadingsSpace{}),
452 )
453 ]
454 .into_iter()
455}
456
457#[derive(Debug)]
458enum Source<'a> {
459 String {
460 origin: Option<&'a str>,
461 src: &'a str,
462 },
463 File(&'a Path),
464}
465
466impl<'a> Source<'a> {
467 fn origin(&self) -> Option<&Path> {
468 match self {
469 Self::String {
470 origin: Some(s), ..
471 } => Some(Path::new(s)),
472 Self::File(p) => Some(p),
473 _ => None,
474 }
475 }
476
477 fn is_string(&self) -> bool {
478 matches!(self, Self::String { .. })
479 }
480
481 async fn fetch(&self, fetch: &dyn fetch::Fetch) -> Result<String, Error> {
482 match self {
483 Self::File(f) => fetch
484 .fetch(f.to_path_buf())
485 .await
486 .with_context(|_| IoSnafu { path: f.to_owned() })
487 .map_err(Into::into),
488 Self::String { src, .. } => Ok((*src).to_owned()),
489 }
490 }
491}
492
493#[derive(Debug, Clone)]
494#[non_exhaustive]
495pub struct LintSettings<'a> {
496 _p: std::marker::PhantomData<&'a dyn Lint>,
497 pub default_annotation_type: AnnotationType,
498}
499
500struct NeverIter<T> {
501 _p: std::marker::PhantomData<fn() -> T>,
502 q: std::convert::Infallible,
503}
504
505impl<T> Iterator for NeverIter<T> {
506 type Item = T;
507
508 fn next(&mut self) -> Option<T> {
509 match self.q {}
510 }
511}
512
513#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
514#[non_exhaustive]
515pub struct Options<M, L> {
516 #[serde(default, skip_serializing_if = "Option::is_none")]
517 pub modifiers: Option<M>,
518
519 #[serde(default, skip_serializing_if = "Option::is_none")]
520 pub lints: Option<L>,
521}
522
523impl<M, L> Default for Options<M, L> {
524 fn default() -> Self {
525 Self {
526 modifiers: None,
527 lints: None,
528 }
529 }
530}
531
532impl<M, L, K> Options<Vec<M>, HashMap<K, L>>
533where
534 M: 'static + Clone + Modifier,
535 L: 'static + Clone + Lint,
536 K: AsRef<str>,
537{
538 pub fn to_iters(
539 &self,
540 ) -> Options<
541 impl Iterator<Item = Box<dyn Modifier>> + '_,
542 impl Iterator<Item = (&'_ str, Box<dyn Lint>)>,
543 > {
544 let modifiers = self
545 .modifiers
546 .as_ref()
547 .map(|m| m.iter().map(|n| Box::new(n.clone()) as Box<dyn Modifier>));
548
549 let lints = self.lints.as_ref().map(|l| {
550 l.iter()
551 .map(|(k, v)| (k.as_ref(), Box::new(v.clone()) as Box<dyn Lint>))
552 });
553
554 Options { modifiers, lints }
555 }
556}
557
558#[derive(Educe)]
559#[educe(Debug)]
560#[must_use]
561pub struct Linter<'a, R> {
562 lints: HashMap<&'a str, (Option<AnnotationType>, Box<dyn Lint>)>,
563 modifiers: Vec<Box<dyn Modifier>>,
564 sources: Vec<Source<'a>>,
565
566 #[educe(Debug(ignore))]
567 reporter: R,
568
569 #[educe(Debug(ignore))]
570 fetch: Box<dyn fetch::Fetch>,
571}
572
573impl<'a, R> Default for Linter<'a, R>
574where
575 R: Default,
576{
577 fn default() -> Self {
578 Self::new(R::default())
579 }
580}
581
582impl<'a, R> Linter<'a, R> {
583 pub fn with_options<'b, M, L>(reporter: R, options: Options<M, L>) -> Self
584 where
585 'b: 'a,
586 L: Iterator<Item = (&'b str, Box<dyn Lint>)>,
587 M: Iterator<Item = Box<dyn Modifier>>,
588 {
589 let modifiers = match options.modifiers {
590 Some(m) => m.collect(),
591 None => default_modifiers().collect(),
592 };
593
594 let lints = match options.lints {
595 Some(l) => l.map(|(slug, lint)| (slug, (None, lint))).collect(),
596 None => default_lints()
597 .map(|(slug, lint)| (slug, (None, lint)))
598 .collect(),
599 };
600
601 Self {
602 reporter,
603 sources: Default::default(),
604 fetch: Box::<fetch::DefaultFetch>::default(),
605 modifiers,
606 lints,
607 }
608 }
609
610 pub fn with_modifiers(reporter: R, modifiers: impl Iterator<Item = Box<dyn Modifier>>) -> Self {
611 Self::with_options(
612 reporter,
613 Options {
614 lints: Option::<NeverIter<_>>::None,
615 modifiers: Some(modifiers),
616 },
617 )
618 }
619
620 pub fn with_lints<'b: 'a>(
621 reporter: R,
622 lints: impl Iterator<Item = (&'b str, Box<dyn Lint>)>,
623 ) -> Self {
624 Self::with_options(
625 reporter,
626 Options {
627 modifiers: Option::<NeverIter<_>>::None,
628 lints: Some(lints),
629 },
630 )
631 }
632
633 pub fn new(reporter: R) -> Self {
634 Self::with_options::<NeverIter<_>, NeverIter<_>>(reporter, Options::default())
635 }
636
637 pub fn warn<T>(self, slug: &'a str, lint: T) -> Self
638 where
639 T: 'static + Lint,
640 {
641 self.add_lint(Some(AnnotationType::Warning), slug, lint)
642 }
643
644 pub fn deny<T>(self, slug: &'a str, lint: T) -> Self
645 where
646 T: 'static + Lint,
647 {
648 self.add_lint(Some(AnnotationType::Error), slug, lint)
649 }
650
651 pub fn modify<T>(mut self, modifier: T) -> Self
652 where
653 T: 'static + Modifier,
654 {
655 self.modifiers.push(Box::new(modifier));
656 self
657 }
658
659 fn add_lint<T>(mut self, level: Option<AnnotationType>, slug: &'a str, lint: T) -> Self
660 where
661 T: 'static + Lint,
662 {
663 self.lints.insert(slug, (level, Box::new(lint)));
664 self
665 }
666
667 pub fn allow(mut self, slug: &str) -> Self {
668 if self.lints.remove(slug).is_none() {
669 panic!("no lint with the slug: {}", slug);
670 }
671
672 self
673 }
674
675 pub fn clear_lints(mut self) -> Self {
676 self.lints.clear();
677 self
678 }
679
680 pub fn set_fetch<F>(mut self, fetch: F) -> Self
681 where
682 F: 'static + fetch::Fetch,
683 {
684 self.fetch = Box::new(fetch);
685 self
686 }
687}
688
689impl<'a, R> Linter<'a, R>
690where
691 R: Reporter,
692{
693 pub fn check_slice(mut self, origin: Option<&'a str>, src: &'a str) -> Self {
694 self.sources.push(Source::String { origin, src });
695 self
696 }
697
698 pub fn check_file(mut self, path: &'a Path) -> Self {
699 self.sources.push(Source::File(path));
700 self
701 }
702
703 pub async fn run(self) -> Result<R, Error> {
704 if self.lints.is_empty() {
705 panic!("no lints activated");
706 }
707
708 if self.sources.is_empty() {
709 panic!("no sources given");
710 }
711
712 let mut to_check = Vec::with_capacity(self.sources.len());
713 let mut fetched_tnips = HashMap::new();
714
715 for source in self.sources {
716 let source_origin = source.origin().map(Path::to_path_buf);
717 let source_content = source.fetch(&*self.fetch).await?;
718
719 to_check.push((source_origin, source_content));
720
721 let (source_origin, source_content) = to_check.last().unwrap();
722 let display_origin = source_origin.as_deref().map(Path::to_string_lossy);
723 let display_origin = display_origin.as_deref();
724
725 let arena = Arena::new();
726 let inner = match process(&reporters::Null, &arena, display_origin, source_content)? {
727 Some(i) => i,
728 None => continue,
729 };
730
731 for (slug, lint) in &self.lints {
732 let context = FetchContext {
733 body: inner.body,
734 preamble: &inner.preamble,
735 tnips: Default::default(),
736 };
737
738 lint.1
739 .find_resources(&context)
740 .with_context(|_| LintSnafu {
741 origin: source_origin.clone(),
742 })?;
743
744 let tnips = context.tnips.into_inner();
745
746 ensure!(
751 tnips.is_empty() || !source.is_string(),
752 SliceFetchedSnafu {
753 lint: *slug,
754 origin: source_origin.clone(),
755 }
756 );
757
758 for tnip in tnips.into_iter() {
759 let root = match source {
760 Source::File(p) => p.parent().unwrap_or_else(|| Path::new(".")),
761 _ => unreachable!(),
762 };
763
764 let path = root.join(tnip);
765
766 let entry = match fetched_tnips.entry(path) {
767 hash_map::Entry::Occupied(_) => continue,
768 hash_map::Entry::Vacant(v) => v,
769 };
770
771 let content = Source::File(entry.key()).fetch(&*self.fetch).await;
772 entry.insert(content);
773 }
774 }
775 }
776
777 let resources_arena = Arena::new();
778 let mut parsed_tnips = HashMap::new();
779
780 for (origin, result) in &fetched_tnips {
781 let source = match result {
782 Ok(o) => o,
783 Err(e) => {
784 parsed_tnips.insert(origin.as_path(), Err(e));
785 continue;
786 }
787 };
788
789 let inner = match process(&self.reporter, &resources_arena, None, source)? {
790 Some(s) => s,
791 None => return Ok(self.reporter),
792 };
793 parsed_tnips.insert(origin.as_path(), Ok(inner));
794 }
795
796 let mut lints: Vec<_> = self.lints.iter().collect();
797 lints.sort_by_key(|l| l.0);
798
799 for (origin, source) in &to_check {
800 let display_origin = origin.as_ref().map(|p| p.to_string_lossy().into_owned());
801 let display_origin = display_origin.as_deref();
802
803 let arena = Arena::new();
804 let inner = match process(&self.reporter, &arena, display_origin, source)? {
805 Some(i) => i,
806 None => continue,
807 };
808
809 let mut settings = LintSettings {
810 _p: std::marker::PhantomData,
811 default_annotation_type: AnnotationType::Error,
812 };
813
814 for modifier in &self.modifiers {
815 let context = Context {
816 inner: inner.clone(),
817 reporter: &self.reporter,
818 tnips: &parsed_tnips,
819 annotation_type: settings.default_annotation_type,
820 };
821
822 modifier.modify(&context, &mut settings)?;
823 }
824
825 for (slug, (annotation_type, lint)) in &lints {
826 let annotation_type = annotation_type.unwrap_or(settings.default_annotation_type);
827 let context = Context {
828 inner: inner.clone(),
829 reporter: &self.reporter,
830 tnips: &parsed_tnips,
831 annotation_type,
832 };
833
834 lint.lint(slug, &context).with_context(|_| LintSnafu {
835 origin: origin.clone(),
836 })?;
837 }
838 }
839
840 Ok(self.reporter)
841 }
842}
843
844fn process<'a>(
845 reporter: &dyn Reporter,
846 arena: &'a Arena<Node<'a, RefCell<Ast>>>,
847 origin: Option<&'a str>,
848 source: &'a str,
849) -> Result<Option<InnerContext<'a>>, Error> {
850 let (preamble_source, body_source) = match Preamble::split(source) {
851 Ok(v) => v,
852 Err(SplitError::MissingStart { .. }) | Err(SplitError::LeadingGarbage { .. }) => {
853 let mut footer = Vec::new();
854 if source.as_bytes().get(3) == Some(&b'\r') {
855 footer.push(Annotation {
856 id: None,
857 label: Some(
858 "found a carriage return (CR), use Unix-style line endings (LF) instead",
859 ),
860 annotation_type: AnnotationType::Help,
861 });
862 }
863 reporter
864 .report(Snippet {
865 title: Some(Annotation {
866 id: None,
867 label: Some("first line must be `---` exactly"),
868 annotation_type: AnnotationType::Error,
869 }),
870 slices: vec![Slice {
871 fold: false,
872 line_start: 1,
873 origin,
874 source: source.lines().next().unwrap_or_default(),
875 annotations: vec![],
876 }],
877 footer,
878 ..Default::default()
879 })
880 .map_err(LintError::from)
881 .with_context(|_| LintSnafu {
882 origin: origin.map(PathBuf::from),
883 })?;
884 return Ok(None);
885 }
886 Err(SplitError::MissingEnd { .. }) => {
887 reporter
888 .report(Snippet {
889 title: Some(Annotation {
890 id: None,
891 label: Some("preamble must be followed by a line containing `---` exactly"),
892 annotation_type: AnnotationType::Error,
893 }),
894 ..Default::default()
895 })
896 .map_err(LintError::from)
897 .with_context(|_| LintSnafu {
898 origin: origin.map(PathBuf::from),
899 })?;
900 return Ok(None);
901 }
902 };
903
904 let preamble = match Preamble::parse(origin, preamble_source) {
905 Ok(p) => p,
906 Err(e) => {
907 for snippet in e.into_errors() {
908 reporter
909 .report(snippet)
910 .map_err(LintError::from)
911 .with_context(|_| LintSnafu {
912 origin: origin.map(PathBuf::from),
913 })?;
914 }
915 Preamble::default()
916 }
917 };
918
919 let options = ComrakOptions {
920 extension: ComrakExtensionOptions {
921 table: true,
922 autolink: true,
923 footnotes: true,
924 ..Default::default()
925 },
926 ..Default::default()
927 };
928
929 let mut preamble_lines = preamble_source.matches('\n').count();
930 preamble_lines += 3;
931
932 let body = comrak::parse_document(arena, body_source, &options);
933
934 for node in body.descendants() {
935 let mut data = node.data.borrow_mut();
936 if data.sourcepos.start.line == 0 {
937 if let Some(parent) = node.parent() {
938 data.sourcepos.start.line = parent.data.borrow().sourcepos.start.line;
940 }
941 } else {
942 data.sourcepos.start.line += preamble_lines;
943 }
944 }
945
946 Ok(Some(InnerContext {
947 body,
948 source,
949 body_source,
950 preamble,
951 origin,
952 }))
953}
954
955#[cfg(test)]
956mod tests {
957 use super::*;
958
959 #[test]
960 fn lints_serialize_deserialize() {
961 type DefaultLints<S> = HashMap<S, DefaultLint<S>>;
962 let config: DefaultLints<&str> = default_lints_enum().collect();
963
964 let serialized = toml::to_string_pretty(&config).unwrap();
965 toml::from_str::<DefaultLints<String>>(&serialized).unwrap();
966 }
967
968 #[test]
969 fn modifiers_serialize_deserialize() {
970 #[derive(Debug, Serialize, Deserialize)]
971 struct Wrapper<S> {
972 modifiers: Vec<DefaultModifier<S>>,
973 }
974
975 let config = Wrapper {
976 modifiers: default_modifiers_enum(),
977 };
978
979 let serialized = toml::to_string_pretty(&config).unwrap();
980 toml::from_str::<Wrapper<String>>(&serialized).unwrap();
981 }
982
983 #[test]
984 fn options_serialize_deserialize() {
985 let options = Options {
986 lints: Some(default_lints_enum().collect::<HashMap<_, _>>()),
987 modifiers: Some(default_modifiers_enum()),
988 };
989
990 type StringOptions =
991 Options<Vec<DefaultModifier<String>>, HashMap<String, DefaultLint<String>>>;
992
993 let serialized = toml::to_string_pretty(&options).unwrap();
994 let actual = toml::from_str::<StringOptions>(&serialized).unwrap();
995 let iters = actual.to_iters();
996
997 #[allow(unused_must_use)]
998 {
999 Linter::with_options(reporters::Null, iters);
1000 }
1001 }
1002}