sofie-check 0.3.0

Structure checks (comment density, god types) and the `sofie check` rules over the app model.
Documentation
use std::fmt;
use std::ops::Add;

use crate::lines::LineKind;
use crate::workspace::{CrateSource, SourceFile};

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct CommentDensity {
    pub code_lines: usize,
    pub comment_lines: usize,
}

impl CommentDensity {
    #[must_use]
    pub fn of_file(file: &SourceFile) -> Self {
        let count = |wanted: LineKind| file.lines.iter().filter(|kind| **kind == wanted).count();
        Self {
            code_lines: count(LineKind::Code),
            comment_lines: count(LineKind::Comment),
        }
    }

    #[must_use]
    pub fn of_crate(crate_source: &CrateSource) -> Self {
        crate_source
            .files
            .iter()
            .map(Self::of_file)
            .fold(Self::default(), Add::add)
    }

    #[must_use]
    pub fn within_limit(self) -> bool {
        self.comment_lines <= crate::structure::FREE_COMMENT_LINES
            || self.ratio() <= crate::structure::MAX_COMMENT_RATIO
    }

    #[must_use]
    pub fn ratio(self) -> f64 {
        if self.code_lines == 0 {
            return 0.0;
        }
        crate::count_as_f64(self.comment_lines) / crate::count_as_f64(self.code_lines)
    }
}

impl Add for CommentDensity {
    type Output = Self;

    fn add(self, other: Self) -> Self {
        Self {
            code_lines: self.code_lines + other.code_lines,
            comment_lines: self.comment_lines + other.comment_lines,
        }
    }
}

impl fmt::Display for CommentDensity {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            formatter,
            "ratio {:.3} ({} comment / {} code lines)",
            self.ratio(),
            self.comment_lines,
            self.code_lines
        )
    }
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use super::CommentDensity;
    use crate::workspace::SourceFile;

    fn density(text: &str) -> CommentDensity {
        CommentDensity::of_file(&SourceFile::parse(PathBuf::from("lib.rs"), text).unwrap())
    }

    #[test]
    fn counts_code_and_comment_lines() {
        let density = density("// why\nfn a() {}\n\nfn b() {} // trailing\n");
        assert_eq!(
            density,
            CommentDensity {
                code_lines: 2,
                comment_lines: 1
            }
        );
        assert!((density.ratio() - 0.5).abs() < f64::EPSILON);
    }

    #[test]
    fn source_without_code_has_zero_ratio() {
        assert!(density("// only a comment").ratio().abs() < f64::EPSILON);
    }
}