From bbe146ee22575ecd36343e2a7eeac83c244e9c2f Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Wed, 14 Jun 2023 18:21:32 +0000 Subject: [PATCH] feat: refer to subfields of objects in string formatting Upstream issue: https://github.com/google/jsonnet/pull/1011 --- --- a/crates/jrsonnet-evaluator/src/error.rs +++ b/crates/jrsonnet-evaluator/src/error.rs @@ -1,6 +1,6 @@ use std::{ fmt::{Debug, Display}, - path::PathBuf, + path::PathBuf, cmp::Ordering, }; use jrsonnet_gcmodule::Trace; @@ -9,9 +9,9 @@ use jrsonnet_types::ValType; use thiserror::Error; -use crate::{function::CallLocation, stdlib::format::FormatError, typed::TypeLocError}; +use crate::{function::CallLocation, stdlib::format::FormatError, typed::TypeLocError, ObjValue}; -fn format_found(list: &[IStr], what: &str) -> String { +pub(crate) fn format_found(list: &[IStr], what: &str) -> String { if list.is_empty() { return String::new(); } @@ -68,6 +68,26 @@ } } +pub(crate) fn suggest_object_fields(v: &ObjValue, key: IStr) -> Vec { + let mut heap = Vec::new(); + for field in v.fields_ex( + true, + #[cfg(feature = "exp-preserve-order")] + false, + ) { + let conf = strsim::jaro_winkler(field.as_str(), key.as_str()); + if conf < 0.8 { + continue; + } + if field.as_str() == key.as_str() { + panic!("looks like string pooling failure, please write any info regarding this crash to https://github.com/CertainLach/jrsonnet/issues/113, thanks!"); + } + heap.push((conf, field)); + } + heap.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(Ordering::Equal)); + heap.into_iter().map(|v| v.1).collect() +} + type FunctionSignature = Vec<(Option, bool)>; /// Possible errors @@ -348,6 +368,9 @@ ($w:ident$(::$i:ident)*$(($($tt:tt)*))?) => { return Err($w$(::$i)*$(($($tt)*))?.into()) }; + ($w:ident$(::$i:ident)*$({$($tt:tt)*})?) => { + return Err($w$(::$i)*$({$($tt)*})?.into()) + }; ($l:literal) => { return Err($crate::error::ErrorKind::RuntimeError($l.into()).into()) }; --- a/crates/jrsonnet-evaluator/src/evaluate/mod.rs +++ b/crates/jrsonnet-evaluator/src/evaluate/mod.rs @@ -12,7 +12,7 @@ use crate::{ arr::ArrValue, destructure::evaluate_dest, - error::ErrorKind::*, + error::{ErrorKind::*, suggest_object_fields}, evaluate::operator::{evaluate_add_op, evaluate_binary_op_special, evaluate_unary_op}, function::{CallLocation, FuncDesc, FuncVal}, throw, @@ -466,31 +466,10 @@ || format!("field <{key}> access"), || match v.get(key.clone().into_flat()) { Ok(Some(v)) => Ok(v), - #[cfg(not(feature = "friendly-errors"))] - Ok(None) => throw!(NoSuchField(key.clone(), vec![])), - #[cfg(feature = "friendly-errors")] Ok(None) => { - let mut heap = Vec::new(); - for field in v.fields_ex( - true, - #[cfg(feature = "exp-preserve-order")] - false, - ) { - let conf = strsim::jaro_winkler( - &field as &str, - &key.clone().into_flat() as &str, - ); - if conf < 0.8 { - continue; - } - heap.push((conf, field)); - } - heap.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(Ordering::Equal)); + let suggestions = suggest_object_fields(&v, key.clone().into_flat()); - throw!(NoSuchField( - key.clone().into_flat(), - heap.into_iter().map(|(_, v)| v).collect() - )) + throw!(NoSuchField(key.clone().into_flat(), suggestions)) } Err(e) => Err(e), }, --- a/crates/jrsonnet-evaluator/src/stdlib/format.rs +++ b/crates/jrsonnet-evaluator/src/stdlib/format.rs @@ -6,7 +6,12 @@ use jrsonnet_types::ValType; use thiserror::Error; -use crate::{error::ErrorKind::*, throw, typed::Typed, Error, ObjValue, Result, Val}; +use crate::{ + error::{format_found, suggest_object_fields, ErrorKind::*}, + throw, + typed::Typed, + Error, ObjValue, Result, Val, +}; #[derive(Debug, Clone, Error, Trace)] pub enum FormatError { @@ -24,6 +29,15 @@ MappingKeysRequired, #[error("no such format field: {0}")] NoSuchFormatField(IStr), + + #[error("expected subfield <{0}> to be an object, got {1} instead")] + SubfieldDidntYieldAnObject(IStr, ValType), + #[error("subfield not found: <[{full}]{current}>{}", format_found(.found, "subfield"))] + SubfieldNotFound { + current: IStr, + full: IStr, + found: Box>, + }, } impl From for Error { @@ -691,6 +705,37 @@ Ok(out) } +fn get_dotted_field(obj: ObjValue, field: &str) -> Result { + let mut current = Val::Obj(obj); + let mut name_offset = 0; + for component in field.split('.') { + let end_offset = name_offset + component.len(); + current = if let Val::Obj(obj) = current { + if let Some(value) = obj.get(component.into())? { + value + } else { + let current = &field[name_offset..end_offset]; + let full = &field[..name_offset]; + let found = Box::new(suggest_object_fields(&obj, current.into())); + throw!(SubfieldNotFound { + current: current.into(), + full: full.into(), + found, + }) + } + } else { + // No underflow may happen, initially we always start with an object + let subfield = &field[..name_offset - 1]; + throw!(SubfieldDidntYieldAnObject( + subfield.into(), + current.value_type() + )); + }; + name_offset = end_offset + 1; + } + Ok(current) +} + pub fn format_obj(str: &str, values: &ObjValue) -> Result { let codes = parse_codes(str)?; let mut out = String::new(); @@ -726,7 +771,7 @@ if let Some(v) = values.get(f.clone())? { v } else { - throw!(NoSuchFormatField(f)); + get_dotted_field(values.clone(), &f)? } }; -- gitstuff