use crate::markdown_linter_guard::{GuardFailure, GuardLimits, run_guarded};
use crate::markdown_linter_config::one_rule_configuration;
use crate::markdown_linter_output::{LintRun, explanation, interpret_embedded_run};
use crate::git_metadata::path_from_git_bytes;
use crate::policy_markdown::{LintRequest, MarkdownLinter};
use crate::wait_progress::Cadence;
use monochromatic_lint::run_embedded::{EmbeddedFailure, EmbeddedRequest, EmbeddedRun, fix_source};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::time::Duration;
pub const LINT_QUIET_WINDOW: Duration = Duration::from_secs(60);
pub type FixFunction = fn(&EmbeddedRequest<'_>) -> Result<EmbeddedRun, EmbeddedFailure>;
pub struct EmbeddedLinter {
fix: FixFunction,
window: Duration,
ceiling: Duration,
cadence: Cadence,
notices: Box<dyn Write>,
abandoned: bool,
}
pub fn embedded_linter() -> EmbeddedLinter {
return EmbeddedLinter {
fix: fix_source,
window: LINT_QUIET_WINDOW,
ceiling: crate::wait_ceilings::LINT_CEILING,
cadence: Cadence::from_environment(),
notices: Box::new(std::io::stderr()),
abandoned: false,
};
}
pub fn embedded_linter_with(fix: FixFunction, window: Duration) -> EmbeddedLinter {
return EmbeddedLinter {
fix,
window,
ceiling: crate::wait_ceilings::LINT_CEILING,
cadence: Cadence::from_environment(),
notices: Box::new(std::io::stderr()),
abandoned: false,
};
}
const ABANDONED_REASON: &str = "an earlier Markdown check in this command did not finish and cli-git stopped waiting \
for it, so monochromatic-lint is not run again in this command; run the command again, or set the policy \
markdown/autofix to off to skip the check";
fn guard_failure_reason(failure: &GuardFailure) -> String {
match failure {
GuardFailure::Start(reason) => {
return format!("cli-git could not start the thread that runs monochromatic-lint: {reason}");
}
GuardFailure::Panicked(reason) => {
return format!(
"monochromatic-lint stopped with an internal error ({}); the file was not checked, run the \
command again, run monochromatic-lint by hand on the file to see the error, or set the policy \
markdown/autofix to off to skip the check",
explanation(reason.as_str())
);
}
GuardFailure::Quiet(window) => {
return format!(
"monochromatic-lint showed no sign of work for {} milliseconds and cli-git stopped waiting for it \
(its thread stays in this process until the command ends); run the command again, run \
monochromatic-lint by hand on the file to see why it stalls, or set the policy markdown/autofix \
to off to skip the check",
window.as_millis()
);
}
GuardFailure::Ceiling(ceiling) => {
return format!(
"monochromatic-lint ran for {} seconds, the longest cli-git lets one Markdown check run, and cli-git stopped waiting \
for it (its thread stays in this process until the command ends); run the command again, run \
monochromatic-lint by hand on the file, or set the policy markdown/autofix to off to skip the check",
ceiling.as_secs()
);
}
GuardFailure::Vanished => {
return String::from(
"the thread that runs monochromatic-lint ended without a result; this is a defect in cli-git.",
);
}
}
}
impl MarkdownLinter for EmbeddedLinter {
fn lint(&mut self, request: &LintRequest<'_>) -> Result<LintRun, String> {
if self.abandoned {
return Err(String::from(ABANDONED_REASON));
}
let Some(name) = path_from_git_bytes(request.path) else {
return Err(String::from(
"cli-git cannot spell this candidate's pathname for monochromatic-lint on this platform",
));
};
let waiting_for: String = format!("monochromatic-lint to check {}", name.display());
let checking: String = name.display().to_string();
let top_level: PathBuf = request.top_level.to_path_buf();
let configuration: String = one_rule_configuration(
request.options.rules.as_slice(),
request.options.exclude.as_slice(),
);
let source: Vec<u8> = request.source.to_vec();
let fix: FixFunction = self.fix;
let outcome: Result<Result<EmbeddedRun, EmbeddedFailure>, GuardFailure> = run_guarded(
move || {
return fix(&EmbeddedRequest {
top_level: top_level.as_path(),
configuration: configuration.as_str(),
name: Path::new(name.as_os_str()),
source: source.as_slice(),
});
},
GuardLimits { window: self.window, ceiling: self.ceiling },
self.cadence,
waiting_for.as_str(),
&mut self.notices,
);
match outcome {
Ok(finished) => {
return interpret_embedded_run(request.source, request.options.rules.as_slice(), finished);
}
Err(failure) => {
self.abandoned = matches!(failure, GuardFailure::Quiet(_) | GuardFailure::Ceiling(_));
if matches!(failure, GuardFailure::Ceiling(_)) {
return Err(format!("monochromatic-lint, checking {checking}: {}", guard_failure_reason(&failure)));
}
return Err(guard_failure_reason(&failure));
}
}
}
}
impl EmbeddedLinter {
pub fn with_ceiling(mut self, ceiling: Duration) -> Self {
self.ceiling = ceiling;
return self;
}
pub fn with_notices(mut self, cadence: Cadence, notices: Box<dyn Write>) -> Self {
self.cadence = cadence;
self.notices = notices;
return self;
}
}
#[cfg(test)]
#[path = "markdown_linter_tests.rs"]
mod tests;