cairo-annotations 0.3.1

Structured annotations for Cairo
Documentation
use crate::annotations::impl_helpers::impl_namespace;
use cairo_lang_sierra::program::StatementIdx;
use derive_more::{Add, AddAssign, Display, Div, DivAssign, Mul, MulAssign, Sub, SubAssign};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::HashMap;

/// Versioned representation of Coverage Annotations.
///
/// Always prefer using this enum when Serializing/Deserializing instead of inner ones.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum VersionedCoverageAnnotations {
    V1(CoverageAnnotationsV1),
}

/// The mapping from sierra statement index
/// to stack of a locations in Cairo code
/// which caused the statement to be generated.
/// And all functions that were inlined
/// or generated along the way up to the first non-inlined function from the original code.
///
/// The vector represents the stack from the least meaningful elements.
///
/// Introduced in Scarb 2.8.0.
///
/// Needs `unstable-add-statements-code-locations-debug-info = true`
/// under `[profile.dev.cairo]` in the Scarb config to be generated.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct CoverageAnnotationsV1 {
    pub statements_code_locations: HashMap<StatementIdx, Vec<CodeLocation>>,
}

/// Represents the location of a Sierra statement in the source code, along with an optional flag
/// indicating whether it was generated by a macro.
///
/// The macro-generated flag is available only in `scarb` versions >= `2.11.0`.
/// - If `Some(true)`, the statement was generated by a macro.
/// - If `Some(false)`, the statement was not generated by a macro.
/// - If `None`, the information is unavailable (due to using an older `scarb` version).
#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct CodeLocation(
    pub SourceFileFullPath,
    pub SourceCodeSpan,
    #[serde(default, skip_serializing_if = "Option::is_none")] pub Option<bool>,
);

/// A full path to a Cairo source file.
///
/// Up to `scarb` `2.8.5` will contain multiple Cairo virtual file markings if the code is generated by macros.
/// Like `/path/to/project/lib.cairo[array_inline_macro][assert_macro]`
/// where `array_inline_macro` and `assert_macro` is a virtual file marking.
#[derive(
    Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize, Deserialize, Display, Default,
)]
pub struct SourceFileFullPath(pub String);

impl SourceFileFullPath {
    /// Removes virtual file markings from the path
    /// and returns the cleaned path along with any extracted markings.
    ///
    /// A virtual file marking is enclosed in square brackets (`[...]`) and appended to the path.
    /// It appears only in `scarb` versions up to `2.8.5`. In later versions, this function will always
    /// return the original path as `&str` and an empty vector of markings.
    ///
    /// Note that this function returns `&str` instead of creating a new `SourceFileFullPath`
    /// to avoid unnecessary allocations.
    ///
    /// # Panics
    /// - If a virtual file marking does not end with `]`, which is unexpected.
    ///
    /// # Example
    /// ```
    /// use cairo_annotations::annotations::coverage::SourceFileFullPath;
    ///
    /// let path = SourceFileFullPath("/path/to/project/lib.cairo[array_inline_macro][assert_macro]".to_string());
    /// let (cleaned, markings) = path.remove_virtual_file_markings();
    /// assert_eq!(cleaned, "/path/to/project/lib.cairo");
    /// assert_eq!(markings, vec!["array_inline_macro", "assert_macro"]);
    /// ```
    #[must_use]
    pub fn remove_virtual_file_markings(&self) -> (&str, Vec<&str>) {
        let mut parts = self.0.split('[');
        let path = parts
            .next()
            .unwrap_or_else(|| unreachable!("split always returns at least one element"));

        let virtual_file_markings = parts
            .map(|virtual_file| {
                virtual_file
                    .strip_suffix(']')
                    .expect("virtual file marking should end with ']'")
            })
            .collect();

        (path, virtual_file_markings)
    }
}

/// A span in a Cairo source file.
#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct SourceCodeSpan {
    /// Beginning of the text span in the Cairo source file.
    pub start: SourceCodeLocation,
    /// End of the text span in the Cairo source file. Currently, always the same as `start`.
    pub end: SourceCodeLocation,
}

/// A location in a Cairo source file.
#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct SourceCodeLocation {
    /// Line index, 0 based.
    /// Most editors show line numbers starting from 1, so when displaying to users, add 1 to this value.
    pub line: LineNumber,
    /// Character index inside the line, 0 based.
    /// Most editors show column numbers starting from 1, so when displaying to users, add 1 to this value.
    pub col: ColumnNumber,
}

#[derive(
    Clone,
    Copy,
    Debug,
    Eq,
    PartialEq,
    Hash,
    Ord,
    PartialOrd,
    Serialize,
    Deserialize,
    Add,
    AddAssign,
    Sub,
    SubAssign,
    Mul,
    MulAssign,
    Div,
    DivAssign,
    Display,
    Default,
)]
pub struct ColumnNumber(pub usize);

#[derive(
    Clone,
    Copy,
    Debug,
    Eq,
    PartialEq,
    Hash,
    Ord,
    PartialOrd,
    Serialize,
    Deserialize,
    Add,
    AddAssign,
    Sub,
    SubAssign,
    Mul,
    MulAssign,
    Div,
    DivAssign,
    Display,
    Default,
)]
pub struct LineNumber(pub usize);

// We can't use untagged enum here. See https://github.com/serde-rs/json/issues/1103
impl Serialize for VersionedCoverageAnnotations {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        match self {
            VersionedCoverageAnnotations::V1(v1) => v1.serialize(serializer),
        }
    }
}

impl<'de> Deserialize<'de> for VersionedCoverageAnnotations {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        CoverageAnnotationsV1::deserialize(deserializer).map(VersionedCoverageAnnotations::V1)
    }
}

impl_namespace!(
    "github.com/software-mansion/cairo-coverage",
    CoverageAnnotationsV1,
    VersionedCoverageAnnotations
);

#[cfg(test)]
mod test {
    use super::*;
    #[test]
    fn test_remove_virtual_file_markings() {
        let path = SourceFileFullPath(
            "/path/to/project/lib.cairo[array_inline_macro][assert_macro]".to_string(),
        );
        let (path, virtual_file_markings) = path.remove_virtual_file_markings();
        assert_eq!(path, "/path/to/project/lib.cairo");
        assert_eq!(
            virtual_file_markings,
            vec!["array_inline_macro", "assert_macro"]
        );
    }

    #[test]
    fn test_remove_virtual_file_markings_no_markings() {
        let path = SourceFileFullPath("/path/to/project/lib.cairo".to_string());
        let (path, virtual_file_markings) = path.remove_virtual_file_markings();
        assert_eq!(path, "/path/to/project/lib.cairo");
        assert_eq!(virtual_file_markings, Vec::<&str>::new());
    }

    #[test]
    fn test_remove_virtual_file_markings_empty() {
        let path = SourceFileFullPath(String::new());
        let (path, virtual_file_markings) = path.remove_virtual_file_markings();
        assert_eq!(path, "");
        assert_eq!(virtual_file_markings, Vec::<&str>::new());
    }

    #[test]
    fn test_remove_virtual_file_markings_single_marking() {
        let path = SourceFileFullPath("/path/to/project/lib.cairo[array_inline_macro]".to_string());
        let (path, virtual_file_markings) = path.remove_virtual_file_markings();
        assert_eq!(path, "/path/to/project/lib.cairo");
        assert_eq!(virtual_file_markings, vec!["array_inline_macro"]);
    }
}