Skip to main content

Template Static Analysis

New in version 1.3.0

Use the analyze() or analyzeSync() methods of a Liquid Template to traverse its abstract syntax tree and report template variable, tag and filter usage.

All Template Variables

The TemplateAnalysis object returned from Template.analyze() includes a variables property, mapping template variable names to arrays of locations where those names occur. Each location is an object with a templateName and lineNumber property.

import { Template } from "liquidscript";

const template = Template.fromString(`\
{% assign people = "Sally, John, Brian, Sue" | split: ", " %}
{{ people }}
{% for name in people %}
{{ forloop.index }} - {{ greeting }}, {{ name }}!
{% endfor %}`);

const analysis = template.analyzeSync();
console.log(Object.keys(analysis.variables));

for (const [name, locations] of Object.entries(analysis.variables)) {
for (const { templateName, lineNumber } of locations) {
console.log(`'${name}' found in '${templateName}' on line ${lineNumber}`);
}
}
output
['people', 'forloop.index', 'greeting', 'name']
'people' found in '<string>' on line 2
'people' found in '<string>' on line 3
'forloop.index' found in '<string>' on line 4
'greeting' found in '<string>' on line 4
'name' found in '<string>' on line 4

Global Template Variables

The globalVariables property of a TemplateAnalysis object is similar to variables, but only includes those variables that are not in scope from previous assign, capture, increment or decrement tags, or added to a block's scope by a block tag.

import { Template } from "liquidscript";

const template = Template.fromString(`\
{% assign people = "Sally, John, Brian, Sue" | split: ", " %}
{{ people }}
{% for name in people %}
{{ forloop.index }} - {{ greeting }}, {{ name }}!
{% endfor %}`);

const analysis = template.analyzeSync();
console.log("all variables:", Object.keys(analysis.variables));
console.log("global variables:", Object.keys(analysis.globalVariables));
output
all variables:  ['people', 'forloop.index', 'greeting', 'name']
global variables: ['greeting']

While greeting is assumed to be global (that is, provided by application developers rather than a template author), LiquidScript knows that forloop is in scope for the duration of the for block. If people were referenced before being assigned, we'd see an entry in the people array for each location where it is out of scope.

import { Template } from "liquidscript";

const template = Template.fromString(`\
{{ people }}
{% assign people = "Sally, John, Brian, Sue" | split: ", " %}
{{ people }}`);

const analysis = template.analyzeSync();

for (const [name, locations] of Object.entries(analysis.globalVariables)) {
for (const { templateName, lineNumber } of locations) {
console.log(
`'${name}' is out of scope in '${templateName}' on line ${lineNumber}`
);
}
}
output
'people' is out of scope in '<string>' on line 1

Local Template Variables

The localVariables property of a TemplateAnalysis object is, again, a mapping of template variable names to their locations. Each entry is the location of an assign, capture, increment, or decrement tag (or any custom tag that introduces names into the template local namespace) that initializes or updates the variable.

import { Template } from "liquidscript";

const template = Template.fromString(`\
{% assign people = "Sally, John, Brian, Sue" | split: ", " %}
{% assign people = "Bob, Frank" | split: ", " %}`);

const analysis = template.analyzeSync();

for (const [name, locations] of Object.entries(analysis.localVariables)) {
for (const { templateName, lineNumber } of locations) {
console.log(
`'${name}' assigned in '${templateName}' on line ${lineNumber}`
);
}
}
output
'people' assigned in '<string>' on line 1
'people' assigned in '<string>' on line 2

Filters

New in version 1.8.0

The filters property of TemplateAnalysis is an object mapping filter names to their locations.

import { Template } from "liquidscript";

const template = Template.fromString(`\
{% assign people = "Sally, John, Brian, Sue" | split: ", " %}
{% for person in people %}
- {{ person | upcase | prepend: 'Hello, ' }}
{% endfor %}`);

const analysis = template.analyzeSync();

for (const [filterName, locations] of Object.entries(analysis.filters)) {
for (const { templateName, lineNumber } of locations) {
console.log(
`'${filterName}' found in '${templateName}' on line ${lineNumber}`
);
}
}
output
'split' found in '<string>' on line 1
'upcase' found in '<string>' on line 3
'prepend' found in '<string>' on line 3

Tags

New in version 1.8.0

The tags property of TemplateAnalysis is an object mapping tag names to their locations. Note that, for block tags, we only report the locations of the opening tag, and {% raw %} tags will never be included.

import { Template } from "liquidscript";

const template = Template.fromString(`\
{% assign people = "Sally, John, Brian, Sue" | split: ", " %}
{% for person in people %}
- {{ person | upcase | prepend: 'Hello, ' }}
{% endfor %}`);

const analysis = template.analyzeSync();

for (const [tagName, locations] of Object.entries(analysis.tags)) {
for (const { templateName, lineNumber } of locations) {
console.log(
`'${tagName}' found in '${templateName}' on line ${lineNumber}`
);
}
}
output
'assign' found in '<string>' on line 1
'for' found in '<string>' on line 2

Analyzing Partial Templates

When the followPartials option to Template.analyze() is true (the default), LiquidScript will attempt to load and analyze templates from include and render tags. In the case of include, this is only possible when the template name is a string literal.

import { Environment, ObjectLoader } from "liquidscript";

const templates = {
layout: `"\
{% include 'nav', title: page_name %}
{% render 'foot' with website as site_name %}
`,
nav: "{{ title }} nav bar",
foot: "a footer for {{ site_name }}",
};

const env = new Environment({ loader: new ObjectLoader(templates) });
const layout = env.getTemplateSync("layout");

const analysis = layout.analyzeSync({ followPartials: true });
console.log(analysis.variables);
output
{
title: [ { templateName: 'nav', lineNumber: 1 } ],
page_name: [ { templateName: 'layout', lineNumber: 1 } ],
site_name: [ { templateName: 'foot', lineNumber: 1 } ],
website: [ { templateName: 'layout', lineNumber: 2 } ]
}

When the raiseForFailures option is true (the default), we should expect a TemplateTraversalError to be thrown if a partial template can not be loaded. If raiseForFailures is false, a mapping of unloadable include/render tags is available as TemplateAnalysis.unloadablePartials.

import { Environment, ObjectLoader } from "liquidscript";

const templates = {
layout: `"\
{% include 'nav', title: page_name %}
{% render 'foot' with website as site_name %}
`,
};

const env = new Environment({ loader: new ObjectLoader(templates) });
const layout = env.getTemplateSync("layout");

const analysis = layout.analyzeSync({
followPartials: true,
raiseForFailures: false,
});
console.log(analysis.unloadablePartials);
output
{
nav: [ { templateName: 'layout', lineNumber: 1 } ],
foot: [ { templateName: 'layout', lineNumber: 2 } ]
}

Analyzing Custom Tags

All built-in tags (the tag's Node and Expression objects) implement a children() method. When analyzing a custom tag that does not implement children(), and with the raiseForFailures argument set to true (the default), LiquidSCript will raise a TemplateTraversalError. When raiseForFailures is false, a mapping of unvisitable AST nodes and expressions is available as TemplateAnalysis.failedVisits.

import {
Environment,
Node,
ObjectLoader,
RenderContext,
RenderStream,
Tag,
tokens,
} from "liquidscript";

class ExampleNode implements Node {
constructor(readonly token: tokens.Token) {}

async render(context: RenderContext, out: RenderStream): Promise<void> {
out.write("example node");
}

renderSync(context: RenderContext, out: RenderStream): void {
out.write("example node");
}

// This node does not implement `children()`
}

class ExampleTag implements Tag {
parse(stream: tokens.TokenStream): Node {
return new ExampleNode(stream.current);
}

// This tag does not implement `children()`
}

const templates = {
layout: "{% example %}",
};

const env = new Environment({ loader: new ObjectLoader(templates) });
env.addTag("example", new ExampleTag());

const layout = env.getTemplateSync("layout");
const analysis = layout.analyzeSync({
followPartials: true,
raiseForFailures: false,
});
console.log(analysis.failedVisits);
output
{ ExampleNode: [ { templateName: 'layout', lineNumber: 1 } ] }

Node.children() should return an array of ChildNode objects. Each ChildNode includes a child Expression and/or Node, plus any names the tag adds to the template local scope or subsequent block scope. Please see src/builtin/tags for examples.

Expression.children() is expected to return an array of child Expressions. For example, RangeLiteral.children() returns an array containing expressions for its start and stop properties. Please see src/expression.ts for examples.