Skip to main content

lintspec_core/definitions/
contract.rs

1//! Parsing and validation of contract 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/// A contract definition
11#[derive(Debug, Clone, bon::Builder)]
12#[non_exhaustive]
13#[builder(on(String, into))]
14pub struct ContractDefinition {
15    /// The name of the contract
16    pub name: Symbol,
17
18    /// The span of the contract definition
19    pub span: TextRange,
20
21    /// The [`NatSpec`] associated with the contract definition, if any
22    pub natspec: Option<NatSpec>,
23}
24
25impl SourceItem for ContractDefinition {
26    fn item_type(&self) -> ItemType {
27        ItemType::Contract
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 ContractDefinition {
44    fn validate(&self, options: &ValidationOptions) -> ItemDiagnostics {
45        let opts = &options.contracts;
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            .contracts(
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) -> ContractDefinition {
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_contract)
113            .unwrap()
114    }
115
116    #[test]
117    fn test_contract() {
118        let contents = "/// @title Contract
119        /// @author Me
120        /// @notice This is a contract
121        contract Test {}";
122        let res = parse_file(contents).validate(&OPTIONS);
123        assert!(res.diags.is_empty(), "{:#?}", res.diags);
124    }
125
126    #[test]
127    fn test_contract_no_natspec() {
128        let contents = "contract 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_contract_multiline() {
138        let contents = "/**
139         * @title Contract
140         * @author Me
141         * @notice This is a contract
142         */
143        contract Test {}";
144        let res = parse_file(contents).validate(&OPTIONS);
145        assert!(res.diags.is_empty(), "{:#?}", res.diags);
146    }
147
148    #[test]
149    fn test_contract_inheritdoc() {
150        // inheritdoc should be ignored as it doesn't apply to contracts
151        let contents = "/// @inheritdoc ITest
152        contract Test is Foo {}";
153        let res = parse_file(contents).validate(
154            &ValidationOptions::builder()
155                .contracts(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}