use std::{collections::HashMap, fmt, result};
use thiserror::Error;
use super::ast::{self, Ast};
use crate::{
error::FrontendError,
span::Span,
utils::{lower_first_letter, sanitize, to_pascal_case},
visitor::Visitor,
};
type Result<T> = result::Result<T, Errors>;
#[derive(Error, Clone, Debug, PartialEq, Eq)]
#[error("{}", .0.iter().map(|e| e.to_string()).collect::<Vec<_>>().join(""))]
pub struct Errors(pub Vec<Error>);
#[derive(Error, Clone, Debug, Eq, PartialEq)]
pub struct Error {
#[source]
kind: ErrorKind,
text: String,
span: Span,
}
impl Error {
#[cfg(test)]
pub fn new(kind: ErrorKind, text: String, span: Span) -> Self {
Error { kind, text, span }
}
}
impl FrontendError<ErrorKind> for Error {
fn kind(&self) -> &ErrorKind {
&self.kind
}
fn text(&self) -> &str {
&self.text
}
fn span(&self) -> &Span {
&self.span
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.format_error(f)
}
}
fn format_spans(spans: &[Span]) -> String {
spans
.iter()
.map(|s| s.start.line.to_string())
.collect::<Vec<_>>()
.join(", ")
}
#[derive(Error, Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ErrorKind {
#[error("found an identifier more than once in lines: {}", format_spans(.0))]
IdentifierDuplicated(Vec<Span>),
#[error("found a condition with no children")]
ConditionEmpty,
#[error("unexpected child node")]
NodeUnexpected,
#[error("no rules where defined")]
TreeEmpty,
}
pub struct SemanticAnalyzer<'t> {
errors: Vec<Error>,
text: &'t str,
identifiers: HashMap<String, Vec<Span>>,
}
impl<'t> SemanticAnalyzer<'t> {
#[must_use]
pub fn new(text: &'t str) -> SemanticAnalyzer<'t> {
SemanticAnalyzer {
text,
errors: Vec::new(),
identifiers: HashMap::new(),
}
}
fn error(&mut self, span: Span, kind: ErrorKind) {
self.errors.push(Error { kind, text: self.text.to_owned(), span });
}
pub fn analyze(&mut self, ast: &ast::Ast) -> Result<()> {
match ast {
Ast::Root(root) => self.visit_root(root),
Ast::Condition(condition) => self.visit_condition(condition),
Ast::Action(action) => self.visit_action(action),
Ast::ActionDescription(description) => {
self.visit_description(description)
}
}
.unwrap();
for spans in self.identifiers.clone().into_values() {
if spans.len() > 1 {
self.error(
spans[0].with_end(spans[0].start),
ErrorKind::IdentifierDuplicated(spans),
);
}
}
if !self.errors.is_empty() {
return Err(Errors(self.errors.clone()));
}
Ok(())
}
}
impl Visitor for SemanticAnalyzer<'_> {
type Error = ();
type Output = ();
fn visit_root(
&mut self,
root: &ast::Root,
) -> result::Result<Self::Output, Self::Error> {
if root.children.is_empty() {
self.error(Span::splat(root.span.end), ErrorKind::TreeEmpty);
}
for ast in &root.children {
match ast {
Ast::Condition(condition) => {
self.visit_condition(condition)?;
}
Ast::Action(action) => {
let identifier = lower_first_letter(&to_pascal_case(
&sanitize(&action.title),
));
match self.identifiers.get_mut(&identifier) {
Some(spans) => spans.push(action.span),
None => {
self.identifiers
.insert(identifier, vec![action.span]);
}
}
self.visit_action(action)?;
}
node => {
self.error(*node.span(), ErrorKind::NodeUnexpected);
}
}
}
Ok(())
}
fn visit_condition(
&mut self,
condition: &ast::Condition,
) -> result::Result<Self::Output, Self::Error> {
if condition.children.is_empty() {
self.error(condition.span, ErrorKind::ConditionEmpty);
}
for ast in &condition.children {
match ast {
Ast::Condition(condition) => {
self.visit_condition(condition)?;
}
Ast::Action(action) => {
self.visit_action(action)?;
}
node => {
self.error(*node.span(), ErrorKind::NodeUnexpected);
}
}
}
Ok(())
}
fn visit_action(
&mut self,
_action: &ast::Action,
) -> result::Result<Self::Output, Self::Error> {
Ok(())
}
fn visit_description(
&mut self,
_description: &ast::Description,
) -> result::Result<Self::Output, Self::Error> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::{
ast,
parser::Parser,
semantics::{self, ErrorKind::*},
span::{Position, Span},
tokenizer::Tokenizer,
};
fn analyze(text: &str) -> semantics::Result<()> {
let tokens = Tokenizer::new().tokenize(text).unwrap();
let ast = Parser::new().parse(text, &tokens).unwrap();
let mut analyzer = semantics::SemanticAnalyzer::new(&text);
analyzer.analyze(&ast)?;
Ok(())
}
#[test]
fn unexpected_node() {
let ast = ast::Ast::Root(ast::Root {
contract_name: "Foo_Test".to_owned(),
children: vec![ast::Ast::Root(ast::Root {
contract_name: "Foo_Test".to_owned(),
children: vec![],
span: Span::new(Position::new(0, 1, 1), Position::new(7, 1, 8)),
})],
span: Span::new(Position::new(0, 1, 1), Position::new(7, 1, 8)),
});
let mut analyzer = semantics::SemanticAnalyzer::new("Foo_Test");
let result = analyzer.analyze(&ast);
assert_eq!(
result.unwrap_err().0,
vec![semantics::Error {
kind: NodeUnexpected,
text: "Foo_Test".to_owned(),
span: Span::new(Position::new(0, 1, 1), Position::new(7, 1, 8)),
}]
);
}
#[test]
fn duplicated_top_level_action() {
assert_eq!(
analyze(
"Foo_Test
├── It should, match the result.
└── It should' match the result.",
)
.unwrap_err()
.0,
vec![semantics::Error {
kind: IdentifierDuplicated(vec![
Span::new(Position::new(9, 2, 1), Position::new(46, 2, 32)),
Span::new(Position::new(48, 3, 1), Position::new(85, 3, 32))
]),
text:
"Foo_Test\n├── It should, match the result.\n└── It should' match the result."
.to_owned(),
span: Span::new(Position::new(9, 2, 1), Position::new(9, 2, 1))
}]
);
}
#[test]
fn condition_empty() {
assert_eq!(
analyze("Foo_Test\n└── when something").unwrap_err().0,
vec![semantics::Error {
kind: ConditionEmpty,
text: "Foo_Test\n└── when something".to_owned(),
span: Span::new(
Position::new(9, 2, 1),
Position::new(32, 2, 18)
),
}]
);
}
#[test]
fn allow_action_without_conditions() {
assert!(analyze("Foo_Test\n└── it a something").is_ok());
}
#[test]
fn test_multiple_errors() {
let text = r"test.sol
├── when 1
└── when 2"
.to_owned();
let errors = semantics::Errors(vec![
semantics::Error::new(
semantics::ErrorKind::ConditionEmpty,
text.clone(),
Span::new(Position::new(9, 2, 1), Position::new(18, 2, 10)),
),
semantics::Error::new(
semantics::ErrorKind::ConditionEmpty,
text.clone(),
Span::new(Position::new(20, 3, 1), Position::new(29, 3, 10)),
),
]);
let actual = format!("{errors}");
let expected = r"•••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••
bulloak error: found a condition with no children
├── when 1
^^^^^^^^^^
--- (line 2, column 1) ---
•••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••
bulloak error: found a condition with no children
└── when 2
^^^^^^^^^^
--- (line 3, column 1) ---
";
assert_eq!(expected, actual);
}
}