merman-analysis 0.8.0-alpha.3

Diagnostics-first analysis contracts and source mapping for Merman.
Documentation
use crate::diagnostic_projection::{
    ParseDiagnosticLocation, core_error_diagnostic, rule_diagnostic,
    rule_diagnostic_without_default_span,
};
use crate::rules::{DIAGRAM_PARSE_RULE_ID, RECOVERED_EDITOR_FACTS_RULE_ID};
use crate::{AnalysisDiagnostic, AnalysisStatus, SourceMap};
use merman_core::{EditorSemanticDiagnostic, EditorSemanticDiagnosticKind};

#[derive(Debug, Clone)]
pub(crate) struct AnalysisRecoveryDiagnostic {
    pub(crate) diagnostic: AnalysisDiagnostic,
    kind: Option<EditorSemanticDiagnosticKind>,
}

impl AnalysisRecoveryDiagnostic {
    pub(crate) fn parser_backed(
        diagnostic: AnalysisDiagnostic,
        kind: EditorSemanticDiagnosticKind,
    ) -> Self {
        Self {
            diagnostic,
            kind: Some(kind),
        }
    }

    pub(crate) fn plain(diagnostic: AnalysisDiagnostic) -> Self {
        Self {
            diagnostic,
            kind: None,
        }
    }
}

pub(crate) fn merge_recovery_diagnostics(
    diagnostics: &mut Vec<AnalysisDiagnostic>,
    recovery_diagnostics: Vec<AnalysisRecoveryDiagnostic>,
    primary_parse_location: Option<ParseDiagnosticLocation>,
) {
    for recovery in recovery_diagnostics {
        if merge_duplicate_parse_recovery_diagnostic(diagnostics, &recovery, primary_parse_location)
        {
            continue;
        }
        diagnostics.push(recovery.diagnostic);
    }
}

fn merge_duplicate_parse_recovery_diagnostic(
    diagnostics: &mut [AnalysisDiagnostic],
    recovery: &AnalysisRecoveryDiagnostic,
    primary_parse_location: Option<ParseDiagnosticLocation>,
) -> bool {
    if recovery.kind != Some(EditorSemanticDiagnosticKind::ParserRecovery) {
        return false;
    }

    let Some(primary) = diagnostics.iter_mut().find(|diagnostic| {
        is_same_parse_recovery_problem(diagnostic, &recovery.diagnostic, primary_parse_location)
    }) else {
        return false;
    };

    if is_better_primary_parse_span(primary, &recovery.diagnostic) {
        if let Some(previous_span) = primary.span.clone() {
            primary.related.push(crate::DiagnosticRelated {
                message: "Parser reported this original parse location before recovery refinement."
                    .to_string(),
                span: Some(previous_span),
            });
        }
        primary.span = recovery.diagnostic.span.clone();
    }
    primary.related.push(crate::DiagnosticRelated {
        message: "Parser recovery produced the same syntax problem while preserving editor facts."
            .to_string(),
        span: recovery.diagnostic.span.clone(),
    });
    true
}

fn is_same_parse_recovery_problem(
    primary: &AnalysisDiagnostic,
    recovery: &AnalysisDiagnostic,
    primary_parse_location: Option<ParseDiagnosticLocation>,
) -> bool {
    if primary.id != DIAGRAM_PARSE_RULE_ID || recovery.id != RECOVERED_EDITOR_FACTS_RULE_ID {
        return false;
    }
    if primary.diagram_type != recovery.diagram_type {
        return false;
    }
    spans_describe_same_problem(primary.span.as_ref(), recovery.span.as_ref())
        || primary_parse_location == Some(ParseDiagnosticLocation::Fallback)
}

fn is_better_primary_parse_span(
    primary: &AnalysisDiagnostic,
    recovery: &AnalysisDiagnostic,
) -> bool {
    let Some(recovery_span) = recovery.span.as_ref() else {
        return false;
    };
    if recovery_span.byte_start == recovery_span.byte_end {
        return false;
    }
    match primary.span.as_ref() {
        None => true,
        Some(primary_span) if primary_span.byte_start == primary_span.byte_end => true,
        Some(primary_span)
            if primary_span.byte_start == recovery_span.byte_start
                && primary_span.byte_end == recovery_span.byte_end =>
        {
            false
        }
        Some(primary_span) => {
            let primary_len = primary_span
                .byte_end
                .saturating_sub(primary_span.byte_start);
            let recovery_len = recovery_span
                .byte_end
                .saturating_sub(recovery_span.byte_start);
            recovery_len > 0 && recovery_len < primary_len
        }
    }
}

fn spans_describe_same_problem(
    primary: Option<&crate::DiagnosticSpan>,
    recovery: Option<&crate::DiagnosticSpan>,
) -> bool {
    match (primary, recovery) {
        (None, _) | (_, None) => true,
        (Some(primary), Some(recovery)) => {
            primary.byte_start == recovery.byte_start
                || primary.byte_end == recovery.byte_end
                || spans_overlap(primary, recovery)
                || point_touches_span(primary, recovery)
                || point_touches_span(recovery, primary)
        }
    }
}

fn spans_overlap(left: &crate::DiagnosticSpan, right: &crate::DiagnosticSpan) -> bool {
    left.byte_start < right.byte_end && right.byte_start < left.byte_end
}

fn point_touches_span(point: &crate::DiagnosticSpan, span: &crate::DiagnosticSpan) -> bool {
    point.byte_start == point.byte_end
        && span.byte_start <= point.byte_start
        && point.byte_start <= span.byte_end
}

pub(crate) fn editor_recovery_diagnostics(
    diagnostics: impl IntoIterator<Item = EditorSemanticDiagnostic>,
    diagram_type: &str,
    source_map: &SourceMap,
    rule_config: &crate::rules::AnalysisRuleConfig,
    source_mapped_spans: bool,
) -> Vec<AnalysisRecoveryDiagnostic> {
    diagnostics
        .into_iter()
        .filter_map(|diagnostic| {
            recovered_editor_diagnostic(
                diagnostic,
                diagram_type,
                source_map,
                rule_config,
                source_mapped_spans,
            )
        })
        .collect()
}

fn recovered_editor_diagnostic(
    diagnostic: EditorSemanticDiagnostic,
    diagram_type: &str,
    source_map: &SourceMap,
    rule_config: &crate::rules::AnalysisRuleConfig,
    source_mapped_spans: bool,
) -> Option<AnalysisRecoveryDiagnostic> {
    let kind = diagnostic.kind;
    let mut out = if source_mapped_spans {
        rule_diagnostic(
            RECOVERED_EDITOR_FACTS_RULE_ID,
            AnalysisStatus::ParseError,
            diagnostic.message,
            source_map,
            rule_config,
        )?
    } else {
        rule_diagnostic_without_default_span(
            RECOVERED_EDITOR_FACTS_RULE_ID,
            AnalysisStatus::ParseError,
            diagnostic.message,
            rule_config,
        )?
    }
    .with_diagram_type(diagram_type);

    if source_mapped_spans {
        if let Some(span) = diagnostic
            .span
            .and_then(|span| source_map.span(span.start, span.end).ok())
        {
            out = out.with_span(span);
        }
    }

    Some(AnalysisRecoveryDiagnostic::parser_backed(out, kind))
}

pub(crate) fn core_error_recovery_diagnostics(
    error: merman_core::Error,
    source_map: &SourceMap,
    rule_config: &crate::rules::AnalysisRuleConfig,
) -> Vec<AnalysisRecoveryDiagnostic> {
    core_error_diagnostic(error, source_map, rule_config)
        .diagnostic
        .map(AnalysisRecoveryDiagnostic::plain)
        .into_iter()
        .collect()
}