use std::collections::BTreeSet;
use helm_schema_ast::{Literal, TemplateExpr};
use crate::abstract_value::{AbstractValue, path_is_encoded};
use crate::eval_effect::{Effects, EvalResult};
use crate::eval_env::EvalEnv;
use crate::expr_eval::{HelperCallValueResolver, eval_expr_with_helper_calls};
use super::strict_operands::{
record_range_key_string_consumer_effects, record_string_consumer_effects,
};
use super::value_facts::{
complete_string_set, identity_value_paths, serialization_payload_paths, value_paths,
value_strings,
};
use super::{eval_all_args, merge_arg_effects};
use helm_schema_ast::{
literal_printf_format, render_printf_scalar_values, render_printf_string_sets,
token_initial_printf_string_argument,
};
pub(super) fn eval_printf(
args: &[TemplateExpr],
env: &EvalEnv,
resolver: &mut impl HelperCallValueResolver,
) -> EvalResult {
let mut effects = Effects::default();
let mut provenance_paths = BTreeSet::new();
let mut widened_paths = BTreeSet::new();
let mut values = Vec::with_capacity(args.len());
let mut scalar_values = Vec::with_capacity(args.len());
let mut scalar_dispatches = Vec::with_capacity(args.len());
let token_initial_string_argument = token_initial_printf_string_argument(args);
for (index, arg) in args.iter().enumerate() {
let mut result = eval_expr_with_helper_calls(arg, env, resolver);
scalar_values.push(
result
.scalar_dispatch
.as_ref()
.and_then(crate::scalar_value::ScalarValueDispatch::constant_value),
);
scalar_dispatches.push(result.scalar_dispatch.clone());
let identity_paths = identity_value_paths(result.value.as_ref());
let plain_slot_format_paths = if token_initial_string_argument == Some(index) {
identity_paths
.iter()
.filter(|path| {
!result.effects.derived_text_paths.contains(*path)
&& !result
.effects
.local_output_meta
.get(*path)
.is_some_and(|meta| {
meta.shape_erased || meta.derived_text || meta.partial_text
})
})
.cloned()
.collect()
} else {
BTreeSet::new()
};
conjoin_formatter_operand_selection(
&plain_slot_format_paths,
result.scalar_dispatch.as_ref(),
&mut result.effects,
);
widened_paths.extend(
value_paths(result.value.as_ref())
.difference(&identity_paths)
.cloned(),
);
effects.merge(result.effects);
for path in &plain_slot_format_paths {
effects
.local_output_meta
.entry(path.clone())
.or_default()
.plain_slot_string_format = true;
}
effects
.plain_slot_string_format_paths
.extend(plain_slot_format_paths);
record_printf_argument_effects(
index == 0,
result.value.as_ref(),
&identity_paths,
&mut effects,
);
provenance_paths.extend(identity_paths);
values.push(result.value);
}
let rendered = literal_printf_format(args).and_then(|format| {
let exact_scalars = scalar_values
.iter()
.skip(1)
.cloned()
.collect::<Option<Vec<_>>>();
exact_scalars
.as_deref()
.and_then(|scalars| render_printf_scalar_values(format, scalars))
.map(|rendered| BTreeSet::from([rendered]))
.or_else(|| {
let arg_strings = values
.iter()
.skip(1)
.map(|value| complete_string_set(value.as_ref()))
.collect::<Option<Vec<_>>>()?;
render_printf_string_sets(format, &arg_strings)
})
});
let scalar_dispatch = printf_scalar_dispatch(args, rendered.as_ref(), &scalar_dispatches);
let mut values = Vec::new();
if let Some(rendered) = rendered {
values.push(AbstractValue::StringSet(rendered));
}
if let Some(paths) = AbstractValue::path_choices(provenance_paths) {
values.push(paths);
}
if let Some(widened) = AbstractValue::widened(widened_paths) {
values.push(widened);
}
let result = EvalResult::with_effects(AbstractValue::choice(values), effects);
match scalar_dispatch {
Some(dispatch) => result.with_scalar_dispatch(dispatch),
None => result,
}
}
fn printf_scalar_dispatch(
args: &[TemplateExpr],
rendered: Option<&BTreeSet<String>>,
scalar_dispatches: &[Option<crate::scalar_value::ScalarValueDispatch>],
) -> Option<crate::scalar_value::ScalarValueDispatch> {
if let Some(rendered) = rendered {
let mut values = rendered.iter();
if let (Some(value), None) = (values.next(), values.next()) {
return Some(crate::scalar_value::ScalarValueDispatch::constant(
helm_schema_core::GuardValue::string(value.clone()),
));
}
}
let format = literal_printf_format(args)?;
let [_, Some(subject)] = scalar_dispatches else {
return None;
};
subject.printf_string(format)
}
pub(super) fn conjoin_formatter_operand_selection(
paths: &BTreeSet<String>,
dispatch: Option<&crate::scalar_value::ScalarValueDispatch>,
effects: &mut Effects,
) {
let Some(dispatch) = dispatch else {
return;
};
for path in paths {
let Some(conditions) = dispatch.identity_selection_conditions(path) else {
continue;
};
if conditions
.iter()
.any(|condition| matches!(condition, helm_schema_core::Predicate::True))
{
continue;
}
let meta = effects.local_output_meta.entry(path.clone()).or_default();
let existing = if meta.predicates.is_empty() {
BTreeSet::from([BTreeSet::new()])
} else {
std::mem::take(&mut meta.predicates)
};
meta.predicates = existing
.into_iter()
.flat_map(|branch| {
conditions.iter().map(move |condition| {
let mut selected = branch.clone();
selected.insert(condition.clone());
selected
})
})
.collect();
}
}
pub(super) fn record_total_conversion_effects(paths: BTreeSet<String>, effects: &mut Effects) {
effects.clear_plain_slot_string_format_paths(&paths);
effects.add_shape_erased_paths(paths.clone());
effects.derived_text_paths.extend(paths);
}
pub(super) fn record_printf_argument_effects(
is_format: bool,
value: Option<&AbstractValue>,
identity_paths: &BTreeSet<String>,
effects: &mut Effects,
) {
if is_format {
let raw: BTreeSet<String> = identity_paths
.iter()
.filter(|path| !effects.derived_text_paths.contains(*path))
.cloned()
.collect();
record_string_consumer_effects(value, &raw, effects);
} else {
effects.add_shape_erased_paths(identity_paths.clone());
}
effects
.derived_text_paths
.extend(identity_paths.iter().cloned());
}
pub(super) fn eval_print(
args: &[TemplateExpr],
env: &EvalEnv,
resolver: &mut impl HelperCallValueResolver,
) -> EvalResult {
let mut effects = Effects::default();
let mut rendered: BTreeSet<String> = [String::new()].into_iter().collect();
for arg in args {
let result = eval_expr_with_helper_calls(arg, env, resolver);
effects.merge(result.effects);
let Some(strings) = complete_string_set(result.value.as_ref()) else {
return EvalResult::with_effects(None, effects);
};
let mut next = BTreeSet::new();
for prefix in &rendered {
for value in &strings {
next.insert(format!("{prefix}{value}"));
}
}
rendered = next;
}
EvalResult::with_effects(Some(AbstractValue::StringSet(rendered)), effects)
}
pub(super) fn eval_replace(
args: &[TemplateExpr],
piped: Option<EvalResult>,
env: &EvalEnv,
resolver: &mut impl HelperCallValueResolver,
) -> EvalResult {
let (old, new, mut subject, piped_for_facts) = match (piped, args) {
(None, [old, new, subject]) => (
eval_expr_with_helper_calls(old, env, resolver),
eval_expr_with_helper_calls(new, env, resolver),
eval_expr_with_helper_calls(subject, env, resolver),
None,
),
(Some(subject), [old, new]) => {
let piped_for_facts = subject.clone();
(
eval_expr_with_helper_calls(old, env, resolver),
eval_expr_with_helper_calls(new, env, resolver),
subject,
Some(piped_for_facts),
)
}
(None, _) => return eval_all_args(args, env, resolver),
(Some(mut subject), _) => {
merge_arg_effects(args, env, resolver, &mut subject.effects);
return subject;
}
};
let subject_effects = subject.effects.clone();
subject.effects.merge(old.effects);
subject.effects.merge(new.effects);
let mut effects = subject.effects;
let old_values = value_strings(old.value.as_ref());
let new_values = value_strings(new.value.as_ref());
let (string_paths, raw_range_key_paths) =
super::strict_operands::string_invocation_operand_facts(
"replace",
args,
piped_for_facts.as_ref(),
env,
resolver,
);
if let Some(old_token) = single_replace_token(&old_values)
&& let Some(value) = subject.value.as_ref().and_then(|value| {
super::value_facts::replace_transformed_value(
value,
&subject_effects,
old_token,
&new_values,
)
})
{
super::strict_operands::record_string_consumer_effects(
subject.value.as_ref(),
&string_paths,
&mut effects,
);
super::strict_operands::record_raw_range_key_string_consumer_paths(
&raw_range_key_paths,
&mut effects,
);
return EvalResult::with_effects(Some(value), effects);
}
let subject_values = value_strings(subject.value.as_ref());
let value = if old_values.is_empty() || new_values.is_empty() || subject_values.is_empty() {
super::value_facts::derive_value_text(subject.value)
} else {
let mut rendered = BTreeSet::new();
for subject in subject_values {
for old in &old_values {
for new in &new_values {
rendered.insert(subject.replace(old, new));
}
}
}
Some(AbstractValue::StringSet(rendered))
};
if replace_preserves_plain_token_language(&old_values, &new_values) {
effects
.plain_text_range_key_paths
.extend(raw_range_key_paths.iter().cloned());
}
super::strict_operands::record_string_transform_effects(
"replace",
value.as_ref(),
&string_paths,
&raw_range_key_paths,
&mut effects,
);
EvalResult::with_effects(value, effects)
}
fn replace_preserves_plain_token_language(
old_values: &BTreeSet<String>,
new_values: &BTreeSet<String>,
) -> bool {
let irrelevant = |text: &String| {
!text.is_empty()
&& !text.chars().any(|character| {
matches!(
character,
':' | '#'
| '\r'
| '\n'
| ' '
| '\t'
| '!'
| '&'
| '*'
| '{'
| '}'
| '['
| ']'
| ','
| '|'
| '>'
| '@'
| '`'
| '%'
| '-'
| '?'
| '\''
| '"'
)
})
};
!old_values.is_empty()
&& !new_values.is_empty()
&& old_values.iter().all(irrelevant)
&& new_values.iter().all(irrelevant)
}
fn single_replace_token(old_values: &BTreeSet<String>) -> Option<&String> {
let mut old_values = old_values.iter();
let (Some(token), None) = (old_values.next(), old_values.next()) else {
return None;
};
(!token.is_empty()).then_some(token)
}
pub(super) fn eval_repeat(
args: &[TemplateExpr],
env: &EvalEnv,
resolver: &mut impl HelperCallValueResolver,
) -> EvalResult {
let [count, subject] = args else {
return eval_all_args(args, env, resolver);
};
let count = eval_expr_with_helper_calls(count, env, resolver);
let mut subject = eval_expr_with_helper_calls(subject, env, resolver);
subject.effects.merge(count.effects);
let count = count
.value
.as_ref()
.and_then(super::value_facts::concrete_integer);
let subject_values = value_strings(subject.value.as_ref());
let Some(count) = count.filter(|count| (0..=4096).contains(count)) else {
return subject;
};
let Ok(count) = usize::try_from(count) else {
return subject;
};
if subject_values.is_empty() {
return subject;
}
let rendered = subject_values
.into_iter()
.map(|value| value.repeat(count))
.collect();
EvalResult::with_effects(Some(AbstractValue::StringSet(rendered)), subject.effects)
}
pub(super) fn eval_tpl(
args: &[TemplateExpr],
env: &EvalEnv,
resolver: &mut impl HelperCallValueResolver,
) -> EvalResult {
let [template_expr, context_expr] = args else {
return eval_all_args(args, env, resolver);
};
let template = eval_expr_with_helper_calls(template_expr, env, resolver);
let mut effects = template.effects;
let _context = eval_expr_with_helper_calls(context_expr, env, resolver);
let value = if expression_applies_to_yaml(template_expr) {
effects
.templated_yaml_paths
.extend(serialization_payload_paths(template.value.as_ref()));
template.value
} else {
let subject_paths = identity_value_paths(template.value.as_ref());
record_string_consumer_effects(template.value.as_ref(), &subject_paths, &mut effects);
if let Some(path) = template.value.as_ref().and_then(|value| match value {
AbstractValue::ValuesPath(path) => Some(path),
AbstractValue::OutputPath(path, meta) if meta.stringified => Some(path),
_ => None,
}) {
effects.templated_text_identity_paths.insert(path.clone());
}
record_range_key_string_consumer_effects(template.value.as_ref(), &mut effects);
effects.derived_text_paths.extend(subject_paths);
template.value
}
.and_then(rendered_content_value);
EvalResult::with_effects(value, effects)
}
pub(super) fn expression_applies_to_yaml(expr: &TemplateExpr) -> bool {
match expr.deparen() {
TemplateExpr::Call { function, .. } => function == "toYaml",
TemplateExpr::Pipeline(stages) => stages.iter().any(|stage| {
matches!(stage.deparen(), TemplateExpr::Call { function, .. } if function == "toYaml")
}),
_ => false,
}
}
pub(super) fn eval_from_yaml(
args: &[TemplateExpr],
piped: Option<EvalResult>,
env: &EvalEnv,
resolver: &mut impl HelperCallValueResolver,
) -> EvalResult {
if let Some(piped) = piped {
let mut result = eval_from_yaml_result(piped);
merge_arg_effects(args, env, resolver, &mut result.effects);
return result;
}
let Some(arg) = args.first() else {
return eval_all_args(args, env, resolver);
};
eval_from_yaml_result(eval_expr_with_helper_calls(arg, env, resolver))
}
pub(super) fn eval_to_yaml(
args: &[TemplateExpr],
env: &EvalEnv,
resolver: &mut impl HelperCallValueResolver,
) -> EvalResult {
let Some(arg) = args.first() else {
return eval_all_args(args, env, resolver);
};
eval_to_yaml_result(eval_expr_with_helper_calls(arg, env, resolver))
}
pub(super) fn eval_to_yaml_result(result: EvalResult) -> EvalResult {
let paths = serialization_payload_paths(result.value.as_ref());
let structurally_rendered = result
.value
.as_ref()
.is_some_and(is_structurally_rendered_yaml_value);
let mut effects = result.effects;
if !structurally_rendered {
effects.yaml_serialized_paths.extend(paths.iter().cloned());
}
effects.derived_text_paths.extend(paths);
EvalResult::with_effects(result.value, effects)
}
pub(super) fn eval_from_json(
args: &[TemplateExpr],
piped: Option<EvalResult>,
env: &EvalEnv,
resolver: &mut impl HelperCallValueResolver,
) -> EvalResult {
if let Some(piped) = piped {
let mut result = eval_from_json_result(piped);
merge_arg_effects(args, env, resolver, &mut result.effects);
return result;
}
let Some(arg) = args.first() else {
return eval_all_args(args, env, resolver);
};
eval_from_json_result(eval_expr_with_helper_calls(arg, env, resolver))
}
pub(super) fn eval_to_json(
args: &[TemplateExpr],
env: &EvalEnv,
resolver: &mut impl HelperCallValueResolver,
) -> EvalResult {
let Some(arg) = args.first() else {
return eval_all_args(args, env, resolver);
};
eval_to_json_result(eval_expr_with_helper_calls(arg, env, resolver))
}
pub(super) fn eval_to_json_result(result: EvalResult) -> EvalResult {
let payload_truth = result.truth.clone();
let paths = serialization_payload_paths(result.value.as_ref());
let mut effects = result.effects;
effects.json_serialized_paths.extend(paths.iter().cloned());
effects.derived_text_paths.extend(paths);
let mut serialized = EvalResult::with_effects(result.value, effects);
serialized.json_payload_truth = payload_truth;
serialized
}
pub(super) fn eval_from_json_result(result: EvalResult) -> EvalResult {
let payload_truth = result.json_payload_truth.clone();
if let Some(folded) = literal_decoded_value(result.value.as_ref(), DecodeFormat::Json) {
return EvalResult::with_effects(Some(folded), result.effects);
}
let paths = serialization_payload_paths(result.value.as_ref());
let round_trips_json = result
.value
.as_ref()
.is_some_and(AbstractValue::is_definitely_json_serialized)
|| !paths.is_empty()
&& paths
.iter()
.all(|path| path_is_encoded(path, &result.effects.json_serialized_paths));
let mut effects = result.effects;
let value = if round_trips_json {
result
.value
.as_ref()
.and_then(AbstractValue::json_roundtrip_identity)
} else {
record_string_consumer_effects(result.value.as_ref(), &paths, &mut effects);
None
};
let mut decoded = EvalResult::with_effects(value, effects);
decoded.set_truth_condition(
payload_truth,
crate::eval_effect::SelectionTruthSource::RawInput,
);
decoded
}
#[derive(Clone, Copy)]
enum DecodeFormat {
Yaml,
Json,
}
fn literal_decoded_value(
value: Option<&AbstractValue>,
format: DecodeFormat,
) -> Option<AbstractValue> {
let AbstractValue::StringSet(strings) = value? else {
return None;
};
let mut strings = strings.iter();
let (Some(text), None) = (strings.next(), strings.next()) else {
return None;
};
match format {
DecodeFormat::Yaml => {
abstract_value_from_yaml(&serde_yaml::from_str::<serde_yaml::Value>(text).ok()?)
}
DecodeFormat::Json => {
abstract_value_from_json(&serde_json::from_str::<serde_json::Value>(text).ok()?)
}
}
}
fn abstract_value_from_yaml(node: &serde_yaml::Value) -> Option<AbstractValue> {
match node {
serde_yaml::Value::Mapping(entries) => {
let mut members = std::collections::BTreeMap::new();
for (key, value) in entries {
let serde_yaml::Value::String(key) = key else {
return None;
};
members.insert(key.clone(), abstract_value_from_yaml(value)?);
}
Some(AbstractValue::Dict(members))
}
serde_yaml::Value::Sequence(items) => Some(AbstractValue::List(
items
.iter()
.map(abstract_value_from_yaml)
.collect::<Option<Vec<_>>>()?,
)),
serde_yaml::Value::String(text) => Some(AbstractValue::StringSet(
[text.clone()].into_iter().collect(),
)),
serde_yaml::Value::Bool(_) | serde_yaml::Value::Number(_) | serde_yaml::Value::Null => {
Some(AbstractValue::Unknown)
}
serde_yaml::Value::Tagged(_) => None,
}
}
fn abstract_value_from_json(node: &serde_json::Value) -> Option<AbstractValue> {
match node {
serde_json::Value::Object(entries) => {
let mut members = std::collections::BTreeMap::new();
for (key, value) in entries {
members.insert(key.clone(), abstract_value_from_json(value)?);
}
Some(AbstractValue::Dict(members))
}
serde_json::Value::Array(items) => Some(AbstractValue::List(
items
.iter()
.map(abstract_value_from_json)
.collect::<Option<Vec<_>>>()?,
)),
serde_json::Value::String(text) => Some(AbstractValue::StringSet(
[text.clone()].into_iter().collect(),
)),
serde_json::Value::Bool(_) | serde_json::Value::Number(_) | serde_json::Value::Null => {
Some(AbstractValue::Unknown)
}
}
}
pub(super) fn eval_from_yaml_result(result: EvalResult) -> EvalResult {
if let Some(folded) = literal_decoded_value(result.value.as_ref(), DecodeFormat::Yaml) {
return EvalResult::with_effects(Some(folded), result.effects);
}
let paths = serialization_payload_paths(result.value.as_ref());
let structurally_rendered_yaml = result
.value
.as_ref()
.is_some_and(is_structurally_rendered_yaml_value)
&& !paths.is_empty()
&& paths
.iter()
.all(|path| result.effects.derived_text_paths.contains(path));
let output_meta = result
.value
.as_ref()
.map(AbstractValue::output_meta)
.unwrap_or_default();
let rendered_yaml_output = !paths.is_empty()
&& paths.iter().all(|path| {
output_meta
.get(path)
.is_some_and(|meta| meta.yaml_serialized)
});
let round_trips_yaml = structurally_rendered_yaml
|| !paths.is_empty()
&& paths
.iter()
.all(|path| path_is_encoded(path, &result.effects.yaml_serialized_paths));
let mut effects = result.effects;
let string_input_paths = if round_trips_yaml {
BTreeSet::new()
} else {
paths
.iter()
.filter(|path| !path_is_encoded(path, &effects.yaml_serialized_paths))
.cloned()
.collect::<BTreeSet<_>>()
};
record_string_consumer_effects(result.value.as_ref(), &string_input_paths, &mut effects);
effects.parsed_yaml_input_paths.extend(string_input_paths);
let value = if round_trips_yaml {
result.value
} else if rendered_yaml_output {
Some(AbstractValue::Unknown)
} else {
AbstractValue::widened(paths)
};
EvalResult::with_effects(value, effects)
}
fn is_structurally_rendered_yaml_value(value: &AbstractValue) -> bool {
match value {
AbstractValue::Dict(_) | AbstractValue::List(_) | AbstractValue::Overlay { .. } => true,
AbstractValue::Choice(choices) => {
!choices.is_empty() && choices.iter().all(is_structurally_rendered_yaml_value)
}
AbstractValue::FirstTruthy(candidates) => {
!candidates.is_empty() && candidates.iter().all(is_structurally_rendered_yaml_value)
}
_ => false,
}
}
pub(super) fn eval_join(
args: &[TemplateExpr],
piped: Option<EvalResult>,
env: &EvalEnv,
resolver: &mut impl HelperCallValueResolver,
) -> EvalResult {
if let Some(mut piped) = piped {
merge_arg_effects(args, env, resolver, &mut piped.effects);
erase_join_input_shape(&mut piped);
return piped;
}
let [separator, subject] = args else {
return eval_all_args(args, env, resolver);
};
let separator = eval_expr_with_helper_calls(separator, env, resolver);
let mut result = eval_expr_with_helper_calls(subject, env, resolver);
result.effects.merge(separator.effects);
erase_join_input_shape(&mut result);
result
}
pub(super) fn erase_join_input_shape(result: &mut EvalResult) {
let paths = identity_value_paths(result.value.as_ref());
result.effects.add_shape_erased_paths(paths.clone());
result.effects.derived_text_paths.extend(paths);
}
pub(super) fn eval_cat(
args: &[TemplateExpr],
env: &EvalEnv,
resolver: &mut impl HelperCallValueResolver,
) -> EvalResult {
let mut effects = Effects::default();
let mut values = Vec::new();
for arg in args {
let result = eval_expr_with_helper_calls(arg, env, resolver);
effects.merge(result.effects);
if let Some(value) = result.value.and_then(rendered_content_value) {
values.push(value);
}
}
EvalResult::with_effects(AbstractValue::choice(values), effects)
}
pub(super) fn rendered_content_value(value: AbstractValue) -> Option<AbstractValue> {
match value {
AbstractValue::StringSet(_)
| AbstractValue::Top
| AbstractValue::Unknown
| AbstractValue::RootContext => None,
AbstractValue::Choice(choices) => AbstractValue::choice(
choices
.into_iter()
.filter_map(rendered_content_value)
.collect(),
),
AbstractValue::FirstTruthy(candidates) => {
let mapped: Vec<Option<AbstractValue>> =
candidates.into_iter().map(rendered_content_value).collect();
let intact = mapped.iter().all(Option::is_some);
let kept: Vec<AbstractValue> = mapped.into_iter().flatten().collect();
if intact {
AbstractValue::first_truthy(kept)
} else {
AbstractValue::choice(kept)
}
}
other => Some(other),
}
}
pub(super) fn eval_trim_affix(
function: &str,
args: &[TemplateExpr],
piped: Option<EvalResult>,
env: &EvalEnv,
resolver: &mut impl HelperCallValueResolver,
) -> EvalResult {
let (affix, mut subject, piped_for_facts) = match (piped, args) {
(None, [affix, subject]) => (
eval_expr_with_helper_calls(affix, env, resolver),
eval_expr_with_helper_calls(subject, env, resolver),
None,
),
(Some(subject), [affix]) => {
let piped_for_facts = subject.clone();
(
eval_expr_with_helper_calls(affix, env, resolver),
subject,
Some(piped_for_facts),
)
}
(None, _) => return eval_all_args(args, env, resolver),
(Some(mut subject), _) => {
merge_arg_effects(args, env, resolver, &mut subject.effects);
return subject;
}
};
let subject_dispatch = subject.scalar_dispatch.clone();
let subject_effects = subject.effects.clone();
subject.effects.merge(affix.effects);
let mut effects = subject.effects;
let (string_paths, raw_range_key_paths) =
super::strict_operands::string_invocation_operand_facts(
function,
args,
piped_for_facts.as_ref(),
env,
resolver,
);
let affix_values = value_strings(affix.value.as_ref());
let token = single_replace_token(&affix_values);
let scalar_dispatch = token
.zip(subject_dispatch.as_ref())
.map(|(token, dispatch)| dispatch.trimmed(token, function == "trimPrefix"));
if let Some(token) = token
&& let Some(value) = subject.value.as_ref().and_then(|value| {
super::value_facts::trim_affix_transformed_value(
value,
&subject_effects,
token,
function == "trimPrefix",
)
})
{
super::strict_operands::record_string_consumer_effects(
subject.value.as_ref(),
&string_paths,
&mut effects,
);
super::strict_operands::record_raw_range_key_string_consumer_paths(
&raw_range_key_paths,
&mut effects,
);
let result = EvalResult::with_effects(Some(value), effects);
return match scalar_dispatch {
Some(dispatch) => result.with_scalar_dispatch(dispatch),
None => result,
};
}
let value = super::value_facts::derive_value_text(subject.value);
super::strict_operands::record_string_transform_effects(
function,
value.as_ref(),
&string_paths,
&raw_range_key_paths,
&mut effects,
);
let result = EvalResult::with_effects(value, effects);
match scalar_dispatch {
Some(dispatch) => result.with_scalar_dispatch(dispatch),
None => result,
}
}
pub(super) fn eval_regex_replace(
function: &str,
args: &[TemplateExpr],
env: &EvalEnv,
resolver: &mut impl HelperCallValueResolver,
) -> EvalResult {
let [pattern_expr, subject_expr, replacement_expr] = args else {
return eval_all_args(args, env, resolver);
};
let pattern = eval_expr_with_helper_calls(pattern_expr, env, resolver);
let mut subject = eval_expr_with_helper_calls(subject_expr, env, resolver);
let replacement = eval_expr_with_helper_calls(replacement_expr, env, resolver);
let subject_effects = subject.effects.clone();
subject.effects.merge(pattern.effects);
subject.effects.merge(replacement.effects);
let mut effects = subject.effects;
let (string_paths, raw_range_key_paths) =
super::strict_operands::string_invocation_operand_facts(
function, args, None, env, resolver,
);
let pattern_strings = value_strings(pattern.value.as_ref());
let escape = pattern_strings
.iter()
.next()
.filter(|_| pattern_strings.len() == 1)
.and_then(|pattern| {
let token = super::value_facts::regex_mandatory_literal(pattern)?;
let erases_to_empty = matches!(
replacement_expr.deparen(),
TemplateExpr::Literal(Literal::String(text) | Literal::RawString(text))
if text.is_empty()
);
if erases_to_empty && pattern.strip_prefix(token.as_str()) == Some(".*$") {
Some(crate::helper_meta::LexicalEscape::CutAtToken(token))
} else {
Some(crate::helper_meta::LexicalEscape::Contains(token))
}
});
if let Some(escape) = escape
&& let Some(value) = subject.value.as_ref().and_then(|value| {
super::value_facts::regex_replace_transformed_value(value, &subject_effects, &escape)
})
{
super::strict_operands::record_string_consumer_effects(
subject.value.as_ref(),
&string_paths,
&mut effects,
);
super::strict_operands::record_raw_range_key_string_consumer_paths(
&raw_range_key_paths,
&mut effects,
);
return EvalResult::with_effects(Some(value), effects);
}
let value = super::value_facts::derive_value_text(subject.value);
super::strict_operands::record_string_transform_effects(
function,
value.as_ref(),
&string_paths,
&raw_range_key_paths,
&mut effects,
);
EvalResult::with_effects(value, effects)
}