Preprocess Jinja YAML before validation - #89
Conversation
Add a small sandboxed renderer with local includes, explicit rendering diagnostics, and generated-output locations. Keep automatic fixes away from template source. Closes #61
Report EG106 at the template include site and require explicit no-dayc suppression to accept missing dependencies. Blank affected documents, retain rendered locations, and test partial validation and suppression behavior.
Report missing includes as WG106 while preserving errors for other dependency failures unless explicitly suppressed. Test both default exit statuses and no-dayc overrides.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Rendering is not resource-bounded, and directive matching and nested source-location handling remain unresolved.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 1
Open (4)
What changed in this PR
This pull request adds opt-in Jinja preprocessing before YAML validation, including local includes, partial validation, diagnostics, and fixer bypasses.
Changes:
- Added sandboxed Jinja rendering and missing-include handling.
- Integrated validation, suppressions, and fixer behavior.
- Added tests, fixtures, documentation, and dependency metadata.
| File | Summary | Review notes |
|---|---|---|
tests/test_jinja.py |
Jinja behavior coverage | No final comments. |
tests/fixtures/jinja/interview.yml |
Jinja fixture | No final comments. |
tests/fixtures/jinja/fields.yml |
Include fixture | No final comments. |
src/dayamlchecker/yaml_structure.py |
Rendering integration and diagnostics | Two moderate findings (2 votes each): match the complete directive line and preserve nested-template source locations for runtime errors. |
src/dayamlchecker/messages.py |
Jinja findings | No final comments. |
src/dayamlchecker/fixer.py |
Skips Jinja templates | Moderate finding (2 votes): match the complete directive line to avoid skipping ordinary YAML files. |
src/dayamlchecker/_jinja.py |
Jinja renderer and include handling | Critical finding (2 votes): bound rendering execution time and memory usage. |
requirements.txt |
Adds Jinja2 dependency | No final comments. |
README.md |
Documents preprocessing | No final comments. |
pyproject.toml |
Adds Jinja2 dependency | No final comments. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Run compilation and rendering in an isolated worker with wall, CPU, memory, and output limits. Share an exact first-line directive matcher between validation and fixing, and carry rewritten Jinja traceback locations across the worker boundary for source suppressions.
|
@jpagh if you have time, would love your eyes on this/run it against your interviews that use Jinja2. I don't know others who use this feature |
|
When that bug is fixed, this is the output: It doesn't seem to like that I refer to a config variable via jinja (https://docassemble.org/docs/config.html#jinja%20data). If it's helpful, you/agent can look at my docassemble-yaml/lsp to see how I handle the jinja. Jinja rendering broken on macOS:
|
|
I did look at your implementation but wanted to focus on preprocessing, so it's a pretty different approach. Do you have any sample Jinja2 YAMLs you can share for validation? |
Static checking has no access to `jinja data`, docassemble's built-in Jinja context, or server configuration, so StrictUndefined rejected interviews that are valid on a real server. Substitute an undefined type that renders as empty and supports chained lookups and calls, while still failing closed on sandbox violations. Also skip RLIMIT_AS on macOS, where Darwin rejects an address-space limit below the process's existing virtual size; the CPU, response-size, and parent wall-time bounds still apply there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
x_documents_anonymized.yml Here are two that show what I use jinja for. Document loads options from the server config, and Review uses jinja macros to make repeatable questions for generics/list items. Sorry, I didn't look at your implementation before I said anything. |
Three fixes from review of the Jinja preprocessor:
Give the Jinja rendering error its own code, EG106. It shared EG105 with
the unrelated test-module pre-load check, and suppression matches on the
code string alone, so `# no-dayc-block: EG105` for a known Jinja
dependency error also silenced that check. The Mako pairs that share
EG111/EG112 are the same check in two contexts and stay as they are.
Let offline server values survive arithmetic and comparison. Undefined
routes those operators to _fail_with_undefined_error, so
`{% if jinja_data.limit > 0 %}` still aborted the file even though
unknown values are meant to read as empty -- a count or threshold from
`jinja data` is one of the main reasons to use it. Sandbox violations
still fail closed through every added operator.
Carry the rendered-Jinja marker on the finding instead of appending
"(rendered Jinja)" to its filename. GitHub could not resolve the
synthetic path and dropped every inline annotation for these files.
The path is now real; because generated line numbers are not lines of
that file, annotations cover the file as a whole and report the
generated line in the message. Text output is unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MakoTemplate raises RuntimeException rather than a reportable error when
handed a non-string, so `question:` with no text ended the whole run with
an unhandled exception instead of producing findings. Skip compilation
for a non-string, matching _malformed_markdown_link_errors just above it;
nothing reads the template attribute.
This predates the Jinja preprocessor, but rendering a server-supplied
value to an empty one makes it easy to reach: `question: {{ config.title }}`
is a valid interview that rendered to a null value.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Audited against the Jinja source rather than one failing file at a time.
UndefinedError is raised only through Undefined._fail_with_undefined_error,
so its operator table is the whole surface for missing values, and the
previous commit covered all of it that Python 3 can call. Three further
paths reach an offline value without going through that table:
- Python protocols the base class never implements: abs(), round(),
__index__ (range(), lipsum()) and __format__ ("{:>5}".format()).
- `| tojson`, which json cannot encode; serialized through a policy
default rather than a new type.
- The implicit else of an inline if. The compiler deliberately binds
`cond_expr_undefined = Undefined`, bypassing the environment's type,
so `{{ "a" if server_value }}` still failed. Rebound in write_commons,
which covers root, block and macro frames.
All 54 built-in filters and all built-in tests now accept an offline
value, as do 39 language constructs; both sweeps are kept as tests, along
with a guard that fails if a future Jinja adds an operator we do not
handle. Sandbox violations still fail closed through every added path.
Known residual: `x is in "a string"` still fails, because CPython requires
a str left operand for string containment.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An expression that depended on `jinja data` rendered to nothing, which left an empty YAML value, an invalid Python line, or a block with no id, and every real problem around it went unreported. Substitute a parseable placeholder instead so the surrounding document is still checked, and report each substitution as WG107 against the original template line. Measured across the checks that touch a substituted value, this turns five false positives into none, and introduces none: EG420 on a field label, EG122 in a code block, EG103/EG414 on a block id, and EG132 on `mandatory` all came from the empty rendering. Each placeholder is distinct, so two unknown block ids no longer look like duplicates of each other -- the case where a later block depends on a value an earlier expression produced. Where that is not enough, WG107 suppresses from the template source like WG106, through the existing _apply_jinja_suppressions path. The line number comes from wrapping output nodes in the code generator, the same rewrite already used for includes. Checks that resolve a name against the rest of the interview treat a placeholder as unresolvable rather than reporting our own substitution as an undefined variable. A sandbox violation is still an error, never a placeholder. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@jpagh when you have a chance, one more check? the latest version should be safe for the kind of file you described, and I added your examples as new test fixtures. |
|
Looks good to me. It worked on both of my Jinja files and reported accurate errors (missing |


Files whose first line is exactly
# use jinjanow render before the existing YAML validation pass instead of being skipped. Validation and fixing share the directive matcher (LF, CRLF, or EOF). The sandboxed renderer supports expressions, loops, macros, and local includes, including partial blocks.Compilation and rendering run in an isolated worker with a 5-second wall timeout, 2-second CPU limit, and 4 MiB source/output limits. A 256 MiB address-space limit applies on Linux and other supported Unix platforms; macOS skips it because Darwin rejects a limit below the process's existing virtual address space, and the CPU, response-size, and parent wall-time bounds still apply there. A bounded worker response protects parent memory. Limit failures produce EG106. Unix resource-limit support is required for Jinja rendering; unsupported platforms report an error instead of rendering without limits. Ordinary YAML validation is unaffected.
Missing or unavailable package-qualified
{% include %}files are skipped by default with warning WG106. A placeholder identifies affected YAML documents, which are blanked while the remaining documents are checked. The warning explains that findings are partial because missing includes may supply document boundaries or definitions. Warnings do not fail CI unless a warning limit is set. Include fallback lists try every candidate, and repeated execution of one include site is reported once.Values the server supplies
This is an offline check, so
jinja data, docassemble's context variables, and server configuration are unavailable. Rather than failing a file that is valid on a real server, unknown values read as empty throughout: in arithmetic and comparisons, through every built-in filter and test, and through the Python protocols (abs(),round(),range(),format(),tojson) and the implicit else of an inlineif, which reach an undefined value without passing through Jinja's own operator table. A branch that tests such a value is taken as if it were empty, and only that branch is checked.An expression that renders to one of these values is replaced with a distinct placeholder such as
dayc_unknown_1and reported as warning WG107 against the original template line. Rendering it to nothing instead would leave an empty YAML value, an invalid Python line, or a block with noid, which hid every real problem around it: across the checks that touch a substituted value, the placeholder removes five false positives (EG420 on a field label, EG122 in a code block, EG103/EG414 on a block id, EG132 onmandatory) and introduces none. Distinct placeholders keep two unknown block ids from looking like duplicates of each other. Checks that resolve a name against the rest of the interview treat a placeholder as unresolvable rather than reporting the substitution as an undefined variable. Nothing that depends on the real value is checked, so WG107 suppresses from the template source like WG106, with# no-dayc: WG107or# no-dayc-block: WG107.Sandbox violations are never turned into a placeholder, and remain errors.
Errors
Missing imports, parent templates, and rendering failures produce EG106. This code was EG105 in earlier revisions of this branch, which collided with the pre-existing test-module pre-load check; suppression matches on the code string alone, so
# no-dayc-block: EG105for a Jinja dependency also silenced that unrelated check. Anyone who adopted EG105 from an earlier revision of this branch needs to update those suppressions to EG106. Validation errors in the remaining YAML retain their normal severity. Jinja syntax and runtime diagnostics preserve original source locations, including nested local templates, across the worker boundary.Findings from rendered output
Findings produced from rendered YAML name the original file and carry a
rendered_jinjamarker rather than a syntheticpath (rendered Jinja)filename, which GitHub could not resolve — it dropped every inline annotation for these files. Text output is unchanged;--format githubannotates the file as a whole and reports the generated line in the message, since generated line numbers are not lines of the source file. Automatic fixes skip templates. Server context, package-qualified file resolution, and template-aware formatting remain outside this offline implementation.Also fixed
MakoTemplateraises rather than reporting when handed a non-string, soquestion:with no text ended a run with an unhandled exception instead of findings. This predates the branch, but rendering a server value to an empty one made it easy to reach.Validation
499 tests and 56 subtests pass; mypy, Black, and
git diff --checkpass on src and tests. Regression coverage includes oversized output, large allocations, worker timeout/cleanup, exact directives, nested runtime locations and suppressions, CI exit behavior, partial blocks, and include fallback lists. Offline-value coverage is verified against the installed Jinja rather than by example: a guard test enumerates every attribute Jinja routes to an undefined error and fails if a future release adds one that is not handled, and sweeps exercise all 54 built-in filters, every built-in test, and 39 language constructs. Fixtures include an example adapted from docassemble's documented include pattern.Known residual:
x is in "a string"still fails, because CPython requires astrleft operand for string containment.Closes #61.
🤖 Generated with Claude Code