alkale 2.0.0

A simple LL(1) lexer library for Rust.
Documentation
//! Module that contains standard [`Notification`] type and related data.

use core::cmp::Ordering;
use std::collections::BinaryHeap;

use crate::span::Span;

/// Indicates that a type acts as a [`Notification`] aggregator, allowing notifications
/// to be appended using [`report`][NotificationAcceptor::report]
pub trait NotificationAcceptor<T> {
    /// Report a [`Notification`] to this type.
    fn report(&mut self, notification: Notification<T>);
}

/// Represents how important a [`Notification`] is, ranging from not important at all,
/// to indicating an unrecoverable program failure.
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Ord, Eq)]
#[non_exhaustive]
pub enum NotificationSeverity {
    /// This notification does not need user attention and does not
    /// affect result validity.
    Info,
    /// This notification is something that needs user attention,
    /// but isn't an error that affects the validity of the result.
    Warning,
    /// This notification was caused by an error and that
    /// the result is invalid.
    Error,
    /// This notification was caused by a fatal error that caused
    /// an immediate exit.
    Fatal,
}

/// A standard notification, representing a single error, warning, or other informational message.
///
/// A new instance can be created using [`NotificationBuilder`], and should generally
/// be reported to a [`NotificationAcceptor`].
///
/// Notifications contain many properties to work in the general case, but
/// most of them are not required, it depends on your project and the needs of
/// your notifications.
///
/// An `extra` field is available for any project-specific data required,
/// the single generic this type takes represents the extra data's type.
///
/// Notification are sorted and checked for equality purely based on their [`Span`] and nothing else.
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct Notification<T = ()> {
    /// The main lint message
    message: String,
    /// The severity of the notification.
    severity: NotificationSeverity,
    /// Where the notification is located.
    span: Option<Span>,
    /// Suggested fix.
    fix: Option<String>,
    /// Tracking issue link.
    issue: Option<String>,
    /// Notification's extra, language-specific data.
    extra: Option<T>,
}

impl<T> Notification<T> {
    /// This [`Notification`]'s message— what the notification is warning against.
    #[inline]
    pub fn message(&self) -> &str {
        &self.message
    }

    /// This [`Notification`]'s span— the position it refers to in the source code.
    #[inline]
    #[must_use]
    pub const fn span(&self) -> &Option<Span> {
        &self.span
    }

    /// This [`Notification`]'s severity— how important it is.
    #[inline]
    #[must_use]
    pub const fn severity(&self) -> &NotificationSeverity {
        &self.severity
    }

    /// Get this [`Notification`]'s suggested fix.
    #[inline]
    #[must_use]
    pub const fn fix(&self) -> &Option<String> {
        &self.fix
    }

    /// Get this [`Notification`]'s issue URL.
    #[inline]
    #[must_use]
    pub const fn issue(&self) -> &Option<String> {
        &self.issue
    }

    /// Get this [`Notification`]'s extra, implementation-specific data.
    #[inline]
    #[must_use]
    pub const fn extra(&self) -> &Option<T> {
        &self.extra
    }

    /// Report this [`Notification`] to a given [`NotificationAcceptor`]. If this
    /// notification was just created from a [`NotificationBuilder`], consider calling
    /// [`report`][NotificationBuilder::report] directly instead of building.
    #[inline]
    pub fn report<A: NotificationAcceptor<T>>(self, acceptor: &mut A) {
        acceptor.report(self);
    }
}

impl<T> PartialEq for Notification<T> {
    #[inline]
    fn eq(&self, other: &Self) -> bool {
        self.span == other.span
    }
}

impl<T> Eq for Notification<T> {}

impl<T> PartialOrd for Notification<T> {
    #[inline]
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl<T> Ord for Notification<T> {
    #[inline]
    #[expect(clippy::option_if_let_else)]
    fn cmp(&self, other: &Self) -> Ordering {
        if let Some(first) = self.span {
            other.span.map_or(Ordering::Greater, |s| first.cmp(&s))
        } else if other.span.is_some() {
            Ordering::Less
        } else {
            Ordering::Equal
        }
    }
}

/// A builder type for [`Notification`]s.
#[derive(Debug)]
pub struct NotificationBuilder<T = ()> {
    /// The result we're building.
    result: Notification<T>,
}

impl<T> NotificationBuilder<T> {
    /// Begin building a new [`Notification`] using the argument as a base message.
    /// If passing in a [`format`]ted string, use the [`format_notification`][crate::format_notification] macro
    /// instead.
    #[inline]
    #[expect(clippy::needless_pass_by_value)]
    pub fn new<S: ToString>(message: S) -> Self {
        Self {
            result: Notification {
                message: message.to_string(),
                severity: NotificationSeverity::Info,
                span: None,
                fix: None,
                issue: None,
                extra: None,
            },
        }
    }

    /// Set this [`Notification`]'s span.
    #[inline]
    #[must_use]
    pub const fn span(mut self, span: Span) -> Self {
        self.result.span = Some(span);
        self
    }

    /// Set this [`Notification`]'s severity level.
    #[inline]
    #[must_use]
    pub const fn severity(mut self, severity: NotificationSeverity) -> Self {
        self.result.severity = severity;
        self
    }

    /// Set this [`Notification`]'s suggested fix.
    #[inline]
    #[must_use]
    #[expect(clippy::needless_pass_by_value)]
    pub fn fix<S: ToString>(mut self, fix: S) -> Self {
        self.result.fix = Some(fix.to_string());
        self
    }

    /// Set this [`Notification`]'s issue URL.
    #[inline]
    #[must_use]
    #[expect(clippy::needless_pass_by_value)]
    pub fn issue<S: ToString>(mut self, issue: S) -> Self {
        self.result.issue = Some(issue.to_string());
        self
    }

    /// Set this [`Notification`]'s extra, implementation-specific data.
    #[inline]
    #[must_use]
    pub fn extra(mut self, extra: T) -> Self {
        self.result.extra = Some(extra);
        self
    }

    /// Complete this [`Notification`] and return it.
    #[inline]
    pub fn build(self) -> Notification<T> {
        self.result
    }

    /// Report this [`Notification`] to a given [NotificationAcceptor]. This method
    /// is preferred instead of [build][Self::build]ing and immediately reporting
    /// the produced notification.
    #[inline]
    pub fn report<A: NotificationAcceptor<T>>(self, acceptor: &mut A) {
        acceptor.report(self.result);
    }
}

/// A list of reported [`Notification`]s— the stored notifications are
/// kept in sorted order.
#[derive(Debug)]
pub struct NotificationList<T> {
    /// The main collection of notifications.
    heap: BinaryHeap<Notification<T>>,
    /// True if no notifications have been reported yet
    /// that are severity error or higher.
    is_valid: bool,
}

impl<T> NotificationList<T> {
    /// Creates a new [`NotificationList`].
    #[inline]
    #[must_use]
    pub const fn new() -> Self {
        Self {
            heap: BinaryHeap::new(),
            is_valid: true,
        }
    }

    /// Creates a new [`NotificationList`] with the specified capacity.
    #[inline]
    #[must_use]
    pub fn with_capacity(capacity: usize) -> Self {
        Self {
            heap: BinaryHeap::with_capacity(capacity),
            is_valid: true,
        }
    }

    /// Push a new [`Notification`] to this list.
    #[inline]
    pub fn push(&mut self, notification: Notification<T>) {
        if self.is_valid && notification.severity >= NotificationSeverity::Error {
            self.is_valid = false;
        }

        self.heap.push(notification);
    }

    /// Convert this list into a sorted [`Vec`] of notifications. All unspanned
    /// notifications will be placed first in the list, the rest will be sorted
    /// by the index of their [`Span`].
    #[inline]
    #[must_use]
    pub fn into_sorted_vec(self) -> Vec<Notification<T>> {
        self.heap.into_sorted_vec()
    }

    /// Returns true if no reported notifications are severity [error][NotificationSeverity::Error]
    /// or higher.
    #[inline]
    #[must_use]
    pub const fn is_valid(&self) -> bool {
        self.is_valid
    }
}

impl<T> Default for NotificationList<T> {
    #[inline]
    fn default() -> Self {
        Self::new()
    }
}

impl<T> NotificationList<T> {
    /// Invokes the argument predicate once for each [`Notification`] stored in this list.
    ///
    /// The predicate takes the current notification and its position as arguments. Its position
    /// is an [`Option`] of tuple `(<line number>, <column number>)`, both 1-based. Both the line
    /// and column number are derived from the input code, so care should be taken to ensure that
    /// the [`Span`]'s of the notifications were all generated from the input code string.
    #[inline]
    pub fn handle<F: Fn(Notification<T>, Option<(usize, usize)>)>(self, code: &str, predicate: F) {
        // Iter of notifications
        let mut notifications = self.into_sorted_vec().into_iter().peekable();
        let chars = code.chars();
        // The number of characters consumed so far.
        let mut current_index = 0usize;
        let mut current_col = 1usize;
        let mut current_line = 1usize;

        for char in chars {
            loop {
                // No notifications left, halt.
                let Some(notification) = notifications.peek() else {
                    return;
                };

                // If it has no span, just immediately run and try again.
                // In practice, this will cause all unspanned notifications to be
                // filtered out immediately.
                let Some(span) = notification.span else {
                    // SAFETY: peek() returned a value, this will just take
                    // ownedship of it.
                    let owned = unsafe { notifications.next().unwrap_unchecked() };

                    predicate(owned, None);
                    continue;
                };

                // If this notification is for this index, call the method and
                // continue, otherwise stop.
                if span.index() == current_index {
                    // SAFETY: peek() returned a value, this will just take
                    // ownedship of it.
                    let owned = unsafe { notifications.next().unwrap_unchecked() };

                    predicate(owned, Some((current_line, current_col)));
                } else {
                    break;
                }
            }

            // SAFETY: current_index is always a valid index for
            // the source code, which has an upper bound of usize::MAX too.
            unsafe {
                current_index = current_index.unchecked_add(1);
            }

            if char == '\n' {
                // In the worst case, our source code is usize::MAX newline characters
                // which would make this be 1 off for the final line. Not a big
                // deal at all.
                current_line = current_line.saturating_add(1);

                current_col = 1;
            } else {
                // In the worst case, our source code is usize::MAX with no newline characters
                // which would make this be 1 off for the final char. Not a big
                // deal at all.
                current_col = current_col.saturating_add(1);
            }
        }
    }
}

/// Works the same as the [`format`][std::format] macro, but passes its result
/// into [`NotificationBuilder::new`]. This may produce more succinct code
/// than passing the format macro into the constructor.
#[macro_export]
macro_rules! format_notification {
    ($($x:tt)*) => {
        $crate::notification::NotificationBuilder::<_>::new(std::format!($($x)*))
    }
}