harn-cli 0.8.35

CLI for the Harn programming language — run, test, REPL, format, and lint
Documentation
use std::path::Path;

use harn_parser::analysis::{
    AnalysisDatabase, AnalysisError, SourceId, SourceVersion, TypeCheckConfig, TypeCheckOutput,
};

use crate::package::CheckConfig;

#[derive(Debug)]
pub(crate) enum FileAnalysisError {
    Read(std::io::Error),
    Analysis(AnalysisError),
}

pub(crate) fn analyze_file(
    analysis: &mut AnalysisDatabase,
    path: &Path,
    config: &CheckConfig,
    module_graph: &harn_modules::ModuleGraph,
) -> Result<TypeCheckOutput, FileAnalysisError> {
    let source = std::fs::read_to_string(path).map_err(FileAnalysisError::Read)?;
    let id = SourceId::path(path);
    analysis.set_source(id.clone(), source, SourceVersion(1));
    analysis
        .typecheck(&id, typecheck_config(path, config, module_graph))
        .map_err(FileAnalysisError::Analysis)
}

pub(crate) fn typecheck_config(
    path: &Path,
    config: &CheckConfig,
    module_graph: &harn_modules::ModuleGraph,
) -> TypeCheckConfig {
    TypeCheckConfig::new()
        .with_strict_types(config.strict_types)
        .with_imported_names(module_graph.imported_names_for_file(path))
        .with_imported_type_decls(
            module_graph
                .imported_type_declarations_for_file(path)
                .unwrap_or_default(),
        )
        .with_imported_callable_decls(
            module_graph
                .imported_callable_declarations_for_file(path)
                .unwrap_or_default(),
        )
}

pub(crate) fn render_file_analysis_error_or_exit(path: &str, error: FileAnalysisError) -> ! {
    match error {
        FileAnalysisError::Read(error) => {
            eprintln!("Error reading {path}: {error}");
        }
        FileAnalysisError::Analysis(AnalysisError::Lex { source, error }) => {
            let diagnostic = harn_parser::diagnostic::render_diagnostic_with_code(
                &source,
                path,
                &span_from_lexer_error(&error),
                "error",
                harn_parser::diagnostic::lexer_error_code(&error),
                &error.to_string(),
                Some("here"),
                None,
            );
            eprint!("{diagnostic}");
        }
        FileAnalysisError::Analysis(AnalysisError::Parse { source, errors }) => {
            for error in &errors {
                let diagnostic = harn_parser::diagnostic::render_diagnostic_with_code(
                    &source,
                    path,
                    &span_from_parser_error(error),
                    "error",
                    harn_parser::diagnostic::parser_error_code(error),
                    &harn_parser::diagnostic::parser_error_message(error),
                    Some(harn_parser::diagnostic::parser_error_label(error)),
                    harn_parser::diagnostic::parser_error_help(error),
                );
                eprint!("{diagnostic}");
            }
        }
        FileAnalysisError::Analysis(AnalysisError::MissingSource(id)) => {
            eprintln!("missing analysis source {}", id.as_str());
        }
    }
    std::process::exit(1);
}

pub(crate) fn span_from_lexer_error(error: &harn_lexer::LexerError) -> harn_lexer::Span {
    match error {
        harn_lexer::LexerError::UnexpectedCharacter(_, span)
        | harn_lexer::LexerError::UnterminatedString(span)
        | harn_lexer::LexerError::UnterminatedBlockComment(span) => *span,
    }
}

pub(crate) fn span_from_parser_error(error: &harn_parser::ParserError) -> harn_lexer::Span {
    match error {
        harn_parser::ParserError::Unexpected { span, .. }
        | harn_parser::ParserError::UnexpectedEof { span, .. } => *span,
    }
}