1use std::collections::BTreeMap;
42use std::ops::Range;
43
44use rowan::{TextRange, TextSize};
45use serde::Deserialize;
46use toml::Spanned;
47
48use crate::diag::{DiagCode, Diagnostic, FileId, Severity, Span};
49use crate::lint::{LintLevel, LintTable, lint_by_name};
50
51#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct Manifest {
71 pub kind: ManifestKind,
72 pub imports: BTreeMap<String, String>,
73 pub defaults: TimingDefaults,
74 pub lints: LintTable,
75 pub codegen_header_file: Option<(String, Range<usize>)>,
76}
77
78#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
81pub struct TimingDefaults {
82 pub timing: Option<String>,
84 pub command_timing: Option<String>,
86 pub query_timing: Option<String>,
88}
89
90impl TimingDefaults {
91 pub fn or(self, fallback: &TimingDefaults) -> TimingDefaults {
93 TimingDefaults {
94 timing: self.timing.or_else(|| fallback.timing.clone()),
95 command_timing: self
96 .command_timing
97 .or_else(|| fallback.command_timing.clone()),
98 query_timing: self.query_timing.or_else(|| fallback.query_timing.clone()),
99 }
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
105pub enum ManifestKind {
106 Package {
108 name: String,
109 name_span: Range<usize>,
112 version: String,
113 },
114 Workspace { members: Vec<String> },
116}
117
118pub fn parse_manifest(file_id: FileId, text: &str) -> (Option<Manifest>, Vec<Diagnostic>) {
125 let mut diags = Vec::new();
126
127 let raw: RawManifest = match toml::from_str(text) {
129 Ok(raw) => raw,
130 Err(err) => {
131 let range = err.span().unwrap_or(0..text.len());
132 let rendered = err.to_string();
139 let message = rendered
140 .lines()
141 .last()
142 .unwrap_or("invalid TOML")
143 .trim()
144 .to_string();
145 diags.push(error(DiagCode::MANI_001, file_id, range, message));
146 return (None, diags);
147 }
148 };
149
150 if raw.package.is_some() && raw.workspace.is_some() {
152 let range = raw
153 .workspace
154 .as_ref()
155 .map(Spanned::span)
156 .unwrap_or(0..text.len());
157 diags.push(error(
158 DiagCode::MANI_002,
159 file_id,
160 range,
161 "manifest declares both `[package]` and `[workspace]`; the two modes are mutually exclusive".to_string(),
162 ));
163 return (None, diags);
164 }
165 if raw.package.is_none() && raw.workspace.is_none() {
166 diags.push(error(
167 DiagCode::MANI_003,
168 file_id,
169 0..text.len(),
170 "manifest declares neither `[package]` nor `[workspace]`".to_string(),
171 ));
172 return (None, diags);
173 }
174
175 check_unknown_keys(file_id, text, &mut diags);
176
177 let imports = collect_imports(file_id, raw.imports, &mut diags);
178 let defaults = raw
181 .defaults
182 .map(|defaults| {
183 let raw = defaults.into_inner();
184 TimingDefaults {
185 timing: raw.timing,
186 command_timing: raw.command_timing,
187 query_timing: raw.query_timing,
188 }
189 })
190 .unwrap_or_default();
191 let lints = collect_lints(file_id, text, &mut diags);
192 let codegen_header_file = raw
193 .codegen
194 .and_then(|codegen| codegen.into_inner().header_file)
195 .map(|value| (value.get_ref().clone(), value.span()));
196
197 let kind = if let Some(pkg) = raw.package {
198 let section_span = pkg.span();
199 let raw_pkg = pkg.into_inner();
200 let (name, name_span) = match raw_pkg.name {
201 Some(name) => (name.get_ref().clone(), name.span()),
202 None => (String::new(), section_span),
203 };
204 if !is_valid_package_name(&name) {
205 diags.push(error(
206 DiagCode::MANI_006,
207 file_id,
208 name_span.clone(),
209 format!(
210 "invalid package name `{name}`; expected lowercase dot-separated segments (e.g. `veh.common`)"
211 ),
212 ));
213 }
214 ManifestKind::Package {
215 name,
216 name_span,
217 version: raw_pkg.version,
218 }
219 } else if let Some(ws) = raw.workspace {
220 ManifestKind::Workspace {
221 members: ws.into_inner().members,
222 }
223 } else {
224 return (None, diags);
226 };
227
228 (
229 Some(Manifest {
230 kind,
231 imports,
232 defaults,
233 lints,
234 codegen_header_file,
235 }),
236 diags,
237 )
238}
239
240#[derive(Deserialize)]
245struct RawManifest {
246 package: Option<Spanned<RawPackage>>,
247 workspace: Option<Spanned<RawWorkspace>>,
248 #[serde(default)]
249 imports: BTreeMap<String, Spanned<String>>,
250 defaults: Option<Spanned<RawDefaults>>,
251 codegen: Option<Spanned<RawCodegen>>,
252}
253
254#[derive(Deserialize)]
255struct RawDefaults {
256 timing: Option<String>,
257 command_timing: Option<String>,
258 query_timing: Option<String>,
259}
260
261#[derive(Deserialize)]
262struct RawCodegen {
263 #[serde(rename = "header-file")]
264 header_file: Option<Spanned<String>>,
265}
266
267#[derive(Deserialize)]
268struct RawPackage {
269 name: Option<Spanned<String>>,
270 #[serde(default)]
271 version: String,
272}
273
274#[derive(Deserialize)]
275struct RawWorkspace {
276 #[serde(default)]
277 members: Vec<String>,
278}
279
280#[derive(Deserialize)]
286struct RawLints {
287 #[serde(default)]
288 lints: Option<BTreeMap<Spanned<String>, Spanned<toml::Value>>>,
289}
290
291fn collect_lints(file_id: FileId, text: &str, diags: &mut Vec<Diagnostic>) -> LintTable {
297 let mut lints = LintTable::new();
298 let raw: RawLints = match toml::from_str(text) {
299 Ok(raw) => raw,
300 Err(_) => {
301 let doc: BTreeMap<String, Spanned<toml::Value>> =
306 toml::from_str(text).unwrap_or_default();
307 let range = doc.get("lints").map(Spanned::span).unwrap_or(0..text.len());
308 diags.push(warning(
309 DiagCode::MANI_010,
310 file_id,
311 range,
312 "`[lints]` must be a table".to_string(),
313 ));
314 return lints;
315 }
316 };
317 for (key, value) in raw.lints.unwrap_or_default() {
318 let name = key.get_ref();
319 let Some(registered) = lint_by_name(name).and_then(|entry| entry.lint) else {
320 diags.push(warning(
321 DiagCode::MANI_010,
322 file_id,
323 key.span(),
324 format!("unknown lint `{name}` in `[lints]`"),
325 ));
326 continue;
327 };
328 let Some(level) = value.get_ref().as_str().and_then(LintLevel::parse) else {
329 diags.push(warning(
330 DiagCode::MANI_010,
331 file_id,
332 key.span(),
333 format!("`[lints].{name}` must be one of \"allow\", \"info\", \"warn\", \"deny\""),
334 ));
335 continue;
336 };
337 lints.insert(registered, level);
338 }
339 lints
340}
341
342fn collect_imports(
346 file_id: FileId,
347 raw: BTreeMap<String, Spanned<String>>,
348 diags: &mut Vec<Diagnostic>,
349) -> BTreeMap<String, String> {
350 let mut imports = BTreeMap::new();
351 for (key, url) in raw {
352 let value = url.get_ref().clone();
353 if !is_valid_import_url(&value) {
354 diags.push(error(
355 DiagCode::MANI_007,
356 file_id,
357 url.span(),
358 format!("invalid import URL `{value}` for `{key}`; expected an `http://` or `https://` URL with a host"),
359 ));
360 }
361 imports.insert(key, value);
362 }
363 imports
364}
365
366fn check_unknown_keys(file_id: FileId, text: &str, diags: &mut Vec<Diagnostic>) {
371 let doc: BTreeMap<String, Spanned<toml::Value>> = match toml::from_str(text) {
372 Ok(doc) => doc,
373 Err(_) => return,
375 };
376 for (key, value) in &doc {
382 match key.as_str() {
383 "package" => check_section_keys(file_id, "package", value, &["name", "version"], diags),
384 "workspace" => check_section_keys(file_id, "workspace", value, &["members"], diags),
385 "defaults" => check_section_keys(
386 file_id,
387 "defaults",
388 value,
389 &["timing", "command_timing", "query_timing"],
390 diags,
391 ),
392 "codegen" => check_section_keys(file_id, "codegen", value, &["header-file"], diags),
393 "imports" => {}
394 "lints" => {}
395 _ => diags.push(warning(
396 DiagCode::MANI_005,
397 file_id,
398 value.span(),
399 format!("unknown manifest key `{key}`"),
400 )),
401 }
402 }
403}
404
405fn check_section_keys(
409 file_id: FileId,
410 section: &str,
411 value: &Spanned<toml::Value>,
412 allowed: &[&str],
413 diags: &mut Vec<Diagnostic>,
414) {
415 let Some(table) = value.get_ref().as_table() else {
416 return;
417 };
418 for key in table.keys() {
419 if !allowed.contains(&key.as_str()) {
420 diags.push(warning(
421 DiagCode::MANI_005,
422 file_id,
423 value.span(),
424 format!("unknown key `{key}` in `[{section}]`"),
425 ));
426 }
427 }
428}
429
430fn is_valid_package_name(name: &str) -> bool {
434 !name.is_empty() && name.split('.').all(is_valid_name_segment)
435}
436
437fn is_valid_name_segment(segment: &str) -> bool {
438 let mut chars = segment.chars();
439 match chars.next() {
440 Some(first) if first.is_ascii_lowercase() => {}
441 _ => return false,
442 }
443 chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
444}
445
446fn is_valid_import_url(url: &str) -> bool {
451 let Some(rest) = url
452 .strip_prefix("https://")
453 .or_else(|| url.strip_prefix("http://"))
454 else {
455 return false;
456 };
457 let host_end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
458 !rest[..host_end].is_empty()
459}
460
461fn error(code: DiagCode, file: FileId, range: Range<usize>, message: String) -> Diagnostic {
464 diagnostic(code, Severity::Error, file, range, message)
465}
466
467fn warning(code: DiagCode, file: FileId, range: Range<usize>, message: String) -> Diagnostic {
469 diagnostic(code, Severity::Warning, file, range, message)
470}
471
472fn diagnostic(
473 code: DiagCode,
474 severity: Severity,
475 file: FileId,
476 range: Range<usize>,
477 message: String,
478) -> Diagnostic {
479 Diagnostic {
480 code,
481 severity,
482 message,
483 primary: Span {
484 file,
485 range: to_text_range(range),
486 },
487 labels: Vec::new(),
488 fixits: Vec::new(),
489 }
490}
491
492fn to_text_range(range: Range<usize>) -> TextRange {
495 TextRange::new(
496 TextSize::from(range.start as u32),
497 TextSize::from(range.end as u32),
498 )
499}
500
501#[cfg(test)]
502mod tests {
503 use super::*;
504 use crate::diag::{MANI_CATALOG, SourceMap};
505
506 const STANDALONE: &str = "\
507[package]
508name = \"veh.common\"
509version = \"1.2.0\"
510
511[imports]
512\"some.dep\" = \"https://ridl.example.com/some/dep@v1.0.0\"
513";
514
515 const WORKSPACE: &str = "\
516[workspace]
517members = [\"veh-common\", \"veh-cluster\", \"veh-adas\"]
518
519[imports]
520\"third-party.foo\" = \"https://ridl.example.com/third-party/foo@v1.0.0\"
521";
522
523 fn parse(text: &str) -> (Option<Manifest>, Vec<Diagnostic>) {
524 let mut map = SourceMap::new();
525 let file_id = map.file_id("ridl.toml", text);
526 parse_manifest(file_id, text)
527 }
528
529 fn codes(diags: &[Diagnostic]) -> Vec<&str> {
530 diags.iter().map(|d| d.code.as_str()).collect()
531 }
532
533 #[test]
534 fn adr0002_standalone_example_parses() {
535 let (manifest, diags) = parse(STANDALONE);
536 assert!(diags.is_empty(), "clean standalone manifest, got {diags:?}");
537 let manifest = manifest.expect("standalone manifest parses");
538 assert_eq!(
539 manifest.kind,
540 ManifestKind::Package {
541 name: "veh.common".to_string(),
542 name_span: range_of(STANDALONE, "\"veh.common\""),
543 version: "1.2.0".to_string(),
544 },
545 );
546 assert_eq!(manifest.imports.len(), 1);
547 assert_eq!(
548 manifest.imports.get("some.dep").map(String::as_str),
549 Some("https://ridl.example.com/some/dep@v1.0.0"),
550 );
551 }
552
553 #[test]
554 fn adr0002_workspace_example_parses() {
555 let (manifest, diags) = parse(WORKSPACE);
556 assert!(diags.is_empty(), "clean workspace manifest, got {diags:?}");
557 let manifest = manifest.expect("workspace manifest parses");
558 assert_eq!(
559 manifest.kind,
560 ManifestKind::Workspace {
561 members: vec![
562 "veh-common".to_string(),
563 "veh-cluster".to_string(),
564 "veh-adas".to_string(),
565 ],
566 },
567 );
568 assert_eq!(
569 manifest.imports.get("third-party.foo").map(String::as_str),
570 Some("https://ridl.example.com/third-party/foo@v1.0.0"),
571 );
572 }
573
574 #[test]
575 fn mani_001_invalid_toml() {
576 let (manifest, diags) = parse("[package]\nname\n");
578 assert!(manifest.is_none(), "invalid TOML yields no manifest");
579 assert_eq!(codes(&diags), vec!["MANI-001"]);
580 assert_eq!(diags[0].severity, Severity::Error);
581 assert!(
584 diags[0].message.contains("expected `=`"),
585 "MANI-001 message must carry the reason, got {:?}",
586 diags[0].message,
587 );
588 assert!(
589 !diags[0].message.contains("TOML parse error at line"),
590 "MANI-001 message must not be the location header, got {:?}",
591 diags[0].message,
592 );
593 }
594
595 #[test]
596 fn mani_001_carries_type_error_reason() {
597 let (manifest, diags) = parse("[package]\nname = 123\nversion = \"1.0.0\"\n");
599 assert!(manifest.is_none());
600 assert_eq!(codes(&diags), vec!["MANI-001"]);
601 assert!(
602 diags[0].message.contains("invalid type"),
603 "MANI-001 message must carry the type-mismatch reason, got {:?}",
604 diags[0].message,
605 );
606 assert!(!diags[0].message.contains("TOML parse error at line"));
607 }
608
609 #[test]
610 fn mani_002_both_sections() {
611 let text = "\
612[package]
613name = \"veh.common\"
614version = \"1.0.0\"
615
616[workspace]
617members = [\"a\"]
618";
619 let (manifest, diags) = parse(text);
620 assert!(manifest.is_none(), "an ambiguous mode yields no manifest");
621 assert_eq!(codes(&diags), vec!["MANI-002"]);
622 assert_eq!(diags[0].severity, Severity::Error);
623 }
624
625 #[test]
626 fn mani_003_neither_section() {
627 let text = "[imports]\n\"x.y\" = \"https://example.com/x\"\n";
629 let (manifest, diags) = parse(text);
630 assert!(manifest.is_none(), "a modeless manifest yields no manifest");
631 assert_eq!(codes(&diags), vec!["MANI-003"]);
632
633 let (empty, empty_diags) = parse("");
635 assert!(empty.is_none());
636 assert_eq!(codes(&empty_diags), vec!["MANI-003"]);
637 }
638
639 #[test]
640 fn mani_004_workspace_manifest_parses_clean_in_isolation() {
641 let (manifest, diags) = parse(WORKSPACE);
645 assert!(diags.is_empty(), "no MANI-004 from the standalone parser");
646 assert!(matches!(
647 manifest.expect("workspace parses").kind,
648 ManifestKind::Workspace { .. },
649 ));
650 assert!(
652 MANI_CATALOG
653 .iter()
654 .any(|entry| entry.code == DiagCode::MANI_004),
655 "MANI-004 is defined for the loader",
656 );
657 }
658
659 #[test]
660 fn mani_005_unknown_key_warns_but_parses() {
661 let text = "\
663[package]
664name = \"veh.common\"
665version = \"1.0.0\"
666description = \"not a known key\"
667";
668 let (manifest, diags) = parse(text);
669 let manifest = manifest.expect("unknown key still parses");
670 assert_eq!(
671 manifest.kind,
672 ManifestKind::Package {
673 name: "veh.common".to_string(),
674 name_span: range_of(text, "\"veh.common\""),
675 version: "1.0.0".to_string(),
676 },
677 );
678 assert_eq!(codes(&diags), vec!["MANI-005"]);
679 assert_eq!(diags[0].severity, Severity::Warning);
680
681 let top = "\
683[package]
684name = \"veh.common\"
685version = \"1.0.0\"
686
687[bogus]
688x = 1
689";
690 let (top_manifest, top_diags) = parse(top);
691 assert!(top_manifest.is_some(), "unknown top-level key still parses");
692 assert_eq!(codes(&top_diags), vec!["MANI-005"]);
693 assert_eq!(top_diags[0].severity, Severity::Warning);
694 }
695
696 #[test]
697 fn mani_006_invalid_package_name() {
698 let text = "[package]\nname = \"Veh.Common\"\nversion = \"1.0.0\"\n";
700 let (manifest, diags) = parse(text);
701 assert!(manifest.is_some(), "a bad name is a diagnostic, not None");
702 assert_eq!(codes(&diags), vec!["MANI-006"]);
703 assert_eq!(diags[0].severity, Severity::Error);
704
705 let no_name = "[package]\nversion = \"1.0.0\"\n";
707 let (_, no_name_diags) = parse(no_name);
708 assert_eq!(codes(&no_name_diags), vec!["MANI-006"]);
709 }
710
711 #[test]
712 fn defaults_timing_is_recorded_unparsed() {
713 let text = "\
716[package]
717name = \"veh.common\"
718version = \"1.0.0\"
719
720[defaults]
721timing = \"[50ms..2s]\"
722";
723 let (manifest, diags) = parse(text);
724 assert!(
725 diags.is_empty(),
726 "a `[defaults]` section is known, got {diags:?}"
727 );
728 let manifest = manifest.expect("the manifest parses");
729 assert_eq!(manifest.defaults.timing.as_deref(), Some("[50ms..2s]"));
730 }
731
732 #[test]
733 fn defaults_command_and_query_timing_parse() {
734 let text = "\
735[package]
736name = \"veh.common\"
737version = \"1.0.0\"
738
739[defaults]
740timing = \"[100ms..1s]\"
741command_timing = \"[..1s]\"
742query_timing = \"[..3s]\"
743";
744 let (manifest, diags) = parse(text);
745 assert!(diags.is_empty(), "known keys draw nothing, got {diags:?}");
746 assert_eq!(
747 manifest.expect("parses").defaults,
748 TimingDefaults {
749 timing: Some("[100ms..1s]".to_string()),
750 command_timing: Some("[..1s]".to_string()),
751 query_timing: Some("[..3s]".to_string()),
752 }
753 );
754 }
755
756 #[test]
760 fn timing_defaults_or_resolves_each_key_on_its_own() {
761 let all = |prefix: &str| TimingDefaults {
762 timing: Some(format!("{prefix}-timing")),
763 command_timing: Some(format!("{prefix}-command")),
764 query_timing: Some(format!("{prefix}-query")),
765 };
766 assert_eq!(all("own").or(&all("fallback")), all("own"));
768 assert_eq!(
770 TimingDefaults::default().or(&all("fallback")),
771 all("fallback")
772 );
773 let fallback = all("fallback");
776 let own = || Some("own".to_string());
777 let cases = [
778 (
779 TimingDefaults {
780 timing: own(),
781 ..TimingDefaults::default()
782 },
783 TimingDefaults {
784 timing: own(),
785 ..fallback.clone()
786 },
787 ),
788 (
789 TimingDefaults {
790 command_timing: own(),
791 ..TimingDefaults::default()
792 },
793 TimingDefaults {
794 command_timing: own(),
795 ..fallback.clone()
796 },
797 ),
798 (
799 TimingDefaults {
800 query_timing: own(),
801 ..TimingDefaults::default()
802 },
803 TimingDefaults {
804 query_timing: own(),
805 ..fallback.clone()
806 },
807 ),
808 ];
809 for (own, expected) in cases {
810 assert_eq!(own.clone().or(&fallback), expected, "{own:?}");
811 }
812 assert_eq!(
814 TimingDefaults::default().or(&TimingDefaults::default()),
815 TimingDefaults::default()
816 );
817 }
818
819 #[test]
820 fn no_defaults_section_leaves_the_defaults_absent() {
821 let (manifest, diags) = parse(STANDALONE);
822 assert!(diags.is_empty());
823 assert_eq!(
824 manifest.expect("parses").defaults,
825 TimingDefaults::default(),
826 "no `[defaults]` means no configured default",
827 );
828 }
829
830 #[test]
831 fn unknown_key_inside_defaults_still_warns() {
832 let text = "\
833[package]
834name = \"veh.common\"
835version = \"1.0.0\"
836
837[defaults]
838timing = \"[100ms..1000ms]\"
839command_timing = \"[..1s]\"
840query_timing = \"[..3s]\"
841bogus = 1
842";
843 let (manifest, diags) = parse(text);
844 assert!(manifest.is_some(), "an unknown nested key still parses");
845 assert_eq!(codes(&diags), vec!["MANI-005"]);
846 assert_eq!(diags[0].severity, Severity::Warning);
847 }
848
849 #[test]
850 fn mani_007_invalid_import_url() {
851 let text = "\
852[package]
853name = \"veh.common\"
854version = \"1.0.0\"
855
856[imports]
857\"some.dep\" = \"not-a-url\"
858";
859 let (manifest, diags) = parse(text);
860 let manifest = manifest.expect("a bad URL is a diagnostic, not None");
861 assert_eq!(
863 manifest.imports.get("some.dep").map(String::as_str),
864 Some("not-a-url"),
865 );
866 assert_eq!(codes(&diags), vec!["MANI-007"]);
867 assert_eq!(diags[0].severity, Severity::Error);
868
869 let http = "\
871[package]
872name = \"veh.common\"
873version = \"1.0.0\"
874
875[imports]
876\"some.dep\" = \"http://example.com/some/dep\"
877";
878 let (_, http_diags) = parse(http);
879 assert!(
880 http_diags.is_empty(),
881 "http:// with a host is a valid URL, got {http_diags:?}",
882 );
883 }
884
885 const PACKAGE_HEAD: &str = "[package]\nname = \"veh.common\"\nversion = \"1.0.0\"\n\n";
886 const WORKSPACE_HEAD: &str = "[workspace]\nmembers = [\"a\"]\n\n";
887
888 fn range_of(text: &str, needle: &str) -> Range<usize> {
890 let start = text.find(needle).expect("the needle occurs in the text");
891 start..start + needle.len()
892 }
893
894 fn spanned_text<'a>(text: &'a str, diag: &Diagnostic) -> &'a str {
896 let range = diag.primary.range;
897 &text[usize::from(range.start())..usize::from(range.end())]
898 }
899
900 #[test]
901 fn lints_table_is_read() {
902 for head in [PACKAGE_HEAD, WORKSPACE_HEAD] {
903 let text = format!("{head}[lints]\nmissing-timing = \"deny\"\n");
904 let (manifest, diags) = parse(&text);
905 assert!(diags.is_empty(), "a valid `[lints]` table, got {diags:?}");
906 let manifest = manifest.expect("the manifest parses");
907 assert_eq!(manifest.lints.get("missing-timing"), Some(&LintLevel::Deny));
908 assert_eq!(manifest.lints.len(), 1);
909 }
910 }
911
912 #[test]
913 fn lints_key_is_known() {
914 let text = format!("{PACKAGE_HEAD}[lints]\n");
915 let (manifest, diags) = parse(&text);
916 assert!(diags.is_empty(), "no MANI-005 for `lints`, got {diags:?}");
917 assert!(manifest.expect("the manifest parses").lints.is_empty());
918 }
919
920 #[test]
921 fn no_lints_table_leaves_lints_empty() {
922 let (manifest, diags) = parse(STANDALONE);
923 assert!(diags.is_empty());
924 assert!(manifest.expect("parses").lints.is_empty());
925 }
926
927 #[test]
928 fn unknown_lint_name() {
929 let text = format!("{PACKAGE_HEAD}[lints]\nnope = \"deny\"\n");
930 let (manifest, diags) = parse(&text);
931 assert_eq!(codes(&diags), vec!["MANI-010"]);
932 assert_eq!(diags[0].severity, Severity::Warning);
933 assert_eq!(diags[0].message, "unknown lint `nope` in `[lints]`");
934 assert!(
935 manifest.expect("the manifest parses").lints.is_empty(),
936 "an unknown lint is not recorded",
937 );
938 }
939
940 #[test]
941 fn code_as_key() {
942 let text = format!("{PACKAGE_HEAD}[lints]\n\"RIDL-100\" = \"deny\"\n");
943 let (manifest, diags) = parse(&text);
944 assert_eq!(codes(&diags), vec!["MANI-010"]);
945 assert_eq!(diags[0].message, "unknown lint `RIDL-100` in `[lints]`");
946 assert!(manifest.expect("the manifest parses").lints.is_empty());
947 }
948
949 #[test]
950 fn error_code_as_key() {
951 let text = format!("{PACKAGE_HEAD}[lints]\n\"RIDL-101\" = \"allow\"\n");
954 let (manifest, diags) = parse(&text);
955 assert_eq!(codes(&diags), vec!["MANI-010"]);
956 assert!(manifest.expect("the manifest parses").lints.is_empty());
957 }
958
959 #[test]
960 fn bad_level() {
961 for value in ["\"Deny\"", "3"] {
962 let text = format!("{PACKAGE_HEAD}[lints]\nmissing-timing = {value}\n");
963 let (manifest, diags) = parse(&text);
964 assert_eq!(codes(&diags), vec!["MANI-010"], "value {value}");
965 assert_eq!(diags[0].severity, Severity::Warning);
966 assert_eq!(
967 diags[0].message,
968 "`[lints].missing-timing` must be one of \"allow\", \"info\", \"warn\", \"deny\"",
969 );
970 assert_eq!(spanned_text(&text, &diags[0]), "missing-timing");
971 assert!(
972 manifest.expect("the manifest parses").lints.is_empty(),
973 "an entry with a bad level is not recorded",
974 );
975 }
976 }
977
978 #[test]
979 fn valid_entries_survive_a_bad_sibling() {
980 let text = format!("{PACKAGE_HEAD}[lints]\nmissing-timing = \"deny\"\nnope = \"warn\"\n");
981 let (manifest, diags) = parse(&text);
982 assert_eq!(codes(&diags), vec!["MANI-010"]);
983 let manifest = manifest.expect("the manifest parses");
984 assert_eq!(manifest.lints.get("missing-timing"), Some(&LintLevel::Deny));
985 assert_eq!(manifest.lints.len(), 1);
986 }
987
988 #[test]
989 fn lints_not_a_table() {
990 let text = format!("lints = 1\n\n{PACKAGE_HEAD}");
992 let (manifest, diags) = parse(&text);
993 assert_eq!(codes(&diags), vec!["MANI-010"], "no MANI-001, no MANI-005");
994 assert_eq!(diags[0].message, "`[lints]` must be a table");
995 assert_eq!(spanned_text(&text, &diags[0]), "1");
996 let manifest = manifest.expect("the rest of the manifest is still read");
997 assert_eq!(
998 manifest.kind,
999 ManifestKind::Package {
1000 name: "veh.common".to_string(),
1001 name_span: range_of(&text, "\"veh.common\""),
1002 version: "1.0.0".to_string(),
1003 },
1004 );
1005 assert!(manifest.lints.is_empty());
1006 }
1007
1008 #[test]
1009 fn mani_010_span_is_the_key() {
1010 let text = format!("{PACKAGE_HEAD}[lints]\nnope = \"deny\"\n");
1011 let (_, diags) = parse(&text);
1012 assert_eq!(codes(&diags), vec!["MANI-010"]);
1013 assert_eq!(spanned_text(&text, &diags[0]), "nope");
1014 }
1015
1016 #[test]
1017 fn codegen_header_file_is_parsed() {
1018 let text = format!("{PACKAGE_HEAD}[codegen]\nheader-file = \"H.txt\"\n");
1019 let (manifest, diags) = parse(&text);
1020 assert!(diags.is_empty(), "no diagnostic, got {diags:?}");
1021 let (path, range) = manifest
1022 .expect("the manifest parses")
1023 .codegen_header_file
1024 .expect("header-file is read");
1025 assert_eq!(path, "H.txt");
1026 assert_eq!(&text[range], "\"H.txt\"");
1027 }
1028
1029 #[test]
1030 fn unknown_codegen_key_is_mani_005() {
1031 let text = format!("{PACKAGE_HEAD}[codegen]\nheader = \"x\"\n");
1032 let (_, diags) = parse(&text);
1033 assert_eq!(codes(&diags), vec!["MANI-005"]);
1034 }
1035}