Skip to main content

lintspec_core/definitions/
interface.rs

1//! Parsing and validation of interface definitions.
2use crate::{
3    lint::{CheckAuthor, CheckNoticeAndDev, CheckTitle, ItemDiagnostics},
4    natspec::NatSpec,
5};
6
7use super::{ItemType, Parent, SourceItem, TextRange, Validate, ValidationOptions};
8
9/// An interface definition
10#[derive(Debug, Clone, bon::Builder)]
11#[non_exhaustive]
12#[builder(on(String, into))]
13pub struct InterfaceDefinition {
14    /// The name of the interface
15    pub name: String,
16
17    /// The span of the interface definition
18    pub span: TextRange,
19
20    /// The [`NatSpec`] associated with the interface definition, if any
21    pub natspec: Option<NatSpec>,
22}
23
24impl SourceItem for InterfaceDefinition {
25    fn item_type(&self) -> ItemType {
26        ItemType::Interface
27    }
28
29    fn parent(&self) -> Option<Parent> {
30        None
31    }
32
33    fn name(&self) -> String {
34        self.name.clone()
35    }
36
37    fn span(&self) -> TextRange {
38        self.span.clone()
39    }
40}
41
42impl Validate for InterfaceDefinition {
43    fn validate(&self, options: &ValidationOptions) -> ItemDiagnostics {
44        let opts = &options.interfaces;
45        let mut out = ItemDiagnostics {
46            parent: self.parent(),
47            item_type: self.item_type(),
48            name: self.name(),
49            span: self.span(),
50            diags: vec![],
51        };
52        out.diags.extend(
53            CheckTitle::builder()
54                .natspec(&self.natspec)
55                .rule(opts.title)
56                .span(&self.span)
57                .build()
58                .check(),
59        );
60        out.diags.extend(
61            CheckAuthor::builder()
62                .natspec(&self.natspec)
63                .rule(opts.author)
64                .span(&self.span)
65                .build()
66                .check(),
67        );
68        out.diags.extend(
69            CheckNoticeAndDev::builder()
70                .natspec(&self.natspec)
71                .notice_rule(opts.notice)
72                .dev_rule(opts.dev)
73                .notice_or_dev(options.notice_or_dev)
74                .span(&self.span)
75                .build()
76                .check(),
77        );
78        out
79    }
80}
81
82#[cfg(test)]
83#[cfg(feature = "solar")]
84mod tests {
85    use std::sync::LazyLock;
86
87    use similar_asserts::assert_eq;
88
89    use crate::{
90        config::{ContractRules, Req},
91        definitions::Definition,
92        parser::{Parse as _, solar::SolarParser},
93    };
94
95    use super::*;
96
97    static OPTIONS: LazyLock<ValidationOptions> = LazyLock::new(|| {
98        ValidationOptions::builder()
99            .inheritdoc(false)
100            .interfaces(
101                ContractRules::builder()
102                    .title(Req::Required)
103                    .author(Req::Required)
104                    .notice(Req::Required)
105                    .build(),
106            )
107            .build()
108    });
109
110    fn parse_file(contents: &str) -> InterfaceDefinition {
111        let mut parser = SolarParser::default();
112        let doc = parser
113            .parse_document(contents.as_bytes(), None::<std::path::PathBuf>, false)
114            .unwrap();
115        doc.definitions
116            .into_iter()
117            .find_map(Definition::to_interface)
118            .unwrap()
119    }
120
121    #[test]
122    fn test_interface() {
123        let contents = "/// @title Interface
124        /// @author Me
125        /// @notice This is an interface
126        interface Test {}";
127        let res = parse_file(contents).validate(&OPTIONS);
128        assert!(res.diags.is_empty(), "{:#?}", res.diags);
129    }
130
131    #[test]
132    fn test_interface_no_natspec() {
133        let contents = "interface Test {}";
134        let res = parse_file(contents).validate(&OPTIONS);
135        assert_eq!(res.diags.len(), 3);
136        assert_eq!(res.diags[0].message, "@title is missing");
137        assert_eq!(res.diags[1].message, "@author is missing");
138        assert_eq!(res.diags[2].message, "@notice is missing");
139    }
140
141    #[test]
142    fn test_interface_multiline() {
143        let contents = "/**
144         * @title Interface
145         * @author Me
146         * @notice This is an interface
147         */
148        interface Test {}";
149        let res = parse_file(contents).validate(&OPTIONS);
150        assert!(res.diags.is_empty(), "{:#?}", res.diags);
151    }
152
153    #[test]
154    fn test_interface_inheritdoc() {
155        // inheritdoc should be ignored as it doesn't apply to interfaces
156        let contents = "/// @inheritdoc ITest
157        interface Test is Foo {}";
158        let res = parse_file(contents).validate(
159            &ValidationOptions::builder()
160                .interfaces(ContractRules::builder().title(Req::Required).build())
161                .build(),
162        );
163        assert_eq!(res.diags.len(), 1);
164        assert_eq!(res.diags[0].message, "@title is missing");
165    }
166}