1use std::collections::{HashMap, HashSet};
7
8use crate::cst::{CabalCst, CstNodeKind};
9use crate::diagnostic::Diagnostic;
10use crate::span::{NodeId, Span};
11
12pub fn validate(cst: &CabalCst) -> Vec<Diagnostic> {
15 let mut diagnostics = Vec::new();
16
17 let ctx = ValidationContext::collect(cst);
18
19 check_required_fields(cst, &ctx, &mut diagnostics);
20 check_cabal_version_first(cst, &ctx, &mut diagnostics);
21 check_cabal_version_value(cst, &ctx, &mut diagnostics);
22 check_duplicate_top_level_fields(cst, &ctx, &mut diagnostics);
23 check_duplicate_section_fields(cst, &ctx, &mut diagnostics);
24 check_duplicate_sections(cst, &ctx, &mut diagnostics);
25 check_import_references(cst, &ctx, &mut diagnostics);
26 check_build_type(cst, &ctx, &mut diagnostics);
27 check_library_exposed_modules(cst, &ctx, &mut diagnostics);
28
29 diagnostics
30}
31
32fn canonicalize_field_name(name: &str) -> String {
38 name.to_ascii_lowercase().replace('_', "-")
39}
40
41fn get_field_value(cst: &CabalCst, node_id: NodeId) -> Option<&str> {
47 let node = cst.node(node_id);
48 node.field_value.map(|span| span.slice(&cst.source).trim())
49}
50
51#[derive(Debug)]
58struct FieldInfo {
59 canonical_name: String,
60 name_span: Span,
61 node_id: NodeId,
62}
63
64#[derive(Debug)]
66struct SectionInfo {
67 keyword: String,
68 arg: Option<String>,
69 keyword_span: Span,
70 node_id: NodeId,
71}
72
73#[derive(Debug)]
76struct ValidationContext {
77 top_level_fields: Vec<FieldInfo>,
79 sections: Vec<SectionInfo>,
81 common_stanza_names: HashSet<String>,
83 imports: Vec<(String, Span, NodeId)>,
85 section_fields: HashMap<usize, Vec<FieldInfo>>,
87}
88
89impl ValidationContext {
90 fn collect(cst: &CabalCst) -> Self {
92 let mut ctx = ValidationContext {
93 top_level_fields: Vec::new(),
94 sections: Vec::new(),
95 common_stanza_names: HashSet::new(),
96 imports: Vec::new(),
97 section_fields: HashMap::new(),
98 };
99
100 let root = cst.node(cst.root);
101 for &child_id in &root.children {
102 let child = cst.node(child_id);
103 match child.kind {
104 CstNodeKind::Field => {
105 if let Some(name_span) = child.field_name {
106 ctx.top_level_fields.push(FieldInfo {
107 canonical_name: canonicalize_field_name(name_span.slice(&cst.source)),
108 name_span,
109 node_id: child_id,
110 });
111 }
112 }
113 CstNodeKind::Section => {
114 let keyword = child
115 .section_keyword
116 .map(|s| s.slice(&cst.source).to_ascii_lowercase())
117 .unwrap_or_default();
118 let arg = child.section_arg.map(|s| s.slice(&cst.source).to_string());
119
120 let keyword_span = child.section_keyword.unwrap_or(child.content_span);
121
122 if keyword == "common" {
123 if let Some(ref name) = arg {
124 ctx.common_stanza_names.insert(name.clone());
125 }
126 }
127
128 ctx.sections.push(SectionInfo {
129 keyword: keyword.clone(),
130 arg,
131 keyword_span,
132 node_id: child_id,
133 });
134
135 ctx.collect_section_children(cst, child_id);
137 }
138 _ => {}
139 }
140 }
141
142 ctx
143 }
144
145 fn collect_section_children(&mut self, cst: &CabalCst, section_id: NodeId) {
148 let section = cst.node(section_id);
149 for &child_id in §ion.children {
150 let child = cst.node(child_id);
151 match child.kind {
152 CstNodeKind::Field => {
153 if let Some(name_span) = child.field_name {
154 self.section_fields
155 .entry(section_id.0)
156 .or_default()
157 .push(FieldInfo {
158 canonical_name: canonicalize_field_name(
159 name_span.slice(&cst.source),
160 ),
161 name_span,
162 node_id: child_id,
163 });
164 }
165 }
166 CstNodeKind::Import => {
167 if let Some(val_span) = child.field_value {
168 let value = val_span.slice(&cst.source).trim().to_string();
169 self.imports.push((value, val_span, child_id));
170 }
171 }
172 CstNodeKind::Section => {
173 let keyword = child
177 .section_keyword
178 .map(|s| s.slice(&cst.source).to_ascii_lowercase())
179 .unwrap_or_default();
180 let arg = child.section_arg.map(|s| s.slice(&cst.source).to_string());
181 let keyword_span = child.section_keyword.unwrap_or(child.content_span);
182
183 if keyword == "common" {
184 if let Some(ref name) = arg {
185 self.common_stanza_names.insert(name.clone());
186 }
187 }
188
189 self.sections.push(SectionInfo {
190 keyword,
191 arg,
192 keyword_span,
193 node_id: child_id,
194 });
195
196 self.collect_section_children(cst, child_id);
198 }
199 CstNodeKind::Conditional => {
200 self.collect_conditional_children(cst, child_id);
202 }
203 _ => {}
204 }
205 }
206 }
207
208 fn collect_conditional_children(&mut self, cst: &CabalCst, cond_id: NodeId) {
210 let cond = cst.node(cond_id);
211 for &child_id in &cond.children {
212 let child = cst.node(child_id);
213 match child.kind {
214 CstNodeKind::Field => {
215 }
219 CstNodeKind::Import => {
220 if let Some(val_span) = child.field_value {
221 let value = val_span.slice(&cst.source).trim().to_string();
222 self.imports.push((value, val_span, child_id));
223 }
224 }
225 CstNodeKind::ElseBlock => {
226 let else_node = cst.node(child_id);
228 for &else_child_id in &else_node.children {
229 let else_child = cst.node(else_child_id);
230 if else_child.kind == CstNodeKind::Import {
231 if let Some(val_span) = else_child.field_value {
232 let value = val_span.slice(&cst.source).trim().to_string();
233 self.imports.push((value, val_span, else_child_id));
234 }
235 }
236 }
237 }
238 CstNodeKind::Conditional => {
239 self.collect_conditional_children(cst, child_id);
240 }
241 _ => {}
242 }
243 }
244 }
245}
246
247fn check_required_fields(
254 _cst: &CabalCst,
255 ctx: &ValidationContext,
256 diagnostics: &mut Vec<Diagnostic>,
257) {
258 let required = ["cabal-version", "name", "version"];
259 for &field_name in &required {
260 let found = ctx
261 .top_level_fields
262 .iter()
263 .any(|f| f.canonical_name == field_name);
264 if !found {
265 diagnostics.push(Diagnostic::error(
267 Span::new(0, 0),
268 format!("missing required field: `{field_name}`"),
269 ));
270 }
271 }
272}
273
274fn check_cabal_version_first(
276 cst: &CabalCst,
277 ctx: &ValidationContext,
278 diagnostics: &mut Vec<Diagnostic>,
279) {
280 let root = cst.node(cst.root);
283 let first_field_id = root.children.iter().find(|&&child_id| {
284 let child = cst.node(child_id);
285 child.kind == CstNodeKind::Field
286 });
287
288 let Some(&first_id) = first_field_id else {
289 return; };
291
292 let first_node = cst.node(first_id);
293 let Some(name_span) = first_node.field_name else {
294 return;
295 };
296
297 let name = canonicalize_field_name(name_span.slice(&cst.source));
298 if name != "cabal-version" {
299 if let Some(cv) = ctx
301 .top_level_fields
302 .iter()
303 .find(|f| f.canonical_name == "cabal-version")
304 {
305 diagnostics.push(Diagnostic::warning(
306 cv.name_span,
307 "`cabal-version` should be the first field in the file",
308 ));
309 }
310 }
311}
312
313fn check_cabal_version_value(
315 cst: &CabalCst,
316 ctx: &ValidationContext,
317 diagnostics: &mut Vec<Diagnostic>,
318) {
319 let Some(cv_field) = ctx
320 .top_level_fields
321 .iter()
322 .find(|f| f.canonical_name == "cabal-version")
323 else {
324 return; };
326
327 let Some(raw_value) = get_field_value(cst, cv_field.node_id) else {
328 return;
329 };
330
331 let version_str = raw_value.strip_prefix(">=").unwrap_or(raw_value).trim();
333
334 let is_valid_version =
336 !version_str.is_empty() && version_str.chars().all(|c| c.is_ascii_digit() || c == '.');
337
338 if !is_valid_version {
339 let val_span = cst
340 .node(cv_field.node_id)
341 .field_value
342 .unwrap_or(cv_field.name_span);
343 diagnostics.push(Diagnostic::warning(
344 val_span,
345 format!("unrecognized `cabal-version` value: `{raw_value}`"),
346 ));
347 }
348}
349
350fn check_duplicate_top_level_fields(
352 _cst: &CabalCst,
353 ctx: &ValidationContext,
354 diagnostics: &mut Vec<Diagnostic>,
355) {
356 check_duplicates_in_field_list(&ctx.top_level_fields, diagnostics);
357}
358
359fn check_duplicate_section_fields(
361 _cst: &CabalCst,
362 ctx: &ValidationContext,
363 diagnostics: &mut Vec<Diagnostic>,
364) {
365 for fields in ctx.section_fields.values() {
366 check_duplicates_in_field_list(fields, diagnostics);
367 }
368}
369
370const REPEATABLE_FIELDS: &[&str] = &[
373 "build-depends",
374 "exposed-modules",
375 "other-modules",
376 "default-extensions",
377 "other-extensions",
378 "ghc-options",
379 "ghc-prof-options",
380 "ghc-shared-options",
381 "pkgconfig-depends",
382 "extra-libraries",
383 "extra-lib-dirs",
384 "extra-framework-dirs",
385 "frameworks",
386 "build-tool-depends",
387 "build-tools",
388 "mixins",
389 "hs-source-dirs",
390 "includes",
391 "include-dirs",
392 "c-sources",
393 "cxx-sources",
394 "js-sources",
395 "extra-ghci-libraries",
396 "extra-bundled-libraries",
397 "autogen-modules",
398 "virtual-modules",
399 "reexported-modules",
400 "signatures",
401];
402
403fn check_duplicates_in_field_list(fields: &[FieldInfo], diagnostics: &mut Vec<Diagnostic>) {
405 let mut seen: HashMap<&str, Span> = HashMap::new();
406 for field in fields {
407 if REPEATABLE_FIELDS.contains(&field.canonical_name.as_str()) {
408 continue;
409 }
410 if let Some(&first_span) = seen.get(field.canonical_name.as_str()) {
411 let _ = first_span; diagnostics.push(Diagnostic::warning(
413 field.name_span,
414 format!("duplicate field: `{}`", field.canonical_name),
415 ));
416 } else {
417 seen.insert(&field.canonical_name, field.name_span);
418 }
419 }
420}
421
422fn check_duplicate_sections(
424 _cst: &CabalCst,
425 ctx: &ValidationContext,
426 diagnostics: &mut Vec<Diagnostic>,
427) {
428 let mut seen: HashMap<(String, Option<String>), Span> = HashMap::new();
431 for section in &ctx.sections {
432 let key = (section.keyword.clone(), section.arg.clone());
433 if let Some(&_first_span) = seen.get(&key) {
434 let label = match §ion.arg {
435 Some(arg) => format!("`{} {}`", section.keyword, arg),
436 None => format!("`{}`", section.keyword),
437 };
438 diagnostics.push(Diagnostic::error(
439 section.keyword_span,
440 format!("duplicate section: {label}"),
441 ));
442 } else {
443 seen.insert(key, section.keyword_span);
444 }
445 }
446}
447
448fn check_import_references(
450 _cst: &CabalCst,
451 ctx: &ValidationContext,
452 diagnostics: &mut Vec<Diagnostic>,
453) {
454 for (value, val_span, _node_id) in &ctx.imports {
455 for stanza_name in value.split(',') {
457 let stanza_name = stanza_name.trim();
458 if !stanza_name.is_empty() && !ctx.common_stanza_names.contains(stanza_name) {
459 diagnostics.push(Diagnostic::error(
460 *val_span,
461 format!("import references undefined common stanza: `{stanza_name}`"),
462 ));
463 }
464 }
465 }
466}
467
468fn check_build_type(cst: &CabalCst, ctx: &ValidationContext, diagnostics: &mut Vec<Diagnostic>) {
470 let Some(bt_field) = ctx
471 .top_level_fields
472 .iter()
473 .find(|f| f.canonical_name == "build-type")
474 else {
475 return;
476 };
477
478 let Some(value) = get_field_value(cst, bt_field.node_id) else {
479 return;
480 };
481
482 const VALID_BUILD_TYPES: &[&str] = &["Simple", "Configure", "Make", "Custom"];
483 if !VALID_BUILD_TYPES.contains(&value) {
484 let val_span = cst
485 .node(bt_field.node_id)
486 .field_value
487 .unwrap_or(bt_field.name_span);
488 diagnostics.push(Diagnostic::error(
489 val_span,
490 format!(
491 "invalid `build-type` value: `{value}` \
492 (expected one of: Simple, Configure, Make, Custom)"
493 ),
494 ));
495 }
496}
497
498fn check_library_exposed_modules(
501 cst: &CabalCst,
502 ctx: &ValidationContext,
503 diagnostics: &mut Vec<Diagnostic>,
504) {
505 for section in &ctx.sections {
506 if section.keyword != "library" {
507 continue;
508 }
509
510 let fields = ctx.section_fields.get(§ion.node_id.0);
511
512 let has_exposed_modules = fields
513 .map(|fs| {
514 fs.iter().any(|f| {
515 if f.canonical_name != "exposed-modules" {
516 return false;
517 }
518 let node = cst.node(f.node_id);
521 let has_inline_value = node
522 .field_value
523 .map(|s| !s.slice(&cst.source).trim().is_empty())
524 .unwrap_or(false);
525 let has_continuation = !node.children.is_empty();
526 has_inline_value || has_continuation
527 })
528 })
529 .unwrap_or(false);
530
531 if !has_exposed_modules {
532 diagnostics.push(Diagnostic::warning(
533 section.keyword_span,
534 "library section has no `exposed-modules` \
535 (or it is empty)",
536 ));
537 }
538 }
539}
540
541#[cfg(test)]
546mod tests {
547 use super::*;
548 use crate::parse::parse;
549
550 fn validate_source(source: &str) -> Vec<Diagnostic> {
552 let result = parse(source);
553 validate(&result.cst)
554 }
555
556 fn has_diagnostic(diagnostics: &[Diagnostic], substring: &str) -> bool {
559 diagnostics.iter().any(|d| d.message.contains(substring))
560 }
561
562 #[test]
565 fn missing_name_field() {
566 let diags = validate_source("cabal-version: 3.0\nversion: 0.1.0.0\n");
567 assert!(has_diagnostic(&diags, "missing required field: `name`"));
568 }
569
570 #[test]
571 fn missing_version_field() {
572 let diags = validate_source("cabal-version: 3.0\nname: foo\n");
573 assert!(has_diagnostic(&diags, "missing required field: `version`"));
574 }
575
576 #[test]
577 fn missing_cabal_version_field() {
578 let diags = validate_source("name: foo\nversion: 0.1.0.0\n");
579 assert!(has_diagnostic(
580 &diags,
581 "missing required field: `cabal-version`"
582 ));
583 }
584
585 #[test]
586 fn all_required_fields_present() {
587 let diags = validate_source("cabal-version: 3.0\nname: foo\nversion: 0.1.0.0\n");
588 assert!(
589 !has_diagnostic(&diags, "missing required field"),
590 "unexpected: {diags:?}"
591 );
592 }
593
594 #[test]
597 fn cabal_version_not_first() {
598 let diags = validate_source("name: foo\ncabal-version: 3.0\nversion: 0.1.0.0\n");
599 assert!(has_diagnostic(
600 &diags,
601 "`cabal-version` should be the first field"
602 ));
603 }
604
605 #[test]
606 fn cabal_version_is_first() {
607 let diags = validate_source("cabal-version: 3.0\nname: foo\nversion: 0.1.0.0\n");
608 assert!(
609 !has_diagnostic(&diags, "should be the first field"),
610 "unexpected: {diags:?}"
611 );
612 }
613
614 #[test]
615 fn cabal_version_first_after_comments() {
616 let diags =
618 validate_source("-- A top comment\ncabal-version: 3.0\nname: foo\nversion: 0.1.0.0\n");
619 assert!(
620 !has_diagnostic(&diags, "should be the first field"),
621 "unexpected: {diags:?}"
622 );
623 }
624
625 #[test]
628 fn duplicate_top_level_field() {
629 let diags = validate_source("cabal-version: 3.0\nname: foo\nname: bar\nversion: 0.1.0.0\n");
630 assert!(has_diagnostic(&diags, "duplicate field: `name`"));
631 }
632
633 #[test]
634 fn duplicate_field_in_section() {
635 let src = "\
636cabal-version: 3.0
637name: foo
638version: 0.1.0.0
639
640library
641 exposed-modules: Foo
642 default-language: Haskell2010
643 default-language: GHC2021
644";
645 let diags = validate_source(src);
646 assert!(has_diagnostic(
647 &diags,
648 "duplicate field: `default-language`"
649 ));
650 }
651
652 #[test]
653 fn repeatable_field_not_flagged_as_duplicate() {
654 let src = "\
655cabal-version: 3.0
656name: foo
657version: 0.1.0.0
658
659library
660 exposed-modules: Foo
661 build-depends: base
662 build-depends: text
663";
664 let diags = validate_source(src);
665 assert!(
666 !has_diagnostic(&diags, "duplicate field"),
667 "build-depends should be allowed multiple times: {diags:?}"
668 );
669 }
670
671 #[test]
672 fn same_field_different_sections_is_ok() {
673 let src = "\
674cabal-version: 3.0
675name: foo
676version: 0.1.0.0
677
678library
679 exposed-modules: Foo
680 build-depends: base
681
682executable bar
683 main-is: Main.hs
684 build-depends: base
685";
686 let diags = validate_source(src);
687 assert!(
688 !has_diagnostic(&diags, "duplicate field"),
689 "unexpected: {diags:?}"
690 );
691 }
692
693 #[test]
696 fn duplicate_executable_sections() {
697 let src = "\
698cabal-version: 3.0
699name: foo
700version: 0.1.0.0
701
702executable bar
703 main-is: Main.hs
704
705executable bar
706 main-is: Other.hs
707";
708 let diags = validate_source(src);
709 assert!(has_diagnostic(
710 &diags,
711 "duplicate section: `executable bar`"
712 ));
713 }
714
715 #[test]
716 fn duplicate_unnamed_library() {
717 let src = "\
718cabal-version: 3.0
719name: foo
720version: 0.1.0.0
721
722library
723 exposed-modules: Foo
724
725library
726 exposed-modules: Bar
727";
728 let diags = validate_source(src);
729 assert!(has_diagnostic(&diags, "duplicate section: `library`"));
730 }
731
732 #[test]
733 fn different_executable_names_is_ok() {
734 let src = "\
735cabal-version: 3.0
736name: foo
737version: 0.1.0.0
738
739executable bar
740 main-is: Main.hs
741
742executable baz
743 main-is: Other.hs
744";
745 let diags = validate_source(src);
746 assert!(
747 !has_diagnostic(&diags, "duplicate section"),
748 "unexpected: {diags:?}"
749 );
750 }
751
752 #[test]
755 fn import_missing_common_stanza() {
756 let src = "\
757cabal-version: 3.0
758name: foo
759version: 0.1.0.0
760
761library
762 import: warnings
763 exposed-modules: Foo
764";
765 let diags = validate_source(src);
766 assert!(has_diagnostic(
767 &diags,
768 "import references undefined common stanza: `warnings`"
769 ));
770 }
771
772 #[test]
773 fn import_with_existing_common_stanza() {
774 let src = "\
775cabal-version: 3.0
776name: foo
777version: 0.1.0.0
778
779common warnings
780 ghc-options: -Wall
781
782library
783 import: warnings
784 exposed-modules: Foo
785";
786 let diags = validate_source(src);
787 assert!(
788 !has_diagnostic(&diags, "import references undefined"),
789 "unexpected: {diags:?}"
790 );
791 }
792
793 #[test]
796 fn valid_build_type_simple() {
797 let diags = validate_source(
798 "cabal-version: 3.0\nname: foo\nversion: 0.1.0.0\nbuild-type: Simple\n",
799 );
800 assert!(
801 !has_diagnostic(&diags, "invalid `build-type`"),
802 "unexpected: {diags:?}"
803 );
804 }
805
806 #[test]
807 fn invalid_build_type() {
808 let diags =
809 validate_source("cabal-version: 3.0\nname: foo\nversion: 0.1.0.0\nbuild-type: Foo\n");
810 assert!(has_diagnostic(&diags, "invalid `build-type` value: `Foo`"));
811 }
812
813 #[test]
816 fn library_without_exposed_modules() {
817 let src = "\
818cabal-version: 3.0
819name: foo
820version: 0.1.0.0
821
822library
823 build-depends: base
824";
825 let diags = validate_source(src);
826 assert!(has_diagnostic(
827 &diags,
828 "library section has no `exposed-modules`"
829 ));
830 }
831
832 #[test]
833 fn library_with_exposed_modules() {
834 let src = "\
835cabal-version: 3.0
836name: foo
837version: 0.1.0.0
838
839library
840 exposed-modules: Foo
841 build-depends: base
842";
843 let diags = validate_source(src);
844 assert!(
845 !has_diagnostic(&diags, "exposed-modules"),
846 "unexpected: {diags:?}"
847 );
848 }
849
850 #[test]
851 fn library_with_multiline_exposed_modules() {
852 let src = "\
853cabal-version: 3.0
854name: foo
855version: 0.1.0.0
856
857library
858 exposed-modules:
859 Foo
860 Bar
861 build-depends: base
862";
863 let diags = validate_source(src);
864 assert!(
865 !has_diagnostic(&diags, "exposed-modules"),
866 "unexpected: {diags:?}"
867 );
868 }
869
870 #[test]
873 fn cabal_version_valid_value() {
874 let diags = validate_source("cabal-version: 3.0\nname: foo\nversion: 0.1.0.0\n");
875 assert!(
876 !has_diagnostic(&diags, "unrecognized `cabal-version`"),
877 "unexpected: {diags:?}"
878 );
879 }
880
881 #[test]
882 fn cabal_version_deprecated_prefix() {
883 let diags = validate_source("cabal-version: >=1.10\nname: foo\nversion: 0.1.0.0\n");
885 assert!(
886 !has_diagnostic(&diags, "unrecognized `cabal-version`"),
887 "unexpected: {diags:?}"
888 );
889 }
890
891 #[test]
892 fn cabal_version_invalid_value() {
893 let diags = validate_source("cabal-version: foobar\nname: foo\nversion: 0.1.0.0\n");
894 assert!(has_diagnostic(&diags, "unrecognized `cabal-version` value"));
895 }
896
897 #[test]
900 fn full_valid_file_no_diagnostics() {
901 let src = "\
902cabal-version: 3.0
903name: foo
904version: 0.1.0.0
905synopsis: A test package
906build-type: Simple
907
908common warnings
909 ghc-options: -Wall
910
911library
912 import: warnings
913 exposed-modules: Foo
914 build-depends: base >=4.14
915
916executable my-exe
917 import: warnings
918 main-is: Main.hs
919 build-depends: base, foo
920
921test-suite tests
922 import: warnings
923 type: exitcode-stdio-1.0
924 main-is: Main.hs
925 build-depends: base, foo, tasty
926";
927 let diags = validate_source(src);
928 assert!(diags.is_empty(), "expected no diagnostics, got: {diags:?}");
929 }
930
931 #[test]
934 fn duplicate_field_case_insensitive() {
935 let diags = validate_source("cabal-version: 3.0\nName: foo\nname: bar\nversion: 0.1.0.0\n");
936 assert!(has_diagnostic(&diags, "duplicate field: `name`"));
937 }
938
939 #[test]
940 fn duplicate_field_underscore_hyphen() {
941 let src = "\
943cabal-version: 3.0
944name: foo
945version: 0.1.0.0
946
947library
948 exposed-modules: Foo
949 default-language: Haskell2010
950 default_language: GHC2021
951";
952 let diags = validate_source(src);
953 assert!(has_diagnostic(
954 &diags,
955 "duplicate field: `default-language`"
956 ));
957 }
958
959 #[test]
962 fn empty_file() {
963 let diags = validate_source("");
964 assert!(has_diagnostic(
966 &diags,
967 "missing required field: `cabal-version`"
968 ));
969 assert!(has_diagnostic(&diags, "missing required field: `name`"));
970 assert!(has_diagnostic(&diags, "missing required field: `version`"));
971 }
972
973 #[test]
974 fn comments_only_file() {
975 let diags = validate_source("-- just a comment\n");
976 assert!(has_diagnostic(
977 &diags,
978 "missing required field: `cabal-version`"
979 ));
980 assert!(has_diagnostic(&diags, "missing required field: `name`"));
981 assert!(has_diagnostic(&diags, "missing required field: `version`"));
982 }
983}