add conditionals query
This commit is contained in:
1 parent
606bf7457f
commit
c0a2e0099c
3 files changed
+96
No files matched your search
@@ -229,6 +229,19 @@ too if it were spelled the same way. `queries/exports.scm` names it for a tool t
|
|||||||
bind in the scope around the block, which a locals query cannot do: it places a definition in the innermost
|
bind in the scope around the block, which a locals query cannot do: it places a definition in the innermost
|
||||||
scope containing it and offers no way out again.
|
scope containing it and offers no way out again.
|
||||||
|
|
||||||
|
`queries/conditionals.scm` is data for a tool too: `@conditional.group` / `@conditional.branch` /
|
||||||
|
`@conditional.default` say which bodies render on only some passes, so a checker can tell that
|
||||||
|
`{% if a %}{% now "Y" as n %}{% endif %}{{ n }}` may reach `{{ n }}` with nothing bound. A locals query
|
||||||
|
cannot: it matches a reference to the definition whose scope contains it and has no notion of a path not
|
||||||
|
taken. A group is exhaustive iff it has a `@conditional.default`, so each pattern captures the group
|
||||||
|
alongside one of its parts and a tool needs no list of which clause belongs to which group. Adding a tag
|
||||||
|
group whose body may be skipped means adding it there — `{% cache %}` is the non-obvious one, since a
|
||||||
|
cache hit skips the body outright and `CacheNode` pushes no context.
|
||||||
|
|
||||||
|
A branch that is also a `@local.scope` confines its bindings whichever way the branch goes, which is why
|
||||||
|
`{% for %}` is inert for that check: `ForNode` renders `nodelist_empty` **inside** the same
|
||||||
|
`context.push()` as the loop body, so `empty_clause` is a scope alongside `for_clause`.
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
Corpus tests live in `test/corpus/*.txt`, one file per tag.
|
Corpus tests live in `test/corpus/*.txt`, one file per tag.
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
; Which parts of a template render conditionally, for a tool checking that a
|
||||||
|
; variable is defined on every path that reaches a use of it.
|
||||||
|
;
|
||||||
|
; Django binds an "as" target straight into the context, and a tag that pushes
|
||||||
|
; no context leaves that binding visible after its group closes. So
|
||||||
|
;
|
||||||
|
; {% if a %}{% now "Y" as n %}{% endif %}{{ n }}
|
||||||
|
;
|
||||||
|
; renders the year when a is truthy and string_if_invalid ("" by default) when
|
||||||
|
; it is not — a silent empty string, not an error. queries/locals.scm cannot
|
||||||
|
; see that: a locals query matches a reference to the definition whose scope
|
||||||
|
; contains it and has no notion of a path that may not be taken.
|
||||||
|
;
|
||||||
|
; The halves a tool joins, by grouping matches on the @conditional.group node:
|
||||||
|
;
|
||||||
|
; @conditional.group the whole construct
|
||||||
|
; @conditional.branch one alternative; at most one of a group's branches
|
||||||
|
; renders on any given pass
|
||||||
|
; @conditional.default the branch that renders when no other one does
|
||||||
|
;
|
||||||
|
; A group is exhaustive iff one of its matches carries a @conditional.default.
|
||||||
|
; If it is not, some pass through it binds nothing, so every definition inside
|
||||||
|
; is conditional. If it is, a definition escapes only when every branch makes
|
||||||
|
; it. Each capture names both halves of the pair, so no ancestor walk is needed
|
||||||
|
; and no clause has to be attributed to a group by name.
|
||||||
|
;
|
||||||
|
; Branches nest, so the same test applies again to a group inside a branch.
|
||||||
|
;
|
||||||
|
; A branch that is also a @local.scope in queries/locals.scm confines its
|
||||||
|
; bindings whether or not it renders; the two queries compose and the scope
|
||||||
|
; decides first.
|
||||||
|
|
||||||
|
; if/elif are alternatives tested in order; else runs when none matched. None
|
||||||
|
; of the three pushes a context, so this is the case the check exists for.
|
||||||
|
(if_group
|
||||||
|
[
|
||||||
|
(if_clause)
|
||||||
|
(elif_clause)
|
||||||
|
] @conditional.branch) @conditional.group
|
||||||
|
|
||||||
|
(if_group
|
||||||
|
(else_clause) @conditional.default) @conditional.group
|
||||||
|
|
||||||
|
; ifchanged renders its body only when the watched value differs from the
|
||||||
|
; previous pass, and its else otherwise. It pushes no context either.
|
||||||
|
(ifchanged_group
|
||||||
|
(ifchanged_clause) @conditional.branch) @conditional.group
|
||||||
|
|
||||||
|
(ifchanged_group
|
||||||
|
(else_clause) @conditional.default) @conditional.group
|
||||||
|
|
||||||
|
; The loop body runs once per item and empty runs when there are none, so the
|
||||||
|
; two partition every render. Both are inside ForNode's context.push() — the
|
||||||
|
; empty branch included, which is easy to miss — so locals.scm scopes both and
|
||||||
|
; nothing escapes either way. Named here because the construct does branch, and
|
||||||
|
; because a tool reading only this file should not have to infer that it does.
|
||||||
|
(for_group
|
||||||
|
(for_clause) @conditional.branch) @conditional.group
|
||||||
|
|
||||||
|
(for_group
|
||||||
|
(empty_clause) @conditional.default) @conditional.group
|
||||||
|
|
||||||
|
; A cache body is skipped entirely on a cache hit, and CacheNode pushes no
|
||||||
|
; context, so a binding made inside it reaches the rest of the template on the
|
||||||
|
; first render and is gone on the next. One branch and no default: never
|
||||||
|
; exhaustive, which is the right answer.
|
||||||
|
(cache_group
|
||||||
|
(cache_clause) @conditional.branch) @conditional.group
|
||||||
|
|
||||||
|
; Not here, deliberately: block and partialdef bodies may also fail to render —
|
||||||
|
; a child template overrides the block, and a partial renders only where
|
||||||
|
; {% partial %} names it — but both clauses are @local.scope in locals.scm, so
|
||||||
|
; nothing they bind escapes to be checked. Bodies that always render once
|
||||||
|
; (with, filter, autoescape, spaceless, language, localize, localtime,
|
||||||
|
; timezone) are not conditional at all, and comment, verbatim and
|
||||||
|
; blocktranslate bodies hold no tags to bind anything.
|
||||||
@@ -11,6 +11,13 @@ variable: (identifier) @local.definition
|
|||||||
(block_clause) @local.scope
|
(block_clause) @local.scope
|
||||||
(partialdef_clause) @local.scope
|
(partialdef_clause) @local.scope
|
||||||
|
|
||||||
|
; ForNode renders nodelist_empty inside the same context.push() as the loop
|
||||||
|
; body, so "{% empty %}" is part of the loop's scope even though it is a
|
||||||
|
; separate clause: "{% for x in ys %}a{% empty %}{% now "Y" as n %}{% endfor %}{{ n }}"
|
||||||
|
; is empty. queries/conditionals.scm names both clauses as branches of the
|
||||||
|
; same group and relies on this.
|
||||||
|
(empty_clause) @local.scope
|
||||||
|
|
||||||
; blocktranslate pushes a context for its "with" and "count" bindings. Its
|
; blocktranslate pushes a context for its "with" and "count" bindings. Its
|
||||||
; asvar target is not one of them: it is written in the same tag but assigned
|
; asvar target is not one of them: it is written in the same tag but assigned
|
||||||
; outside it, and carries an "asvar" field rather than "variable" so that this
|
; outside it, and carries an "asvar" field rather than "variable" so that this
|
||||||
|
|||||||
Reference in new issue
Block a user