dprint-plugin-pug 0.1.4

A super minimal Pug formatter plugin for dprint.
Documentation
mod support;

pub use support::{ast, config, formatter, lexer, parser};

use std::collections::BTreeMap;

use config::Configuration;
use support::{FixtureBehavior, FixtureRole, FormatOutcome, StructureCoverage, UpstreamFixture};

#[test]
fn registers_all_vendored_upstream_pug_fixtures_by_bucket() {
    let fixtures = support::upstream_pug_sources();

    assert_eq!(
        fixtures.len(),
        459,
        "expected to register every vendored .pug fixture"
    );

    let expected = vec![
        (String::from("packages/pug-filters/test/cases"), 1),
        (String::from("packages/pug-lexer/test/cases"), 118),
        (String::from("packages/pug-lexer/test/errors"), 26),
        (String::from("packages/pug-linker/test/cases-src"), 40),
        (String::from("packages/pug-linker/test/errors-src"), 3),
        (String::from("packages/pug-linker/test/fixtures"), 17),
        (
            String::from("packages/pug-linker/test/special-cases-src"),
            3,
        ),
        (String::from("packages/pug/examples"), 19),
        (String::from("packages/pug/test/anti-cases"), 22),
        (String::from("packages/pug/test/browser"), 1),
        (String::from("packages/pug/test/cases"), 137),
        (String::from("packages/pug/test/cases-es2015"), 1),
        (String::from("packages/pug/test/dependencies"), 7),
        (String::from("packages/pug/test/duplicate-block"), 2),
        (String::from("packages/pug/test/eachOf/error"), 4),
        (String::from("packages/pug/test/eachOf/passing"), 2),
        (String::from("packages/pug/test/extends-not-top-level"), 3),
        (String::from("packages/pug/test/fixtures"), 39),
        (String::from("packages/pug/test/markdown-it"), 2),
        (String::from("packages/pug/test/regression-2436"), 6),
        (String::from("packages/pug/test/shadowed-block"), 3),
        (String::from("packages/pug/test/temp"), 3),
    ];

    assert_eq!(
        support::bucket_counts(&fixtures),
        expected,
        "upstream fixture bucket inventory changed"
    );
}

#[test]
fn formats_every_vendored_upstream_pug_fixture_without_panicking() {
    for fixture in support::upstream_pug_sources() {
        let formatted = support::format_source(&fixture.source, &Configuration::default());
        assert!(
            formatted.ends_with('\n'),
            "formatted output should end with a newline for {}",
            fixture.relative_path
        );
    }
}

#[test]
fn reports_current_upstream_case_and_anti_case_coverage() {
    let fixtures = support::upstream_pug_sources();
    let behaviors = fixtures
        .iter()
        .map(support::analyze_fixture)
        .collect::<Vec<_>>();

    let report = render_behavior_report(&fixtures, &behaviors);

    let total_by_role = summarize_by_role(&behaviors);
    assert_eq!(
        total_by_role,
        vec![
            (FixtureRole::Example, 19),
            (FixtureRole::Case, 308),
            (FixtureRole::AntiCase, 55),
            (FixtureRole::Support, 77),
        ],
        "fixture role inventory drifted\n{report}"
    );

    let format_by_role = summarize_format_outcomes(&behaviors);
    let structure_by_role = summarize_structure_coverage(&behaviors);
    let diagnostics_by_role = summarize_diagnostics_by_role(&behaviors);

    assert_eq!(
        format_by_role.into_iter().collect::<Vec<_>>(),
        vec![
            ((FixtureRole::Example, FormatOutcome::Idempotent), 3),
            ((FixtureRole::Example, FormatOutcome::Rewritten), 16),
            ((FixtureRole::Case, FormatOutcome::Idempotent), 66),
            ((FixtureRole::Case, FormatOutcome::Rewritten), 242),
            ((FixtureRole::AntiCase, FormatOutcome::Idempotent), 14),
            ((FixtureRole::AntiCase, FormatOutcome::Rewritten), 41),
            ((FixtureRole::Support, FormatOutcome::Idempotent), 13),
            ((FixtureRole::Support, FormatOutcome::Rewritten), 64),
        ],
        "format outcome register drifted\n{report}"
    );

    assert_eq!(
        structure_by_role.into_iter().collect::<Vec<_>>(),
        vec![
            (
                (FixtureRole::Example, StructureCoverage::FullyStructured),
                10
            ),
            ((FixtureRole::Example, StructureCoverage::Mixed), 9),
            ((FixtureRole::Case, StructureCoverage::NoStatements), 4),
            ((FixtureRole::Case, StructureCoverage::FullyStructured), 225),
            ((FixtureRole::Case, StructureCoverage::Mixed), 75),
            ((FixtureRole::Case, StructureCoverage::RawOnly), 4),
            (
                (FixtureRole::AntiCase, StructureCoverage::FullyStructured),
                39
            ),
            ((FixtureRole::AntiCase, StructureCoverage::Mixed), 5),
            ((FixtureRole::AntiCase, StructureCoverage::RawOnly), 11),
            ((FixtureRole::Support, StructureCoverage::NoStatements), 1),
            (
                (FixtureRole::Support, StructureCoverage::FullyStructured),
                72
            ),
            ((FixtureRole::Support, StructureCoverage::Mixed), 2),
            ((FixtureRole::Support, StructureCoverage::RawOnly), 2),
        ],
        "structure coverage register drifted\n{report}"
    );

    assert_eq!(
        diagnostics_by_role.into_iter().collect::<Vec<_>>(),
        vec![
            ((FixtureRole::Case, String::from("warnings")), 2),
            ((FixtureRole::AntiCase, String::from("warnings")), 11),
        ],
        "diagnostic inventory drifted\n{report}"
    );

    println!("{report}");
}

#[test]
fn checked_in_upstream_register_report_is_current() {
    let fixtures = support::upstream_pug_sources();
    let behaviors = fixtures
        .iter()
        .map(support::analyze_fixture)
        .collect::<Vec<_>>();
    let report = render_behavior_report(&fixtures, &behaviors);
    let expected = std::fs::read_to_string(support::vendored_upstream_register_path())
        .expect("checked-in upstream register should exist");

    assert_eq!(report, expected, "checked-in upstream register drifted");
}

fn summarize_by_role(behaviors: &[FixtureBehavior]) -> Vec<(FixtureRole, usize)> {
    let mut counts = BTreeMap::new();

    for behavior in behaviors {
        *counts.entry(behavior.role).or_insert(0usize) += 1;
    }

    counts.into_iter().collect()
}

fn summarize_format_outcomes(
    behaviors: &[FixtureBehavior],
) -> BTreeMap<(FixtureRole, FormatOutcome), usize> {
    let mut counts = BTreeMap::new();

    for behavior in behaviors {
        *counts
            .entry((behavior.role, behavior.format_outcome))
            .or_insert(0usize) += 1;
    }

    counts
}

fn summarize_structure_coverage(
    behaviors: &[FixtureBehavior],
) -> BTreeMap<(FixtureRole, StructureCoverage), usize> {
    let mut counts = BTreeMap::new();

    for behavior in behaviors {
        *counts
            .entry((behavior.role, behavior.structure_coverage))
            .or_insert(0usize) += 1;
    }

    counts
}

fn summarize_diagnostics_by_role(
    behaviors: &[FixtureBehavior],
) -> BTreeMap<(FixtureRole, String), usize> {
    let mut counts = BTreeMap::new();

    for behavior in behaviors {
        *counts
            .entry((behavior.role, String::from("warnings")))
            .or_insert(0usize) += behavior.diagnostics.warnings;
        *counts
            .entry((behavior.role, String::from("errors")))
            .or_insert(0usize) += behavior.diagnostics.errors;
        *counts
            .entry((behavior.role, String::from("fatals")))
            .or_insert(0usize) += behavior.diagnostics.fatals;
    }

    counts.retain(|_, count| *count > 0);
    counts
}

fn render_behavior_report(fixtures: &[UpstreamFixture], behaviors: &[FixtureBehavior]) -> String {
    let mut output = String::new();

    output.push_str("Upstream fixture register\n");
    output.push_str("========================\n");
    output.push_str(&format!("total fixtures: {}\n", behaviors.len()));
    output.push('\n');

    output.push_str("By bucket\n");
    for (bucket, count) in support::bucket_counts(fixtures) {
        output.push_str(&format!("- {bucket}: {count}\n"));
    }
    output.push('\n');

    output.push_str("By role\n");
    for (role, count) in summarize_by_role(behaviors) {
        output.push_str(&format!("- {role}: {count}\n"));
    }
    output.push('\n');

    output.push_str("Format outcomes by role\n");
    for ((role, outcome), count) in summarize_format_outcomes(behaviors) {
        output.push_str(&format!("- {role} / {}: {count}\n", format_label(outcome)));
    }
    output.push('\n');

    output.push_str("Structure coverage by role\n");
    for ((role, coverage), count) in summarize_structure_coverage(behaviors) {
        output.push_str(&format!(
            "- {role} / {}: {count}\n",
            structure_label(coverage)
        ));
    }
    output.push('\n');

    output.push_str("Diagnostics by role\n");
    let diagnostics_by_role = summarize_diagnostics_by_role(behaviors);
    if diagnostics_by_role.is_empty() {
        output.push_str("- none\n");
    } else {
        for ((role, severity), count) in diagnostics_by_role {
            output.push_str(&format!("- {role} / {severity}: {count}\n"));
        }
    }
    output.push('\n');

    output.push_str("Rewritten anti-cases\n");
    let rewritten_anti_cases = behaviors
        .iter()
        .filter(|behavior| {
            behavior.role == FixtureRole::AntiCase
                && behavior.format_outcome == FormatOutcome::Rewritten
        })
        .collect::<Vec<_>>();
    if rewritten_anti_cases.is_empty() {
        output.push_str("- none\n");
    } else {
        for behavior in rewritten_anti_cases {
            output.push_str(&format!(
                "- {} [{}]\n",
                behavior.relative_path, behavior.bucket
            ));
        }
    }
    output.push('\n');

    output.push_str("Warned case fixtures\n");
    let warned_case_fixtures = behaviors
        .iter()
        .filter(|behavior| behavior.role == FixtureRole::Case && behavior.diagnostics.warnings > 0)
        .collect::<Vec<_>>();
    if warned_case_fixtures.is_empty() {
        output.push_str("- none\n");
    } else {
        for behavior in warned_case_fixtures {
            output.push_str(&format!(
                "- {} [{}] warnings={}\n",
                behavior.relative_path, behavior.bucket, behavior.diagnostics.warnings
            ));
        }
    }
    output.push('\n');

    output.push_str("Warned anti-cases\n");
    let warned_anti_cases = behaviors
        .iter()
        .filter(|behavior| {
            behavior.role == FixtureRole::AntiCase && behavior.diagnostics.warnings > 0
        })
        .collect::<Vec<_>>();
    if warned_anti_cases.is_empty() {
        output.push_str("- none\n");
    } else {
        for behavior in warned_anti_cases {
            output.push_str(&format!(
                "- {} [{}] warnings={}\n",
                behavior.relative_path, behavior.bucket, behavior.diagnostics.warnings
            ));
        }
    }
    output.push('\n');

    output.push_str("Most opaque case fixtures\n");
    let mut opaque_cases = behaviors
        .iter()
        .filter(|behavior| behavior.role == FixtureRole::Case)
        .collect::<Vec<_>>();
    opaque_cases.sort_by(|left, right| {
        right
            .stats
            .raw_statements
            .cmp(&left.stats.raw_statements)
            .then_with(|| left.relative_path.cmp(&right.relative_path))
    });

    for behavior in opaque_cases.into_iter().take(20) {
        output.push_str(&format!(
            "- raw {}/{} | {} | {}\n",
            behavior.stats.raw_statements,
            behavior.stats.statements,
            structure_label(behavior.structure_coverage),
            behavior.relative_path
        ));
    }

    output
}

fn format_label(outcome: FormatOutcome) -> &'static str {
    match outcome {
        FormatOutcome::Idempotent => "idempotent",
        FormatOutcome::Rewritten => "rewritten",
    }
}

fn structure_label(coverage: StructureCoverage) -> &'static str {
    match coverage {
        StructureCoverage::NoStatements => "no-statements",
        StructureCoverage::FullyStructured => "fully-structured",
        StructureCoverage::Mixed => "mixed",
        StructureCoverage::RawOnly => "raw-only",
    }
}