Skip to content

Commit 8a5371f

Browse files
committed
Provisional static analysis of snippets
1 parent ecda899 commit 8a5371f

File tree

4 files changed

+57
-3
lines changed

4 files changed

+57
-3
lines changed

liquid/builtin/tags/render_tag.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ def children(
244244
"""Return this node's children."""
245245
if isinstance(self.name, Identifier):
246246
# We're expecting an inline snippet.
247+
# Always visit inline snippets, even if include_partials is False.
247248
template: Optional[BoundTemplate] = static_context.resolve(
248249
self.name, token=self.token, default=None
249250
)
@@ -307,7 +308,13 @@ def partial_scope(self) -> Optional[Partial]:
307308
)
308309
)
309310

310-
return Partial(name=self.name, scope=PartialScope.ISOLATED, in_scope=scope)
311+
# Static analysis will use the parent template name if Partial.name is
312+
# empty. Which is what we want for inline snippets.
313+
return Partial(
314+
name=self.name if isinstance(self.name, StringLiteral) else "",
315+
scope=PartialScope.ISOLATED,
316+
in_scope=scope,
317+
)
311318

312319

313320
BIND_TAGS = frozenset((TOKEN_WITH, TOKEN_FOR))

liquid/builtin/tags/snippet.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,18 @@ def children(
6161
include_partials: bool = True, # noqa: ARG002
6262
) -> Iterable[Node]:
6363
"""Return this node's children."""
64-
yield self.block
64+
# Snippets are only visited when rendered so we get accurate variable
65+
# scope analysis.
66+
static_context.assign(
67+
self.name,
68+
BoundTemplate(
69+
static_context.env,
70+
self.block.nodes,
71+
static_context.template.name,
72+
static_context.template.path,
73+
),
74+
)
75+
return []
6576

6677
def template_scope(self) -> Iterable[Identifier]:
6778
"""Return variables this node adds to the template local scope."""

liquid/static_analysis.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,9 +179,15 @@ def _visit(node: Node, template_name: str, scope: _StaticScope) -> None:
179179
else str(partial.name.evaluate(static_context))
180180
)
181181

182-
if partial_name in seen:
182+
# XXX: what about rendering the same partial with different arguments?
183+
# TODO: to avoid double counting, we'll only want to count globals if we've
184+
# seen a partial before, and only if the arguments differ.
185+
# TODO: protect against recursive include/render!
186+
if partial_name and partial_name in seen:
183187
return
184188

189+
partial_name = partial_name or template_name
190+
185191
partial_scope = (
186192
_StaticScope(set(partial.in_scope))
187193
if partial.scope == PartialScope.ISOLATED

tests/test_static_analysis.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -936,3 +936,33 @@ def test_analyze_translate(env: Environment) -> None:
936936
"translate": [Span("", 4)],
937937
},
938938
)
939+
940+
941+
def test_analyze_snippet(env: Environment) -> None:
942+
source = "\n".join(
943+
[
944+
"{% snippet foo %}",
945+
" Hi!",
946+
" {{ bar }}",
947+
" {{ baz }}",
948+
"{% endsnippet %}",
949+
"",
950+
"{% render foo, bar: '42' %}",
951+
]
952+
)
953+
954+
_assert(
955+
env.from_string(source, name="x"),
956+
locals={"foo": [Variable(["foo"], Span("x", 11))]},
957+
globals={
958+
"baz": [Variable(["baz"], Span("x", 41))],
959+
},
960+
variables={
961+
"baz": [Variable(["baz"], Span("x", 41))],
962+
"bar": [Variable(["bar"], Span("x", 29))],
963+
},
964+
tags={
965+
"snippet": [Span("x", 3)],
966+
"render": [Span("x", 69)],
967+
},
968+
)

0 commit comments

Comments
 (0)