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