Skip to main content

lintspec_core/definitions/
interface.rs

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