mdbook-rustdoc-links 3.0.2

mdBook preprocessor to support intra-doc links
use std::{collections::BTreeSet, ops::Range};

use cargo_metadata::diagnostic::{
    Diagnostic,
    DiagnosticLevel::{self, *},
    DiagnosticSpan,
};
use tap::Pipe;

use mdbookkit::diagnostics::{
    Highlight, IssueLevel, IssueReport, Note, Suggestion, annotate_snippets::AnnotationKind,
};

use crate::options::{BuildOptions, Builder, CargoOptions, FeatureSelection};

pub struct RustcDiagnostic<'a, 'r> {
    pub diagnostic: &'a Diagnostic,
    pub source_map: &'r (dyn SourceMap + 'r),
}

pub trait SourceMap {
    fn map_span(&self, span: &DiagnosticSpan) -> Option<Range<usize>>;
    fn include_note(&self, diag: &Diagnostic, note: &str) -> bool;
}

impl<'a, 'r> From<RustcDiagnostic<'a, 'r>> for IssueReport<'a> {
    fn from(this: RustcDiagnostic<'a, 'r>) -> Self {
        let RustcDiagnostic {
            diagnostic,
            source_map,
        } = this;

        let annotations = (diagnostic.spans.iter())
            .filter(|item| item.suggested_replacement.is_none())
            .filter_map(|item| {
                Highlight::span(source_map.map_span(item)?)
                    .kind(if item.is_primary {
                        AnnotationKind::Primary
                    } else {
                        AnnotationKind::Context
                    })
                    .maybe_label(item.label.as_ref())
                    .build()
                    .pipe(Some)
            })
            .collect();

        let patches = (diagnostic.spans.iter())
            .filter_map(|item| {
                let repl = item.suggested_replacement.as_ref()?;
                let span = source_map.map_span(item)?;
                Some(Suggestion::span(span).repl(repl).build())
            })
            .collect();

        let notes = (diagnostic.children.iter())
            .filter(|item| is_note(item))
            .filter(|item| source_map.include_note(diagnostic, &item.message))
            .map(|item| {
                Note::level(report_level(item.level))
                    .message(&item.message)
                    .build()
            })
            .collect();

        let secondary = (diagnostic.children.iter())
            .filter(|item| !is_note(item))
            .map(|diagnostic| {
                RustcDiagnostic {
                    diagnostic,
                    source_map,
                }
                .into()
            })
            .collect();

        IssueReport::level(report_level(diagnostic.level))
            .title(&diagnostic.message)
            .annotations(annotations)
            .patches(patches)
            .notes(notes)
            .secondary(secondary)
            .build()
    }
}

pub fn report_level(level: DiagnosticLevel) -> IssueLevel {
    match level {
        Error => IssueLevel::Error,
        Warning => IssueLevel::Warning,
        Note => IssueLevel::Note,
        Help => IssueLevel::Help,
        FailureNote => IssueLevel::Note,
        Ice => IssueLevel::Error,
        _ => IssueLevel::Error,
    }
}

fn is_note(diagnostic: &Diagnostic) -> bool {
    diagnostic.spans.is_empty()
}

#[derive(Debug, Default, Clone)]
pub struct DiagnosticNotes {
    options_specified: BTreeSet<&'static str>,
    preludes_derived: Vec<String>,
    preludes_not_derived: Option<&'static str>,
    visited: VisitedNotes,
}

#[derive(Debug, Default, Clone)]
struct VisitedNotes {
    options_specified: bool,
    preludes_derived: bool,
    preludes_not_derived: bool,
}

impl DiagnosticNotes {
    pub fn note_options_specified(&mut self) -> Option<String> {
        if self.visited.options_specified {
            return None;
        }
        self.visited.options_specified = true;
        let options = self.print_specified_options("")?;
        let note = format! {
            "the following options have been specified, which \
            may have affected link resolution:\n{options}",
        };
        Some(note)
    }

    pub fn print_specified_options(&self, indent: &str) -> Option<String> {
        if self.options_specified.is_empty() {
            return None;
        }
        self.options_specified
            .iter()
            .map(|opt| format!("{indent}- {opt}"))
            .collect::<Vec<_>>()
            .join("\n")
            .pipe(Some)
    }

    pub fn note_preludes_derived(&mut self) -> Option<String> {
        if self.preludes_derived.is_empty() || self.visited.preludes_derived {
            return None;
        }
        self.visited.preludes_derived = true;
        let preludes = self
            .preludes_derived
            .iter()
            .map(|module| format!("use {module};"))
            .collect::<Vec<_>>()
            .join("\n");
        let note = format! {
            "in order to resolve links, the preprocessor creates a temporary crate;\n\
            the following prelude has been implicitly added to the temporary crate:\n`{preludes}`"
        };
        Some(note)
    }

    pub fn note_preludes_not_derived(&mut self) -> Option<String> {
        if self.visited.preludes_not_derived {
            return None;
        }
        self.visited.preludes_not_derived = true;
        let reason = self.preludes_not_derived?;
        let note = format! {
            "in order to resolve links, the preprocessor creates a temporary crate;\n\
            a prelude was not implicitly added to the temporary crate because:\n{reason}"
        };
        Some(note)
    }

    pub fn mark_option_specified(&mut self, builder: &Builder) {
        let Builder { targets, options } = builder;
        let BuildOptions {
            packages,
            preludes,
            features,
            rustc_args,
            rustdoc_args,
            cargo,
            docs_rs,
        } = options;
        if !targets.is_empty() {
            self.options_specified.insert("build.targets");
        }
        if packages.is_some() {
            self.options_specified.insert("build.packages");
        }
        if preludes.is_some() {
            self.options_specified.insert("build.preludes");
        }
        {
            let FeatureSelection {
                features,
                all_features,
                no_default_features,
            } = features;
            if !features.is_empty() {
                self.options_specified.insert("build.features");
            }
            if all_features.is_some() {
                self.options_specified.insert("build.all-features");
            }
            if no_default_features.is_some() {
                self.options_specified.insert("build.no-default-features");
            }
        }
        if !rustc_args.is_empty() {
            self.options_specified.insert("build.rustc-args");
        }
        if !rustdoc_args.is_empty() {
            self.options_specified.insert("build.rustdoc-args");
        }
        {
            let CargoOptions {
                toolchain,
                cargo_args,
                runner,
            } = cargo;
            if toolchain.is_some() {
                self.options_specified.insert("build.toolchain");
            }
            if !cargo_args.is_empty() {
                self.options_specified.insert("build.cargo-args");
            }
            if !runner.is_undefined() {
                self.options_specified.insert("build.runner");
            }
        }
        if docs_rs.is_some() {
            self.options_specified.insert("build.docs-rs");
        }
    }

    pub fn mark_preludes_derived(&mut self, preludes: Vec<String>) -> Vec<String> {
        self.preludes_derived = preludes.clone();
        preludes
    }

    pub fn mark_preludes_not_derived_because(&mut self, reason: &'static str) {
        self.preludes_not_derived = Some(reason)
    }
}

#[macro_export]
macro_rules! with_notes {
    ($emit:ident, $hints:expr) => {
        $emit! { "{:?}{}", {
            if let Some(options) = $hints.print_specified_options("    ") {
                format!("\nnote: the following options have been specified, which may have caused the error:\n{options}\n")
            } else {
                String::new()
            }
        } }
    };
}