1use std::{
6 fs::File,
7 io,
8 path::{Path, PathBuf},
9};
10
11use serde::Serialize;
12
13use crate::{
14 config::{Config, ContractRules, FunctionConfig, Req, VariableConfig, WithParamsRules},
15 definitions::{Identifier, ItemType, Parent},
16 error::{Error, Result},
17 natspec::{NatSpec, NatSpecKind},
18 parser::{DocumentId, Parse, ParsedDocument},
19 textindex::TextRange,
20};
21
22#[derive(Debug, Clone, Serialize, thiserror::Error)]
24#[error("Error")]
25pub struct FileDiagnostics {
26 pub path: PathBuf,
28
29 #[serde(skip_serializing)]
33 pub document_id: DocumentId,
34
35 pub items: Vec<ItemDiagnostics>,
37}
38
39#[derive(Debug, Clone, Serialize, bon::Builder)]
41#[non_exhaustive]
42#[builder(on(String, into))]
43pub struct ItemDiagnostics {
44 pub parent: Option<Parent>,
46
47 pub item_type: ItemType,
49
50 pub name: String,
52
53 pub span: TextRange,
55
56 pub diags: Vec<Diagnostic>,
58}
59
60impl ItemDiagnostics {
61 pub fn print_compact(
67 &self,
68 f: &mut impl io::Write,
69 path: impl AsRef<Path>,
70 root_dir: impl AsRef<Path>,
71 ) -> std::result::Result<(), io::Error> {
72 fn inner(
73 this: &ItemDiagnostics,
74 f: &mut impl io::Write,
75 path: &Path,
76 root_dir: &Path,
77 ) -> std::result::Result<(), io::Error> {
78 let source_name = match path.strip_prefix(root_dir) {
79 Ok(relative_path) => relative_path.to_string_lossy(),
80 Err(_) => path.to_string_lossy(),
81 };
82 writeln!(f, "{source_name}:{}", this.span.start)?;
83 if let Some(parent) = &this.parent {
84 writeln!(f, "{} {}.{}", this.item_type, parent, this.name)?;
85 } else {
86 writeln!(f, "{} {}", this.item_type, this.name)?;
87 }
88 for diag in &this.diags {
89 writeln!(f, " {}", diag.message)?;
90 }
91 writeln!(f)?;
92 Ok(())
93 }
94 inner(self, f, path.as_ref(), root_dir.as_ref())
95 }
96}
97
98#[derive(Debug, Clone, Serialize)]
100pub struct Diagnostic {
101 pub span: TextRange,
106 pub message: String,
107}
108
109pub fn lint(
116 mut parser: impl Parse,
117 path: impl AsRef<Path>,
118 options: &ValidationOptions,
119 keep_contents: bool,
120) -> Result<Option<FileDiagnostics>> {
121 fn inner(
122 path: &Path,
123 document: ParsedDocument,
124 options: &ValidationOptions,
125 ) -> Option<FileDiagnostics> {
126 let mut items: Vec<_> = document
127 .definitions
128 .into_iter()
129 .filter_map(|item| {
130 let mut item_diags = item.validate(options);
131 if item_diags.diags.is_empty() {
132 None
133 } else {
134 item_diags.diags.sort_unstable_by_key(|d| d.span.start);
135 Some(item_diags)
136 }
137 })
138 .collect();
139 if items.is_empty() {
140 return None;
141 }
142 items.sort_unstable_by_key(|i| i.span.start);
143 Some(FileDiagnostics {
144 path: path.to_path_buf(),
145 document_id: document.id,
146 items,
147 })
148 }
149 let file = File::open(&path).map_err(|err| Error::IOError {
150 path: path.as_ref().to_path_buf(),
151 err,
152 })?;
153 let document = parser.parse_document(file, Some(&path), keep_contents)?;
154 Ok(inner(path.as_ref(), document, options))
155}
156
157#[derive(Debug, Clone, PartialEq, Eq, bon::Builder)]
159#[non_exhaustive]
160pub struct ValidationOptions {
161 #[builder(default = true)]
163 pub inheritdoc: bool,
164
165 #[builder(default = false)]
167 pub inheritdoc_override: bool,
168
169 #[builder(default)]
171 pub notice_or_dev: bool,
172
173 #[builder(default)]
175 pub contracts: ContractRules,
176
177 #[builder(default)]
179 pub interfaces: ContractRules,
180
181 #[builder(default)]
183 pub libraries: ContractRules,
184
185 #[builder(default = WithParamsRules::default_constructor())]
187 pub constructors: WithParamsRules,
188
189 #[builder(default)]
191 pub enums: WithParamsRules,
192
193 #[builder(default = WithParamsRules::required())]
195 pub errors: WithParamsRules,
196
197 #[builder(default = WithParamsRules::required())]
199 pub events: WithParamsRules,
200
201 #[builder(default)]
203 pub functions: FunctionConfig,
204
205 #[builder(default = WithParamsRules::required())]
207 pub modifiers: WithParamsRules,
208
209 #[builder(default)]
211 pub structs: WithParamsRules,
212
213 #[builder(default)]
215 pub variables: VariableConfig,
216}
217
218impl Default for ValidationOptions {
219 fn default() -> Self {
224 Self {
225 inheritdoc: true,
226 inheritdoc_override: false,
227 notice_or_dev: false,
228 contracts: ContractRules::default(),
229 interfaces: ContractRules::default(),
230 libraries: ContractRules::default(),
231 constructors: WithParamsRules::default_constructor(),
232 enums: WithParamsRules::default(),
233 errors: WithParamsRules::required(),
234 events: WithParamsRules::required(),
235 functions: FunctionConfig::default(),
236 modifiers: WithParamsRules::required(),
237 structs: WithParamsRules::default(),
238 variables: VariableConfig::default(),
239 }
240 }
241}
242
243impl From<Config> for ValidationOptions {
245 fn from(value: Config) -> Self {
246 Self {
247 inheritdoc: value.lintspec.inheritdoc,
248 inheritdoc_override: value.lintspec.inheritdoc_override,
249 notice_or_dev: value.lintspec.notice_or_dev,
250 contracts: value.contracts,
251 interfaces: value.interfaces,
252 libraries: value.libraries,
253 constructors: value.constructors,
254 enums: value.enums,
255 errors: value.errors,
256 events: value.events,
257 functions: value.functions,
258 modifiers: value.modifiers,
259 structs: value.structs,
260 variables: value.variables,
261 }
262 }
263}
264
265impl From<&Config> for ValidationOptions {
267 fn from(value: &Config) -> Self {
268 Self {
269 inheritdoc: value.lintspec.inheritdoc,
270 inheritdoc_override: value.lintspec.inheritdoc_override,
271 notice_or_dev: value.lintspec.notice_or_dev,
272 contracts: value.contracts.clone(),
273 interfaces: value.interfaces.clone(),
274 libraries: value.libraries.clone(),
275 constructors: value.constructors.clone(),
276 enums: value.enums.clone(),
277 errors: value.errors.clone(),
278 events: value.events.clone(),
279 functions: value.functions.clone(),
280 modifiers: value.modifiers.clone(),
281 structs: value.structs.clone(),
282 variables: value.variables.clone(),
283 }
284 }
285}
286
287pub trait Validate {
289 fn validate(&self, options: &ValidationOptions) -> ItemDiagnostics;
291}
292
293#[derive(Debug, Clone, bon::Builder)]
295pub struct CheckParams<'a> {
296 natspec: &'a Option<NatSpec>,
298 rule: Req,
300 params: &'a [Identifier],
302 default_span: TextRange,
304}
305
306impl CheckParams<'_> {
307 #[must_use]
313 pub fn check(&self) -> Vec<Diagnostic> {
314 let mut res = match self.rule {
315 Req::Ignored => return Vec::new(),
316 Req::Required => self.check_required(),
317 Req::Forbidden => self.check_forbidden(),
318 };
319 res.sort_unstable_by_key(|d| d.span.start.utf8);
320 res
321 }
322
323 fn check_required(&self) -> Vec<Diagnostic> {
325 let Some(natspec) = self.natspec else {
326 return self.missing_diags().collect();
327 };
328 self.extra_diags()
329 .chain(self.count_diags(natspec))
330 .collect()
331 }
332
333 fn check_forbidden(&self) -> Vec<Diagnostic> {
335 if let Some(natspec) = self.natspec
336 && natspec.has_param()
337 {
338 vec![Diagnostic {
339 span: self.default_span.clone(),
340 message: "@param is forbidden".to_string(),
341 }]
342 } else {
343 Vec::new()
344 }
345 }
346
347 fn missing_diags(&self) -> impl Iterator<Item = Diagnostic> {
349 self.params.iter().filter_map(|p| {
350 p.name.as_ref().map(|name| Diagnostic {
351 span: p.span.clone(),
352 message: format!("@param {name} is missing"),
353 })
354 })
355 }
356
357 fn extra_diags(&self) -> impl Iterator<Item = Diagnostic> {
359 self.natspec
361 .as_ref()
362 .map(|n| {
363 n.items.iter().filter_map(|item| {
364 let NatSpecKind::Param { name } = &item.kind else {
365 return None;
366 };
367 if self
368 .params
369 .iter()
370 .any(|p| matches!(p.name.as_ref(), Some(param_name) if param_name == name))
371 {
372 None
373 } else {
374 let span_start = self.default_span.start + item.span.start;
376 let span_end = self.default_span.start + item.span.end;
377 Some(Diagnostic {
378 span: span_start..span_end,
379 message: format!("extra @param {name}"),
380 })
381 }
382 })
383 })
384 .into_iter()
385 .flatten()
386 }
387
388 fn count_diags(&self, natspec: &NatSpec) -> impl Iterator<Item = Diagnostic> {
390 self.counts(natspec).filter_map(|(param, count)| {
391 let name = param.name.as_ref().map_or("unnamed_param", String::as_str);
392 match count {
393 0 => Some(Diagnostic {
394 span: param.span.clone(),
395 message: format!("@param {name} is missing"),
396 }),
397 1 => None,
398 2.. => Some(Diagnostic {
399 span: param.span.clone(),
400 message: format!("@param {name} is present more than once"),
401 }),
402 }
403 })
404 }
405
406 fn counts(&self, natspec: &NatSpec) -> impl Iterator<Item = (&Identifier, usize)> {
408 self.params.iter().map(|p: &Identifier| {
410 let param_name = p.name.as_ref().map_or("", String::as_str);
411 (
412 p,
413 natspec
414 .items
415 .iter()
416 .filter(
417 |n| matches!(&n.kind, NatSpecKind::Param { name } if name == param_name),
418 )
419 .count(),
420 )
421 })
422 }
423}
424
425#[derive(Debug, Clone, bon::Builder)]
427pub struct CheckReturns<'a> {
428 natspec: &'a Option<NatSpec>,
430 rule: Req,
432 returns: &'a [Identifier],
434 default_span: TextRange,
436 is_var: bool,
438}
439
440impl CheckReturns<'_> {
441 #[must_use]
447 pub fn check(&self) -> Vec<Diagnostic> {
448 match self.rule {
449 Req::Ignored => Vec::new(),
450 Req::Required => self.check_required(),
451 Req::Forbidden => self.check_forbidden(),
452 }
453 }
454
455 fn check_required(&self) -> Vec<Diagnostic> {
457 let Some(natspec) = self.natspec else {
458 return self.missing_diags().collect();
459 };
460 self.return_diags(natspec)
461 .chain(self.extra_unnamed_diags(natspec))
462 .collect()
463 }
464
465 fn check_forbidden(&self) -> Vec<Diagnostic> {
467 if let Some(natspec) = self.natspec
468 && natspec.has_return()
469 {
470 vec![Diagnostic {
471 span: self.default_span.clone(),
472 message: "@return is forbidden".to_string(),
473 }]
474 } else {
475 Vec::new()
476 }
477 }
478
479 fn missing_diags(&self) -> impl Iterator<Item = Diagnostic> {
481 self.returns.iter().enumerate().map(|(idx, r)| {
482 let message = if let Some(name) = &r.name {
483 format!("@return {name} is missing")
484 } else if self.is_var {
485 "@return is missing".to_string()
486 } else {
487 format!("@return missing for unnamed return #{}", idx + 1)
488 };
489 Diagnostic {
490 span: r.span.clone(),
491 message,
492 }
493 })
494 }
495
496 fn named_count_diag(natspec: &NatSpec, ret: &Identifier, name: &str) -> Option<Diagnostic> {
498 match natspec.count_return(ret) {
499 0 => Some(Diagnostic {
500 span: ret.span.clone(),
501 message: format!("@return {name} is missing"),
502 }),
503 1 => None,
504 2.. => Some(Diagnostic {
505 span: ret.span.clone(),
506 message: format!("@return {name} is present more than once"),
507 }),
508 }
509 }
510
511 fn unnamed_diag(
513 &self,
514 returns_count: usize,
515 idx: usize,
516 ret: &Identifier,
517 ) -> Option<Diagnostic> {
518 if idx + 1 > returns_count {
519 let message = if self.is_var {
520 "@return is missing".to_string()
521 } else {
522 format!("@return missing for unnamed return #{}", idx + 1)
523 };
524 Some(Diagnostic {
525 span: ret.span.clone(),
526 message,
527 })
528 } else {
529 None
530 }
531 }
532
533 fn return_diags(&self, natspec: &NatSpec) -> impl Iterator<Item = Diagnostic> {
535 let returns_count = natspec.count_all_returns();
536 self.returns
537 .iter()
538 .enumerate()
539 .filter_map(move |(idx, ret)| {
540 if let Some(name) = &ret.name {
541 Self::named_count_diag(natspec, ret, name)
543 } else {
544 self.unnamed_diag(returns_count, idx, ret)
546 }
547 })
548 }
549
550 fn extra_unnamed_diags(&self, natspec: &NatSpec) -> impl Iterator<Item = Diagnostic> {
552 let unnamed_returns = self.returns.iter().filter(|r| r.name.is_none()).count();
553 if natspec.count_unnamed_returns() > unnamed_returns {
554 Some(Diagnostic {
555 span: self
556 .returns
557 .last()
558 .cloned()
559 .map_or(self.default_span.clone(), |r| r.span),
560 message: "too many unnamed returns".to_string(),
561 })
562 } else {
563 None
564 }
565 .into_iter()
566 }
567}
568
569#[derive(Debug, Clone, bon::Builder)]
571pub struct CheckNotice<'a> {
572 natspec: &'a Option<NatSpec>,
574 rule: Req,
576 span: &'a TextRange,
578}
579
580impl CheckNotice<'_> {
581 #[must_use]
584 pub fn check(&self) -> Option<Diagnostic> {
585 match self.rule {
586 Req::Ignored => None,
587 Req::Required => self.check_required(),
588 Req::Forbidden => self.check_forbidden(),
589 }
590 }
591
592 fn check_required(&self) -> Option<Diagnostic> {
594 if let Some(natspec) = self.natspec
595 && natspec.has_notice()
596 {
597 None
598 } else {
599 Some(Diagnostic {
600 span: self.span.clone(),
601 message: "@notice is missing".to_string(),
602 })
603 }
604 }
605
606 fn check_forbidden(&self) -> Option<Diagnostic> {
608 if let Some(natspec) = self.natspec
609 && natspec.has_notice()
610 {
611 Some(Diagnostic {
612 span: self.span.clone(),
613 message: "@notice is forbidden".to_string(),
614 })
615 } else {
616 None
617 }
618 }
619}
620
621#[derive(Debug, Clone, bon::Builder)]
623pub struct CheckDev<'a> {
624 natspec: &'a Option<NatSpec>,
626 rule: Req,
628 span: &'a TextRange,
630}
631
632impl CheckDev<'_> {
633 #[must_use]
636 pub fn check(&self) -> Option<Diagnostic> {
637 match self.rule {
638 Req::Ignored => None,
639 Req::Required => self.check_required(),
640 Req::Forbidden => self.check_forbidden(),
641 }
642 }
643
644 fn check_required(&self) -> Option<Diagnostic> {
646 if let Some(natspec) = self.natspec
647 && natspec.has_dev()
648 {
649 None
650 } else {
651 Some(Diagnostic {
652 span: self.span.clone(),
653 message: "@dev is missing".to_string(),
654 })
655 }
656 }
657
658 fn check_forbidden(&self) -> Option<Diagnostic> {
660 if let Some(natspec) = self.natspec
661 && natspec.has_dev()
662 {
663 Some(Diagnostic {
664 span: self.span.clone(),
665 message: "@dev is forbidden".to_string(),
666 })
667 } else {
668 None
669 }
670 }
671}
672
673#[derive(Debug, Clone, bon::Builder)]
675pub struct CheckTitle<'a> {
676 natspec: &'a Option<NatSpec>,
678 rule: Req,
680 span: &'a TextRange,
682}
683
684impl CheckTitle<'_> {
685 #[must_use]
688 pub fn check(&self) -> Option<Diagnostic> {
689 match self.rule {
690 Req::Ignored => None,
691 Req::Required => self.check_required(),
692 Req::Forbidden => self.check_forbidden(),
693 }
694 }
695
696 fn check_required(&self) -> Option<Diagnostic> {
698 if let Some(natspec) = self.natspec
699 && natspec.has_title()
700 {
701 None
702 } else {
703 Some(Diagnostic {
704 span: self.span.clone(),
705 message: "@title is missing".to_string(),
706 })
707 }
708 }
709
710 fn check_forbidden(&self) -> Option<Diagnostic> {
712 if let Some(natspec) = self.natspec
713 && natspec.has_title()
714 {
715 Some(Diagnostic {
716 span: self.span.clone(),
717 message: "@title is forbidden".to_string(),
718 })
719 } else {
720 None
721 }
722 }
723}
724
725#[derive(Debug, Clone, bon::Builder)]
727pub struct CheckAuthor<'a> {
728 natspec: &'a Option<NatSpec>,
730 rule: Req,
732 span: &'a TextRange,
734}
735
736impl CheckAuthor<'_> {
737 #[must_use]
740 pub fn check(&self) -> Option<Diagnostic> {
741 match self.rule {
742 Req::Ignored => None,
743 Req::Required => self.check_required(),
744 Req::Forbidden => self.check_forbidden(),
745 }
746 }
747
748 fn check_required(&self) -> Option<Diagnostic> {
750 if let Some(natspec) = self.natspec
751 && natspec.has_author()
752 {
753 None
754 } else {
755 Some(Diagnostic {
756 span: self.span.clone(),
757 message: "@author is missing".to_string(),
758 })
759 }
760 }
761
762 fn check_forbidden(&self) -> Option<Diagnostic> {
764 if let Some(natspec) = self.natspec
765 && natspec.has_author()
766 {
767 Some(Diagnostic {
768 span: self.span.clone(),
769 message: "@author is forbidden".to_string(),
770 })
771 } else {
772 None
773 }
774 }
775}
776
777#[derive(Debug, Clone, bon::Builder)]
779pub struct CheckNoticeAndDev<'a> {
780 natspec: &'a Option<NatSpec>,
782 notice_rule: Req,
784 dev_rule: Req,
786 notice_or_dev: bool,
788 span: &'a TextRange,
790}
791
792impl CheckNoticeAndDev<'_> {
793 #[must_use]
802 pub fn check(&self) -> Vec<Diagnostic> {
803 match (self.notice_or_dev, self.notice_rule, self.dev_rule) {
804 (true, Req::Required, Req::Ignored | Req::Required)
805 | (true, Req::Ignored, Req::Required) => self.check_notice_or_dev(),
806 (true, Req::Forbidden, _) | (true, _, Req::Forbidden) | (false, _, _) => {
807 self.check_separately()
808 }
809 (true, Req::Ignored, Req::Ignored) => Vec::new(),
810 }
811 }
812
813 fn check_notice_or_dev(&self) -> Vec<Diagnostic> {
815 if let Some(natspec) = self.natspec
816 && (natspec.has_notice() || natspec.has_dev())
817 {
818 Vec::new()
819 } else {
820 vec![Diagnostic {
821 span: self.span.clone(),
822 message: "@notice or @dev is missing".to_string(),
823 }]
824 }
825 }
826
827 fn check_separately(&self) -> Vec<Diagnostic> {
829 let mut res = Vec::new();
830 res.extend(
831 CheckNotice::builder()
832 .natspec(self.natspec)
833 .rule(self.notice_rule)
834 .span(self.span)
835 .build()
836 .check(),
837 );
838 res.extend(
839 CheckDev::builder()
840 .natspec(self.natspec)
841 .rule(self.dev_rule)
842 .span(self.span)
843 .build()
844 .check(),
845 );
846 res
847 }
848}
849
850#[cfg(test)]
851mod tests {
852 use similar_asserts::assert_eq;
853
854 use crate::config::{BaseConfig, FunctionRules, NoticeDevRules};
855
856 use super::*;
857
858 #[test]
859 fn test_validation_options_default() {
860 assert_eq!(
861 ValidationOptions::default(),
862 ValidationOptions::builder().build()
863 );
864
865 let default_config = Config::default();
866 let options = ValidationOptions::from(&default_config);
867 assert_eq!(ValidationOptions::default(), options);
868 }
869
870 #[test]
871 fn test_validation_options_conversion() {
872 let config = Config::builder().build();
873 let options = ValidationOptions::from(&config);
874 assert_eq!(config.lintspec.inheritdoc, options.inheritdoc);
875 assert_eq!(config.lintspec.notice_or_dev, options.notice_or_dev);
876 assert_eq!(config.contracts, options.contracts);
877 assert_eq!(config.interfaces, options.interfaces);
878 assert_eq!(config.libraries, options.libraries);
879 assert_eq!(config.constructors, options.constructors);
880 assert_eq!(config.enums, options.enums);
881 assert_eq!(config.errors, options.errors);
882 assert_eq!(config.events, options.events);
883 assert_eq!(config.functions, options.functions);
884 assert_eq!(config.modifiers, options.modifiers);
885 assert_eq!(config.structs, options.structs);
886 assert_eq!(config.variables, options.variables);
887
888 let config = Config::builder()
889 .lintspec(
890 BaseConfig::builder()
891 .inheritdoc(false)
892 .notice_or_dev(true)
893 .build(),
894 )
895 .contracts(
896 ContractRules::builder()
897 .title(Req::Required)
898 .author(Req::Required)
899 .dev(Req::Required)
900 .notice(Req::Forbidden)
901 .build(),
902 )
903 .interfaces(
904 ContractRules::builder()
905 .title(Req::Ignored)
906 .author(Req::Forbidden)
907 .dev(Req::Forbidden)
908 .notice(Req::Required)
909 .build(),
910 )
911 .libraries(
912 ContractRules::builder()
913 .title(Req::Forbidden)
914 .author(Req::Ignored)
915 .dev(Req::Required)
916 .notice(Req::Ignored)
917 .build(),
918 )
919 .constructors(WithParamsRules::builder().dev(Req::Required).build())
920 .enums(WithParamsRules::builder().param(Req::Required).build())
921 .errors(WithParamsRules::builder().notice(Req::Forbidden).build())
922 .events(WithParamsRules::builder().param(Req::Forbidden).build())
923 .functions(
924 FunctionConfig::builder()
925 .private(FunctionRules::builder().dev(Req::Required).build())
926 .build(),
927 )
928 .modifiers(WithParamsRules::builder().dev(Req::Forbidden).build())
929 .structs(WithParamsRules::builder().notice(Req::Ignored).build())
930 .variables(
931 VariableConfig::builder()
932 .private(NoticeDevRules::builder().dev(Req::Required).build())
933 .build(),
934 )
935 .build();
936 let options = ValidationOptions::from(&config);
937 assert_eq!(config.lintspec.inheritdoc, options.inheritdoc);
938 assert_eq!(config.lintspec.notice_or_dev, options.notice_or_dev);
939 assert_eq!(config.contracts, options.contracts);
940 assert_eq!(config.interfaces, options.interfaces);
941 assert_eq!(config.libraries, options.libraries);
942 assert_eq!(config.constructors, options.constructors);
943 assert_eq!(config.enums, options.enums);
944 assert_eq!(config.errors, options.errors);
945 assert_eq!(config.events, options.events);
946 assert_eq!(config.functions, options.functions);
947 assert_eq!(config.modifiers, options.modifiers);
948 assert_eq!(config.structs, options.structs);
949 assert_eq!(config.variables, options.variables);
950 }
951}