bhc-parser 0.2.4

Parser for Haskell 2026 source code
Documentation
//! Test parsing XMonad source files.
//!
//! These tests parse a local XMonad checkout at /tmp/xmonad and are skipped
//! when it is absent (clone https://github.com/xmonad/xmonad to /tmp/xmonad
//! to run them).

use bhc_parser::parse_module;
use bhc_span::FileId;
use std::fs;

/// Returns true (and prints a notice) when the XMonad checkout is missing.
fn xmonad_missing() -> bool {
    if std::path::Path::new("/tmp/xmonad/src").exists() {
        false
    } else {
        eprintln!("skipping: no XMonad checkout at /tmp/xmonad");
        true
    }
}

fn test_parse_file(path: &str) -> (usize, usize, usize) {
    let source = fs::read_to_string(path).expect("read file");
    let file_id = FileId::new(0);
    let (module, diagnostics) = parse_module(&source, file_id);

    let errors = diagnostics
        .iter()
        .filter(|d| d.severity == bhc_diagnostics::Severity::Error)
        .count();

    match module {
        Some(m) => (m.imports.len(), m.decls.len(), errors),
        None => (0, 0, errors),
    }
}

#[test]
fn test_xmonad_core() {
    if xmonad_missing() {
        return;
    }
    let (imports, decls, errors) = test_parse_file("/tmp/xmonad/src/XMonad/Core.hs");
    println!(
        "Core.hs: {} imports, {} decls, {} errors",
        imports, decls, errors
    );
    // Just check it parses at all
    assert!(decls > 0, "Should have some declarations");
}

#[test]
fn test_xmonad_stackset() {
    if xmonad_missing() {
        return;
    }
    let (imports, decls, errors) = test_parse_file("/tmp/xmonad/src/XMonad/StackSet.hs");
    println!(
        "StackSet.hs: {} imports, {} decls, {} errors",
        imports, decls, errors
    );
    assert!(decls > 0, "Should have some declarations");
}

#[test]
fn test_xmonad_main() {
    if xmonad_missing() {
        return;
    }
    let (imports, decls, errors) = test_parse_file("/tmp/xmonad/src/XMonad.hs");
    println!(
        "XMonad.hs: {} imports, {} decls, {} errors",
        imports, decls, errors
    );
    assert!(decls > 0 || imports > 0, "Should have content");
}

#[test]
fn test_all_xmonad_files() {
    if xmonad_missing() {
        return;
    }
    let files = [
        ("/tmp/xmonad/src/XMonad.hs", "XMonad"),
        ("/tmp/xmonad/src/XMonad/Core.hs", "Core"),
        ("/tmp/xmonad/src/XMonad/StackSet.hs", "StackSet"),
        ("/tmp/xmonad/src/XMonad/Config.hs", "Config"),
        ("/tmp/xmonad/src/XMonad/Layout.hs", "Layout"),
        ("/tmp/xmonad/src/XMonad/Main.hs", "Main"),
        ("/tmp/xmonad/src/XMonad/ManageHook.hs", "ManageHook"),
        ("/tmp/xmonad/src/XMonad/Operations.hs", "Operations"),
    ];

    let mut total_imports = 0;
    let mut total_decls = 0;
    let mut total_errors = 0;

    for (path, name) in files {
        let (imports, decls, errors) = test_parse_file(path);
        println!(
            "{}: {} imports, {} decls, {} errors",
            name, imports, decls, errors
        );
        total_imports += imports;
        total_decls += decls;
        total_errors += errors;
    }

    println!(
        "\nTOTAL: {} imports, {} decls, {} errors",
        total_imports, total_decls, total_errors
    );
}

#[test]
fn test_layout_parse_errors() {
    if xmonad_missing() {
        return;
    }
    let path = "/tmp/xmonad/src/XMonad/Layout.hs";
    let source = fs::read_to_string(path).expect("read file");
    let file_id = FileId::new(0);
    let (_module, diagnostics) = parse_module(&source, file_id);

    println!("\n=== Layout.hs Parse Errors ===\n");
    for d in diagnostics
        .iter()
        .filter(|d| d.severity == bhc_diagnostics::Severity::Error)
    {
        println!("Error: {}", d.message);
        for label in &d.labels {
            let offset = label.span.span.lo.0 as usize;
            let line = source[..offset].matches('\n').count() + 1;
            let line_start = source[..offset].rfind('\n').map(|x| x + 1).unwrap_or(0);
            let col = offset - line_start;
            println!("  At line {}, col {}: {}", line, col, label.message);
            // Show the source line
            let line_end = source[offset..]
                .find('\n')
                .map(|x| offset + x)
                .unwrap_or(source.len());
            println!("    {}", &source[line_start..line_end]);
        }
        println!();
    }
}

#[test]
fn test_operations_parse_errors() {
    if xmonad_missing() {
        return;
    }
    let path = "/tmp/xmonad/src/XMonad/Operations.hs";
    let source = fs::read_to_string(path).expect("read file");
    let file_id = FileId::new(0);
    let (_module, diagnostics) = parse_module(&source, file_id);

    println!("\n=== Operations.hs Parse Errors ===\n");
    for d in diagnostics
        .iter()
        .filter(|d| d.severity == bhc_diagnostics::Severity::Error)
        .take(10)
    {
        println!("Error: {}", d.message);
        for label in &d.labels {
            let offset = label.span.span.lo.0 as usize;
            let line = source[..offset].matches('\n').count() + 1;
            let line_start = source[..offset].rfind('\n').map(|x| x + 1).unwrap_or(0);
            let col = offset - line_start;
            println!("  At line {}, col {}: {}", line, col, label.message);
            // Show the source line
            let line_end = source[offset..]
                .find('\n')
                .map(|x| offset + x)
                .unwrap_or(source.len());
            println!("    {}", &source[line_start..line_end]);
        }
        println!();
    }
}