use std::path::{Path, PathBuf};
use std::process::ExitCode;
use ridl_core::diag::{DiagCode, Diagnostic, Severity, render};
use ridl_core::interface_lock::{self, InterfaceLock, InvalidLockKey, LockKey, MergeOutcome};
use ridl_core::{RidlDatabase, load_workspace};
use ridl_ir::v2;
use rowan::TextRange;
struct LockedPackage {
dir: PathBuf,
lock: InterfaceLock,
ir: v2::Package,
}
pub(crate) fn only_lock_orphans(diagnostics: &[Diagnostic]) -> bool {
diagnostics.iter().all(|diagnostic| {
diagnostic.severity != Severity::Error || diagnostic.code == DiagCode::RIDL_409
})
}
pub fn run_lock(path: &Path, renames: &[String], retires: &[String]) -> ExitCode {
let renames: Vec<(LockKey, LockKey)> =
match renames.iter().map(|flag| parse_rename(flag)).collect() {
Ok(renames) => renames,
Err(message) => return usage_error(&message),
};
let retires: Vec<LockKey> = match retires
.iter()
.map(|flag| parse_key("--retire", flag))
.collect()
{
Ok(retires) => retires,
Err(message) => return usage_error(&message),
};
let editing = !renames.is_empty() || !retires.is_empty();
let mut db = RidlDatabase::default();
let output = match ridlc::compile_workspace(&mut db, path) {
Ok(output) => output,
Err(err) => {
eprintln!("error: {err}");
return ExitCode::from(2);
}
};
let has_error = output
.diagnostics
.iter()
.any(|diagnostic| diagnostic.severity == Severity::Error);
if has_error && !(editing && only_lock_orphans(&output.diagnostics)) {
eprint!("{}", render(&output.diagnostics, &output.sources));
return ExitCode::FAILURE;
}
let irs: Vec<v2::Package> = output
.checked
.into_iter()
.map(|checked| checked.ir)
.collect();
let mut packages = match locked_packages(path, irs) {
Ok(packages) => packages,
Err(err) => {
eprintln!("error: {err}");
return ExitCode::from(2);
}
};
let outcome = if editing {
edit(path, &mut packages, &renames, &retires)
} else {
allocate(path, &mut packages)
};
if let Err(code) = outcome {
return code;
}
let shown: Vec<Diagnostic> = if editing {
output
.diagnostics
.into_iter()
.filter(|diagnostic| diagnostic.code != DiagCode::RIDL_409)
.collect()
} else {
output.diagnostics
};
eprint!("{}", render(&shown, &output.sources));
ExitCode::SUCCESS
}
fn parse_rename(flag: &str) -> Result<(LockKey, LockKey), String> {
let Some((old, new)) = flag.split_once('=') else {
return Err(format!("--rename takes OLD=NEW, got `{flag}`"));
};
Ok((parse_key("--rename", old)?, parse_key("--rename", new)?))
}
fn parse_key(flag: &str, text: &str) -> Result<LockKey, String> {
text.parse()
.map_err(|InvalidLockKey(reason)| format!("{flag}: {reason}"))
}
fn usage_error(message: &str) -> ExitCode {
eprintln!("error: {message}");
ExitCode::from(2)
}
fn locked_packages(entry: &Path, irs: Vec<v2::Package>) -> std::io::Result<Vec<LockedPackage>> {
let mut db = RidlDatabase::default();
let loaded = load_workspace(&mut db, entry)?;
let handles = loaded.workspace.packages(&db).clone();
debug_assert_eq!(handles.len(), irs.len());
Ok(handles
.iter()
.zip(irs)
.map(|(package, ir)| {
debug_assert_eq!(package.name(&db), &ir.name);
let first = package
.files(&db)
.first()
.expect("a loaded package holds at least one file")
.path(&db);
let dir = Path::new(first)
.parent()
.filter(|dir| !dir.as_os_str().is_empty())
.map_or_else(|| PathBuf::from("."), Path::to_path_buf);
let lock = package
.lock(&db)
.as_ref()
.map_or_else(InterfaceLock::default, |lock| lock.lock.clone());
LockedPackage { dir, lock, ir }
})
.collect())
}
fn allocate(entry: &Path, packages: &mut [LockedPackage]) -> Result<(), ExitCode> {
let prefixed = packages.len() > 1;
for package in packages.iter_mut() {
let mut provisional: Vec<(u32, LockKey)> = package
.ir
.shapes()
.filter(|shape| shape.interface.provisional)
.map(|shape| (shape.interface.number, shape_key(&shape)))
.collect();
if provisional.is_empty() {
continue;
}
provisional.sort();
let prefix = if prefixed {
format!("{}: ", relative_dir(entry, &package.dir))
} else {
String::new()
};
let mut lines = Vec::with_capacity(provisional.len());
for (_, key) in provisional {
let number = package
.lock
.allocate(key.clone())
.expect("a provisional shape has no live entry: the checker read this lock");
lines.push(format!("{prefix}allocated {key} {number}"));
}
write(package)?;
for line in lines {
println!("{line}");
}
}
Ok(())
}
fn edit(
entry: &Path,
packages: &mut [LockedPackage],
renames: &[(LockKey, LockKey)],
retires: &[LockKey],
) -> Result<(), ExitCode> {
let [package] = packages else {
return Err(usage_error(&format!(
"`--rename` and `--retire` edit one package's `{}`, but `{}` holds {} packages; name \
the package directory",
interface_lock::FILE_NAME,
entry.display(),
packages.len()
)));
};
let mut lines = Vec::new();
for (old, new) in renames {
if !is_provisional(&package.ir, new) {
return Err(usage_error(&format!(
"--rename {old}={new}: `{new}` is not a declaration without an entry in package \
`{}`",
package.ir.name
)));
}
let number = package
.lock
.rename(old, new.clone())
.map_err(|err| usage_error(&format!("--rename {old}={new}: {err}")))?;
lines.push(format!("renamed {old} {new} {number}"));
}
for key in retires {
if is_declared(&package.ir, key) {
return Err(usage_error(&format!(
"--retire {key}: `{key}` is still declared in package `{}`; remove the declaration \
first, or keep the entry live",
package.ir.name
)));
}
let number = package
.lock
.retire(key)
.map_err(|err| usage_error(&format!("--retire {key}: {err}")))?;
lines.push(format!("retired {key} {number}"));
}
write(package)?;
for line in lines {
println!("{line}");
}
Ok(())
}
fn write(package: &LockedPackage) -> Result<(), ExitCode> {
interface_lock::write(&package.dir, &package.lock).map_err(|err| {
eprintln!(
"error: cannot write {}: {err}",
package.dir.join(interface_lock::FILE_NAME).display()
);
ExitCode::from(2)
})
}
pub(crate) fn shape_key(shape: &v2::InterfaceShape<'_>) -> LockKey {
if shape.is_inline() {
LockKey::Service(shape.name.to_string())
} else {
LockKey::Interface(shape.name.to_string())
}
}
fn is_declared(ir: &v2::Package, key: &LockKey) -> bool {
ir.shapes().any(|shape| shape_key(&shape) == *key)
}
fn is_provisional(ir: &v2::Package, key: &LockKey) -> bool {
ir.shapes()
.any(|shape| shape.interface.provisional && shape_key(&shape) == *key)
}
fn relative_dir(entry: &Path, dir: &Path) -> String {
match dir.strip_prefix(entry) {
Ok(relative) if relative.as_os_str().is_empty() => ".".to_string(),
Ok(relative) => relative.display().to_string(),
Err(_) => dir.display().to_string(),
}
}
pub fn run_lock_merge(base: &Path, ours: &Path, theirs: &Path, marker_size: usize) -> ExitCode {
let sides = (
read_side(base, true),
read_side(ours, false),
read_side(theirs, false),
);
let (base, ours_lock, theirs_lock) = match sides {
(Ok(base), Ok(ours), Ok(theirs)) => (base, ours, theirs),
(Err(code), _, _) | (_, Err(code), _) | (_, _, Err(code)) => return code,
};
let (text, code) = match interface_lock::merge(&base, &ours_lock, &theirs_lock, marker_size) {
MergeOutcome::Clean(merged) => (merged.render(), ExitCode::SUCCESS),
MergeOutcome::Conflict { text } => {
eprintln!(
"error: {}: the two sides disagree; the disagreeing entries are between git \
conflict markers, and the file is malformed (RIDL-410) until they are resolved \
by hand",
ours.display()
);
(text, ExitCode::FAILURE)
}
};
if let Err(err) = std::fs::write(ours, text) {
eprintln!("error: cannot write {}: {err}", ours.display());
return ExitCode::from(2);
}
code
}
fn read_side(path: &Path, empty_is_default: bool) -> Result<InterfaceLock, ExitCode> {
let text = std::fs::read_to_string(path).map_err(|err| {
eprintln!("error: cannot read {}: {err}", path.display());
ExitCode::from(2)
})?;
if empty_is_default && text.trim().is_empty() {
return Ok(InterfaceLock::default());
}
interface_lock::parse(&text).map_err(|err| {
eprintln!(
"error: {}:{}: {}",
path.display(),
line_of(&text, err.range),
err.message
);
ExitCode::from(2)
})
}
fn line_of(text: &str, range: TextRange) -> usize {
let start = usize::from(range.start()).min(text.len());
text[..start].matches('\n').count() + 1
}