class

Krikri::VariableSubstitutor::ExpressionEvaluator

Inherits Reference < Object

ExpressionEvaluator - Orchestrates evaluation of all expression types Delegates to specialized evaluators based on expression type

Constants

BOOLEAN_CONDITION_TOKENS = [" is ", " in ", " not ", "==", "!=", "<=", ">=", " and ", " or "]

Tokens whose presence on one side of a top-level or/and mean that side is a genuine boolean condition (comparison, is-test, negation, or another nested or/and) rather than a plain value - in that case the whole expression must keep going through ConditionalEvaluator's boolean coercion instead of #evaluate_ value_or_and's value-passthrough semantics.

INDIRECTION_ONLY_RE = /\A\{\{\s*[A-Za-z_]\w*\s*\}\}\z/

A bare identifier whose OWN stored raw value is a pure, single- level {{ other_var }} indirection (no filters, no dotted/ bracket access) - the exact shape KNOWN_MISSING.md's "native typing" gap documents as the one that actually diverges in practice (found live in robertdebock.java/buluma.java's own java_version == 8 gate). This engine's {{ }} substitution deliberately preserves the SOURCE type as a string through such an indirection rather than re-inferring a scalar type from rendered text (see crinja_renderer.cr's own rerender_string_ value comment on why - protecting buluma.bind's ( bind_python_version == '3') idiom, which needs the opposite behavior) - correct for real Ansible's OWN pre-2.19 templating model, but ansible-core 2.19 made native types the default, so a same-run X == <int> against exactly this indirected shape can take the wrong branch silently. Not chased as a general fix (measured at ~0.16% frequency across 611 real roles - see KNOWN_MISSING.md's own decision-rule writeup, which explicitly defers the full native-typing rewrite); this is the narrow one-off it names as the alternative.

MAX_CRINJA_DELEGATION_DEPTH = 20
MEMO_CACHE_MAX_ENTRIES = 10000

All three caches are keyed on raw expression TEXT, and an expression can legitimately vary per loop item (assert:'s that: "{{ item.a }} == 'x'" finalizes to different text per item) - unbounded growth over a long run. The memoized values are PURE functions of the text, so resetting is always safe; a reset only costs re-computation.

PASSWORD_CHARS = ((("a".."z").to_a + ("A".."Z").to_a) + ("0".."9").to_a) + [".", ",", ":", "-", "_"]

real Ansible's password lookup default charset (ascii_letters + digits + ".,:-_", its own DEFAULT_PASSWORD_CHARS) and default length (20).

PASSWORD_LENGTH = 20
REGEX_PLAIN_REFERENCE = /\A[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*|\[(?:-?\d+|'[^']*'|"[^"]*")\])*\z/

Evaluate any expression and return string result. A thin guard in front of #evaluate_expr for the inline ternary TRUTHY if COND else FALSY (real Jinja2/Ansible syntax, used directly in default vars like konstruktoid-hardening's sysctl_conf_dir: "{{ '/usr/lib/sysctl.d' if usr_lib_sysctl_d_dir else '/etc/sysctl.d' }}") - split out from the main body (rather than added as another branch in it) purely to keep that method's already-high cyclomatic complexity from tipping over ameba's threshold. Checked before any of #evaluate_expr's own checks since COND itself commonly contains a comparison - splitting first keeps that comparison scoped to COND instead of being (wrongly) evaluated against the whole expression. A plain variable reference: name, dotted path, bracket index. No filters, operators, calls or literals - matching REGEX_BARE_VAR_REF's spirit in variable_substitutor.cr.

Constructors

Instance methods

evaluate(expr : String) : String
Source
evaluate_or_undefined(expr : String) : String | Undefined

The undefined-typed form of #evaluate: Undefined::INSTANCE when the expression genuinely does not resolve to any value, the rendered String otherwise - which may itself be the literal text "undefined" when a real stored value collides with the sentinel.

This is the seam the "undefined"-string sentinel architecture could not cross (KNOWN_MISSING.md's dotted-index collision entry): #evaluate's String return type cannot distinguish a genuine miss from a real value that happens to BE the text "undefined", so any caller that re-checks its own output (rendered == "undefined") misreads the collision as a miss and, under strict-undefined, fails a task real Ansible runs. The disambiguation here never compares strings: a rendered "undefined" is demoted to Undefined only when the undefined-typed structural resolver (VariableLookup#resolve - nil on a miss, JSON::Any otherwise) ALSO finds no value. For the plain dotted/bracket chain shapes its strict-undefined and dynamic-dict-key callers feed it, that resolver is complete (quoted/integer/bare/bracket index keys, numeric dot-indexing into lists, dynamic keys via recursion, recursive re-templating of templated bases). Shapes resolve can't parse degrade to the old behavior rather than to a regression: they only reach the ambiguous branch when the render was already the sentinel text.

Source
evaluate_output(expr : String) : String

#evaluate, but formatting a CONTAINER result the way real Ansible renders one into final text - Python's repr (['a', 'b']), not this codebase's internal JSON-compact form (["a","b"]).

Deliberately narrow: only a plain variable reference (bare, dotted or indexed) is re-resolved structurally here, because those are the shapes whose value is available WITHOUT re-running the evaluation, and {{ some_list }} is where this difference actually shows up. A filter chain still renders through #evaluate's JSON form - see KNOWN_MISSING.md; closing that needs the evaluator to carry structured results out to the final boundary, which is the round trip CrinjaRenderer#evaluate_value! warns about.

Only VarSubstitutor's outermost {{ }} expansion may call this.

Source
evaluate_structured(expr : String) : JSON::Any | Nil

Structured (raw JSON::Any) evaluation of a full expression, for callers outside this class - Krikri.bracket_index_failure_message's strict-probe use (round 812045, pluggero.bibata_cursor), which must see a JSON-null result as a real None value, not as the "undefined"-sentinel string the String-returning #evaluate collapses it to. Deliberately does NOT rescue: the caller decides what a Crinja failure means on its own path.

Source