bulloak_syntax/
semantics.rs

1//! Implementation of the semantic analysis of a bulloak tree.
2use std::{collections::HashMap, fmt, result};
3
4use thiserror::Error;
5
6use super::ast::{self, Ast};
7use crate::{
8    error::FrontendError,
9    span::Span,
10    utils::{lower_first_letter, sanitize, to_pascal_case},
11    visitor::Visitor,
12};
13
14type Result<T> = result::Result<T, Errors>;
15
16/// A collection of errors that occurred during semantic analysis.
17#[derive(Error, Clone, Debug, PartialEq, Eq)]
18#[error("{}", .0.iter().map(|e| e.to_string()).collect::<Vec<_>>().join(""))]
19pub struct Errors(pub Vec<Error>);
20
21/// An error that occurred while doing semantic analysis on the abstract
22/// syntax tree.
23#[derive(Error, Clone, Debug, Eq, PartialEq)]
24pub struct Error {
25    /// The kind of error.
26    #[source]
27    kind: ErrorKind,
28    /// The original text that the visitor generated the error from. Every
29    /// span in an error is a valid range into this string.
30    text: String,
31    /// The span of this error.
32    span: Span,
33}
34
35impl Error {
36    /// Instantiates a new `Error`.
37    #[cfg(test)]
38    pub fn new(kind: ErrorKind, text: String, span: Span) -> Self {
39        Error { kind, text, span }
40    }
41}
42
43impl FrontendError<ErrorKind> for Error {
44    /// Return the type of this error.
45    fn kind(&self) -> &ErrorKind {
46        &self.kind
47    }
48
49    /// The original text string in which this error occurred.
50    fn text(&self) -> &str {
51        &self.text
52    }
53
54    /// Return the span at which this error occurred.
55    fn span(&self) -> &Span {
56        &self.span
57    }
58}
59
60impl fmt::Display for Error {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        self.format_error(f)
63    }
64}
65
66fn format_spans(spans: &[Span]) -> String {
67    spans
68        .iter()
69        .map(|s| s.start.line.to_string())
70        .collect::<Vec<_>>()
71        .join(", ")
72}
73
74/// The type of an error that occurred while building an AST.
75#[derive(Error, Clone, Debug, Eq, PartialEq)]
76#[non_exhaustive]
77pub enum ErrorKind {
78    /// Found two conditions or top-level actions with the same title.
79    #[error("found an identifier more than once in lines: {}", format_spans(.0))]
80    IdentifierDuplicated(Vec<Span>),
81    /// Found a condition with no children.
82    #[error("found a condition with no children")]
83    ConditionEmpty,
84    /// Found an unexpected node. This is most probably a bug in the
85    /// parser implementation.
86    #[error("unexpected child node")]
87    NodeUnexpected,
88    /// Found no rules to emit.
89    #[error("no rules where defined")]
90    TreeEmpty,
91}
92
93/// A visitor that performs semantic analysis on an AST.
94pub struct SemanticAnalyzer<'t> {
95    /// A list of errors that occurred while analyzing the AST.
96    errors: Vec<Error>,
97    /// The original text that the visitor generated the errors from. Every
98    /// span in an error is a valid range into this string.
99    text: &'t str,
100    /// A map from modifier name to it's locations in the input.
101    identifiers: HashMap<String, Vec<Span>>,
102}
103
104impl<'t> SemanticAnalyzer<'t> {
105    /// Create a new semantic analyzer.
106    #[must_use]
107    pub fn new(text: &'t str) -> SemanticAnalyzer<'t> {
108        SemanticAnalyzer {
109            text,
110            errors: Vec::new(),
111            identifiers: HashMap::new(),
112        }
113    }
114
115    /// Create a new error given an AST node and error type.
116    fn error(&mut self, span: Span, kind: ErrorKind) {
117        self.errors.push(Error { kind, text: self.text.to_owned(), span });
118    }
119
120    /// Traverse the given AST and store any errors that occur.
121    ///
122    /// Note that this implementation is a bit weird in that we
123    /// create the `Err` variant of the result by hand.
124    pub fn analyze(&mut self, ast: &ast::Ast) -> Result<()> {
125        match ast {
126            Ast::Root(root) => self.visit_root(root),
127            Ast::Condition(condition) => self.visit_condition(condition),
128            Ast::Action(action) => self.visit_action(action),
129            Ast::ActionDescription(description) => {
130                self.visit_description(description)
131            }
132        }
133        // It is fine to unwrap here since analysis errors will
134        // be stored in `self.errors`.
135        .unwrap();
136
137        // Check for duplicate conditions.
138        for spans in self.identifiers.clone().into_values() {
139            if spans.len() > 1 {
140                self.error(
141                    // FIXME: This is a patch until we start storing locations
142                    // for parts of an AST node. In this case, we need the
143                    // location of the condition's title.
144                    spans[0].with_end(spans[0].start),
145                    ErrorKind::IdentifierDuplicated(spans),
146                );
147            }
148        }
149
150        if !self.errors.is_empty() {
151            return Err(Errors(self.errors.clone()));
152        }
153
154        Ok(())
155    }
156}
157
158/// A visitor that performs semantic analysis on an AST.
159impl Visitor for SemanticAnalyzer<'_> {
160    type Error = ();
161    type Output = ();
162
163    fn visit_root(
164        &mut self,
165        root: &ast::Root,
166    ) -> result::Result<Self::Output, Self::Error> {
167        if root.children.is_empty() {
168            self.error(Span::splat(root.span.end), ErrorKind::TreeEmpty);
169        }
170
171        for ast in &root.children {
172            match ast {
173                Ast::Condition(condition) => {
174                    // NOTE: We no longer record condition titles for duplicate
175                    // detection. Duplicates in condition titles are allowed,
176                    // and a single modifier definition will be reused.
177                    self.visit_condition(condition)?;
178                }
179                Ast::Action(action) => {
180                    // Top-level actions must be checked for duplicates since
181                    // they will become Solidity functions.
182                    let identifier = lower_first_letter(&to_pascal_case(
183                        &sanitize(&action.title),
184                    ));
185                    match self.identifiers.get_mut(&identifier) {
186                        Some(spans) => spans.push(action.span),
187                        None => {
188                            self.identifiers
189                                .insert(identifier, vec![action.span]);
190                        }
191                    }
192                    self.visit_action(action)?;
193                }
194                node => {
195                    self.error(*node.span(), ErrorKind::NodeUnexpected);
196                }
197            }
198        }
199
200        Ok(())
201    }
202
203    fn visit_condition(
204        &mut self,
205        condition: &ast::Condition,
206    ) -> result::Result<Self::Output, Self::Error> {
207        if condition.children.is_empty() {
208            self.error(condition.span, ErrorKind::ConditionEmpty);
209        }
210
211        // IMPORTANT: Allow duplicate condition titles.
212        // We do not record modifiers in `identifiers` anymore, so duplicates
213        // of the same condition title won't trigger an error.
214
215        for ast in &condition.children {
216            match ast {
217                Ast::Condition(condition) => {
218                    self.visit_condition(condition)?;
219                }
220                Ast::Action(action) => {
221                    self.visit_action(action)?;
222                }
223                node => {
224                    self.error(*node.span(), ErrorKind::NodeUnexpected);
225                }
226            }
227        }
228
229        Ok(())
230    }
231
232    fn visit_action(
233        &mut self,
234        _action: &ast::Action,
235    ) -> result::Result<Self::Output, Self::Error> {
236        // We don't implement any semantic rules here for now.
237        Ok(())
238    }
239
240    fn visit_description(
241        &mut self,
242        _description: &ast::Description,
243    ) -> result::Result<Self::Output, Self::Error> {
244        // We don't implement any semantic rules here for now.
245        Ok(())
246    }
247}
248
249#[cfg(test)]
250mod tests {
251
252    use crate::{
253        ast,
254        parser::Parser,
255        semantics::{self, ErrorKind::*},
256        span::{Position, Span},
257        tokenizer::Tokenizer,
258    };
259
260    fn analyze(text: &str) -> semantics::Result<()> {
261        let tokens = Tokenizer::new().tokenize(text).unwrap();
262        let ast = Parser::new().parse(text, &tokens).unwrap();
263        let mut analyzer = semantics::SemanticAnalyzer::new(&text);
264        analyzer.analyze(&ast)?;
265
266        Ok(())
267    }
268
269    #[test]
270    fn unexpected_node() {
271        let ast = ast::Ast::Root(ast::Root {
272            contract_name: "Foo_Test".to_owned(),
273            children: vec![ast::Ast::Root(ast::Root {
274                contract_name: "Foo_Test".to_owned(),
275                children: vec![],
276                span: Span::new(Position::new(0, 1, 1), Position::new(7, 1, 8)),
277            })],
278            span: Span::new(Position::new(0, 1, 1), Position::new(7, 1, 8)),
279        });
280
281        let mut analyzer = semantics::SemanticAnalyzer::new("Foo_Test");
282        let result = analyzer.analyze(&ast);
283        assert_eq!(
284            result.unwrap_err().0,
285            vec![semantics::Error {
286                kind: NodeUnexpected,
287                text: "Foo_Test".to_owned(),
288                span: Span::new(Position::new(0, 1, 1), Position::new(7, 1, 8)),
289            }]
290        );
291    }
292
293    #[test]
294    fn duplicated_top_level_action() {
295        assert_eq!(
296            analyze(
297                "Foo_Test
298├── It should, match the result.
299└── It should' match the result.",
300            )
301            .unwrap_err()
302            .0,
303            vec![semantics::Error {
304                kind: IdentifierDuplicated(vec![
305                    Span::new(Position::new(9, 2, 1), Position::new(46, 2, 32)),
306                    Span::new(Position::new(48, 3, 1), Position::new(85, 3, 32))
307                ]),
308                text:
309                    "Foo_Test\n├── It should, match the result.\n└── It should' match the result."
310                        .to_owned(),
311                span: Span::new(Position::new(9, 2, 1), Position::new(9, 2, 1))
312            }]
313        );
314    }
315
316    #[test]
317    fn condition_empty() {
318        assert_eq!(
319            analyze("Foo_Test\n└── when something").unwrap_err().0,
320            vec![semantics::Error {
321                kind: ConditionEmpty,
322                text: "Foo_Test\n└── when something".to_owned(),
323                span: Span::new(
324                    Position::new(9, 2, 1),
325                    Position::new(32, 2, 18)
326                ),
327            }]
328        );
329    }
330
331    #[test]
332    fn allow_action_without_conditions() {
333        assert!(analyze("Foo_Test\n└── it a something").is_ok());
334    }
335
336    #[test]
337    fn test_multiple_errors() {
338        let text = r"test.sol
339├── when 1
340└── when 2"
341            .to_owned();
342
343        let errors = semantics::Errors(vec![
344            semantics::Error::new(
345                semantics::ErrorKind::ConditionEmpty,
346                text.clone(),
347                Span::new(Position::new(9, 2, 1), Position::new(18, 2, 10)),
348            ),
349            semantics::Error::new(
350                semantics::ErrorKind::ConditionEmpty,
351                text.clone(),
352                Span::new(Position::new(20, 3, 1), Position::new(29, 3, 10)),
353            ),
354        ]);
355        let actual = format!("{errors}");
356
357        let expected = r"•••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••
358bulloak error: found a condition with no children
359
360├── when 1
361^^^^^^^^^^
362
363--- (line 2, column 1) ---
364•••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••
365bulloak error: found a condition with no children
366
367└── when 2
368^^^^^^^^^^
369
370--- (line 3, column 1) ---
371";
372
373        assert_eq!(expected, actual);
374    }
375}