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 jinja_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 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.

JINJA_CONST_TEMPLATE_WARN_MUTEX = Mutex.new
LOOKUP_NONE_TERM = "\uE000lookup-none"

The null-file-term marker for the strings-only lookup-args wire (the rerender below and lookup_file in lookup_file.cr are the only hands it passes through): a private-use control character like param_sentinels.cr's own sentinels, so no real role's lookup term - a path, a glob, anything a playbook can write - can ever collide with the equality check in lookup_file.

MAX_CRINJA_DELEGATION_DEPTH = 20
MAX_REPEAT_ELEMENTS = 10000000

Python * repeat semantics: a String or Array operand repeated by an int-coercible one (Bool counts as its int-subclass value; a Float64 multiplier is a real TypeError, not a repeat). Returns nil when the pair isn't a repeat shape at all. Checked in BOTH orientations by the caller (str * int and int * str). A negative count repeats zero times (Python semantics); a count that would materialize more than MAX_REPEAT_ELEMENTS elements raises rather than exhausting memory the way real Python's MemoryError fails the task.

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) + [".", ",", ":", "-", "_"]

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_]*|\.[0-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 (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. Dotted NUMERIC parts (.0, Jinja's list-index shorthand) are plain references too: excluding them sent l.0.0 to the Jinja fallback, whose stringification loses the container's native shape (structured_container saw a JSON TEXT string, not a dict, so a mixed-text parent={{ l.0.0 }} rendered {"name":"s1"} where Ansible renders Python repr {'name': 's1'} - live-verified vs 2.19.11). VariableLookup's apply_dotted_parts owns the Array-vs-Hash-key decision.

Constructors

new(vars : Hash(String, JSON::Any), decode : Bool = false)
Source

Class methods

emit_jinja_const_template_warning

Public because the krikri-jinja engine's own lookup-terms re-render (krikri_jinja_lookups.cr's prepare_lookup_terms - the native-typed whole-span route set_fact values and debug msgs take) renders the same constants the same way and must warn the same way.

Source

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 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 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 JinjaRenderer#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
lookup_list_form=(lookup_list_form : Bool)

Set by #evaluate_lookup's dispatch when the result it returns kept its LIST form (ansible-core's ",".join raised TypeError and the sequence came back as-is) rather than comma-joining - the embedded-rendering decision in #evaluate_bare_lookup_call reads it straight after its own #evaluate_lookup call. Provenance from the dispatch, never from the returned text's shape: a lookup('file') whose content merely looks like a JSON array is a joined/str result and stays verbatim text (issue #695).

Source
lookup_list_form?

Set by #evaluate_lookup's dispatch when the result it returns kept its LIST form (ansible-core's ",".join raised TypeError and the sequence came back as-is) rather than comma-joining - the embedded-rendering decision in #evaluate_bare_lookup_call reads it straight after its own #evaluate_lookup call. Provenance from the dispatch, never from the returned text's shape: a lookup('file') whose content merely looks like a JSON array is a joined/str result and stays verbatim text (issue #695).

Source
lookup_undef_default

Set by the env lookup's dispatch when a missing env variable's default= option ITSELF evaluated to the "undefined" sentinel (the default=undef() shape): the lookup's real value is ansible's undefined marker, not the sentinel as an ordinary degraded result. The strict consumers raise with the plugin's own hint (see evaluate_bare_lookup_call); a lenient consumer (| default(...)) keeps consuming the sentinel, which the filter engine's undefined? already treats as a miss. Reset at every #evaluate_lookup entry, like #lookup_list_form.

Source
lookup_undef_default=(lookup_undef_default : String | Nil)

Set by the env lookup's dispatch when a missing env variable's default= option ITSELF evaluated to the "undefined" sentinel (the default=undef() shape): the lookup's real value is ansible's undefined marker, not the sentinel as an ordinary degraded result. The strict consumers raise with the plugin's own hint (see evaluate_bare_lookup_call); a lenient consumer (| default(...)) keeps consuming the sentinel, which the filter engine's undefined? already treats as a miss. Reset at every #evaluate_lookup entry, like #lookup_list_form.

Source