use std::collections::BTreeMap;
use std::path::PathBuf;
use std::sync::Arc;
use dre_protocol::host::LogSink;
use serde_json::{Map, Value};
use crate::Diagnostics;
use crate::profiles::{LOCAL_TYPE, Role};
use crate::project::{PluginKind, Project};
use crate::run::find_plugin;
pub use dre_protocol::util::parse_cell;
pub fn is_cell(s: &str) -> bool {
parse_cell(s).is_some()
}
struct Use {
ctx: String,
file: PathBuf,
profile: Option<String>,
}
type Blocks = BTreeMap<String, (Map<String, Value>, Vec<Use>)>;
pub fn check(project: &Project, offline: bool, diags: &mut Diagnostics) {
let mut blocks: BTreeMap<(PluginKind, String), Blocks> = BTreeMap::new();
let mut add = |kind, name: &str, options: &Map<String, Value>, u: Use| {
let key = serde_json::to_string(options).unwrap_or_default();
blocks
.entry((kind, name.to_string()))
.or_default()
.entry(key)
.or_insert_with(|| (options.clone(), Vec::new()))
.1
.push(u);
};
for (format, options) in &project.format_options {
let u = Use {
ctx: format!("`format_options.{format}`"),
file: PathBuf::from(crate::project::PROJECT_FILE),
profile: None,
};
add(PluginKind::Format, format, options, u);
}
for r in &project.reports {
for b in &r.bindings {
let ctx = match &b.set {
Some(s) => format!("report `{}`, Set `{s}`", r.name),
None => format!("report `{}`", r.name),
};
let at = |profile: Option<&str>| Use {
ctx: ctx.clone(),
file: r.file.clone(),
profile: profile.map(str::to_string),
};
add(PluginKind::Format, &b.output.format, &b.output.options, at(None));
for (i, d) in b.output.destinations.iter().enumerate() {
let rendered = b
.parsed
.as_ref()
.and_then(|p| p.destinations.get(i).cloned().flatten());
let profile = rendered.as_deref().unwrap_or(&d.profile);
let kind = if project.profiles.is_builtin_local(profile) {
LOCAL_TYPE
} else {
match project.profiles.target(Role::Destination, profile) {
Some(t) => t.kind.as_str(),
None => continue,
}
};
add(PluginKind::Destination, kind, &d.options, at(Some(profile)));
}
}
}
let log: LogSink = Arc::new(|_, _| {});
for ((kind, name), blocks) in blocks {
let report = |diags: &mut Diagnostics, u: &Use, e: &str| {
let (code, msg) = match &u.profile {
Some(p) => (
"invalid-destination-option",
format!("{}: destination `{p}`: {e}", u.ctx),
),
None => ("invalid-output-option", format!("{}: {e}", u.ctx)),
};
diags.error(code, Some(u.file.clone()), None, msg);
};
if kind == PluginKind::Destination && name == LOCAL_TYPE {
for (options, uses) in blocks.values() {
if let Some(k) = options.keys().next() {
for u in uses {
report(
diags,
u,
&format!(
"the local destination takes no options, but got `{k}`; check the key's spelling"
),
);
}
}
}
continue;
}
let plugin = match find_plugin(project, kind, &name) {
Ok(p) => p,
Err(crate::plugins::LocateError::NotProvided(_)) => continue,
Err(e) => {
let first = blocks.values().flat_map(|(_, u)| u).next();
let file = first.map(|u| u.file.clone());
let at = first.map(|u| format!("{}: ", u.ctx)).unwrap_or_default();
if offline {
diags.warning(
"options-unchecked",
file,
None,
format!("{at}{kind} `{name}`'s options weren't checked: {e}"),
);
} else {
diags.error(
"plugin-not-found",
file,
None,
format!("{at}{kind} `{name}` has no plugin to check it against: {e}"),
);
}
continue;
}
};
let mut p = match plugin.start(log.clone(), Some(&project.root)) {
Ok(p) => p,
Err(e) => {
diags.warning(
"options-unchecked",
None,
None,
format!("can't check the options of {kind} `{name}`: {e}"),
);
continue;
}
};
if !p.has(dre_protocol::CAP_VALIDATE) {
if blocks.values().any(|(o, _)| !o.is_empty()) {
diags.warning(
"options-unchecked",
None,
None,
format!(
"{kind} `{name}` {} predates option checks, so its options weren't checked; run `dre plugin update {kind}/{name}`",
p.info().version
),
);
}
let _ = p.close();
continue;
}
for (options, uses) in blocks.into_values() {
match p.validate(options) {
Ok(errors) => {
for u in &uses {
for e in &errors {
report(diags, u, e);
}
}
}
Err(e) => {
diags.warning(
"options-unchecked",
None,
None,
format!("{kind} `{name}` failed to check its options: {e}"),
);
break;
}
}
}
let _ = p.close();
}
}