1use 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#[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#[derive(Error, Clone, Debug, Eq, PartialEq)]
24pub struct Error {
25 #[source]
27 kind: ErrorKind,
28 text: String,
31 span: Span,
33}
34
35impl Error {
36 #[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 fn kind(&self) -> &ErrorKind {
46 &self.kind
47 }
48
49 fn text(&self) -> &str {
51 &self.text
52 }
53
54 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#[derive(Error, Clone, Debug, Eq, PartialEq)]
76#[non_exhaustive]
77pub enum ErrorKind {
78 #[error("found an identifier more than once in lines: {}", format_spans(.0))]
80 IdentifierDuplicated(Vec<Span>),
81 #[error("found a condition with no children")]
83 ConditionEmpty,
84 #[error("unexpected child node")]
87 NodeUnexpected,
88 #[error("no rules where defined")]
90 TreeEmpty,
91}
92
93pub struct SemanticAnalyzer<'t> {
95 errors: Vec<Error>,
97 text: &'t str,
100 identifiers: HashMap<String, Vec<Span>>,
102}
103
104impl<'t> SemanticAnalyzer<'t> {
105 #[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 fn error(&mut self, span: Span, kind: ErrorKind) {
117 self.errors.push(Error { kind, text: self.text.to_owned(), span });
118 }
119
120 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 .unwrap();
136
137 for spans in self.identifiers.clone().into_values() {
139 if spans.len() > 1 {
140 self.error(
141 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
158impl 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 self.visit_condition(condition)?;
178 }
179 Ast::Action(action) => {
180 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 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 Ok(())
238 }
239
240 fn visit_description(
241 &mut self,
242 _description: &ast::Description,
243 ) -> result::Result<Self::Output, Self::Error> {
244 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}