use ltk_hash::{BinHash, Hash as _};
use ltk_meta::path::PropertyPath;
use crate::{
ast::{
diagnostics::{Diagnostic as D, PatchField, TypeMismatch},
node::{
root::{FileKind, RootKind, RootValue},
roots::Roots,
MapEntry,
},
Property, RootPatch, Value,
},
rito, Spanned,
};
use super::Builder;
const PATCH_CLASS: &str = "patch";
const PATCH_TEXT_VERSION: u32 = 3;
impl Builder<'_> {
pub(super) fn resolve_file_kind(&mut self, roots: &mut Roots) {
if roots.file_type().map(|root| root.value) == Some(FileKind::Patch) {
self.resolve_patch_records(roots);
self.check_patch_roots(roots);
return;
}
for root in &*roots {
if matches!(*root.name, RootKind::Patches | RootKind::Deleted) {
self.push(
D::PatchOnlyRoot {
span: root.name.span,
root_kind: *root.name,
}
.unwrap(),
);
}
}
}
fn resolve_patch_records(&mut self, roots: &mut Roots) {
let Some(patches) = roots.patches.as_mut() else {
return;
};
let Some(Some(RootValue::Value(Value::Map(map)))) =
roots.all.get(patches.idx).map(|root| &root.value)
else {
return;
};
patches.value = map
.entries
.iter()
.filter_map(|MapEntry { key, value, .. }| self.resolve_patch(key, value.as_ref()))
.collect();
}
fn check_patch_roots(&mut self, roots: &Roots) {
if let Some(version) = roots.version() {
if version.value != PATCH_TEXT_VERSION {
self.push(
D::UnsupportedPatchVersion {
span: version.original(roots).value_span(),
version: version.value,
}
.unwrap(),
);
}
}
if let Some(linked) = roots.linked() {
if !linked.value.is_empty() {
self.push(
D::PatchLinked {
span: linked.original(roots).value_span(),
}
.unwrap(),
);
}
}
}
fn resolve_patch(&mut self, key: &Value, value: Option<&Value>) -> Option<RootPatch> {
let Value::Hash(object_hash) = key else {
return None;
};
let object_hash = object_hash.try_as_bin_hash()?;
let Some(Value::Embedded(record)) = value else {
return None;
};
if record.class_hash.value != BinHash::hash_str(PATCH_CLASS) {
self.push(
D::UnexpectedPatchClass {
span: record.class_hash.span(),
}
.unwrap(),
);
}
let mut fields = PatchFields::default();
for property in &record.properties {
let Some(field) = PatchField::named(property.name.value) else {
self.push(
D::UnexpectedPatchField {
span: property.span(),
}
.unwrap(),
);
continue;
};
let slot = fields.slot(field);
if slot.is_some() {
self.push(
D::DuplicatePatchField {
span: property.name.span(),
field,
}
.unwrap(),
);
continue;
}
*slot = Some(property);
}
for field in PatchField::ALL {
if fields.get(field).is_none() {
self.push(
D::MissingPatchField {
span: record.class_hash.span(),
field,
}
.unwrap(),
);
}
}
let path = self.resolve_patch_path(fields.path?.value.as_ref());
let patch_value = fields.value?.value.as_ref().filter(|v| is_resolved(v));
Some(RootPatch {
object_hash,
path: path?,
value: patch_value?.clone(),
span: object_hash.span().cover(record.span),
})
}
fn resolve_patch_path(&mut self, value: Option<&Value>) -> Option<Spanned<PropertyPath>> {
match value? {
Value::String(text) => match PropertyPath::new(text.value.as_str()) {
Ok(path) => Some(Spanned::new(text.span, path)),
Err(error) => {
self.push(
D::InvalidPropertyPath {
span: text.span,
error,
}
.unwrap(),
);
None
}
},
value if !is_resolved(value) => None,
value => {
self.push(D::unwrap(
TypeMismatch {
span: value.span(),
expected: rito!(String).into(),
expected_span: None,
got: value.rito_type().into(),
}
.into(),
));
None
}
}
}
}
#[derive(Default)]
struct PatchFields<'a> {
path: Option<&'a Property>,
value: Option<&'a Property>,
}
impl<'a> PatchFields<'a> {
fn get(&self, field: PatchField) -> Option<&'a Property> {
match field {
PatchField::Path => self.path,
PatchField::Value => self.value,
}
}
fn slot(&mut self, field: PatchField) -> &mut Option<&'a Property> {
match field {
PatchField::Path => &mut self.path,
PatchField::Value => &mut self.value,
}
}
}
fn is_resolved(value: &Value) -> bool {
!matches!(value, Value::Unresolved { .. } | Value::Unknown(_))
}