bwq 0.3.1

A linter and parser for Brandwatch query files (.bwq)
Documentation
use thiserror::Error;

#[derive(Debug, Clone, PartialEq)]
pub struct Position {
    pub line: usize,
    pub column: usize,
    pub offset: usize,
}

impl Position {
    pub fn new(line: usize, column: usize, offset: usize) -> Self {
        Self {
            line,
            column,
            offset,
        }
    }
}

#[derive(Debug, Clone, PartialEq)]
pub struct Span {
    pub start: Position,
    pub end: Position,
}

impl Span {
    pub fn new(start: Position, end: Position) -> Self {
        Self { start, end }
    }

    pub fn single(pos: Position) -> Self {
        Self {
            start: pos.clone(),
            end: pos,
        }
    }
}

#[derive(Error, Debug, Clone, PartialEq)]
pub enum LintError {
    #[error("Lexer error at {position:?}: {message}")]
    LexerError { position: Position, message: String },

    #[error("Parser error at {span:?}: {message}")]
    ParserError { span: Span, message: String },

    #[error("Validation error at {span:?}: {message}")]
    ValidationError { span: Span, message: String },

    #[error("Boolean operator '{operator}' must be capitalized at {span:?}")]
    InvalidBooleanCase { span: Span, operator: String },

    #[error("Unbalanced parentheses at {span:?}")]
    UnbalancedParentheses { span: Span },

    #[error(
        "Invalid wildcard placement at {span:?}: wildcards cannot be at the beginning of a word"
    )]
    InvalidWildcardPlacement { span: Span },

    #[error("Invalid proximity operator syntax at {span:?}: {message}")]
    InvalidProximityOperator { span: Span, message: String },

    #[error("Invalid field operator syntax at {span:?}: {message}")]
    InvalidFieldOperator { span: Span, message: String },

    #[error("Invalid range syntax at {span:?}: expected '[value TO value]'")]
    InvalidRangeSyntax { span: Span },

    #[error("Unexpected token '{token}' at {span:?}")]
    UnexpectedToken { span: Span, token: String },

    #[error("Expected '{expected}' but found '{found}' at {span:?}")]
    ExpectedToken {
        span: Span,
        expected: String,
        found: String,
    },
}

#[derive(Debug, Clone, PartialEq)]
pub enum LintWarning {
    PotentialTypo { span: Span, suggestion: String },
    DeprecatedOperator { span: Span, replacement: String },
    PerformanceWarning { span: Span, message: String },
}

pub type LintResult<T> = Result<T, LintError>;

#[derive(Debug, Clone, PartialEq, Default)]
pub struct LintReport {
    pub errors: Vec<LintError>,
    pub warnings: Vec<LintWarning>,
}

impl LintReport {
    pub fn new() -> Self {
        Self {
            errors: Vec::new(),
            warnings: Vec::new(),
        }
    }

    pub fn add_error(&mut self, error: LintError) {
        self.errors.push(error);
    }

    pub fn add_warning(&mut self, warning: LintWarning) {
        self.warnings.push(warning);
    }

    pub fn has_errors(&self) -> bool {
        !self.errors.is_empty()
    }

    pub fn has_warnings(&self) -> bool {
        !self.warnings.is_empty()
    }

    pub fn is_clean(&self) -> bool {
        self.errors.is_empty() && self.warnings.is_empty()
    }
}