Skip to content

Commit

Permalink
Async analysis
Browse files Browse the repository at this point in the history
  • Loading branch information
jg-rp committed Nov 22, 2024
1 parent 730ab19 commit c0a19e3
Show file tree
Hide file tree
Showing 17 changed files with 146 additions and 66 deletions.
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export { Drop } from './drop'
export { Emitter } from './emitters'
export { defaultOperators, Operators, evalToken, evalQuotedToken, Expression, isFalsy, isTruthy } from './render'
export { Context, Scope } from './context'
export { Value, Hash, Template, FilterImplOptions, Tag, Filter, Output, Variable, VariableLocation, VariableSegments, Variables, analyzeSync } from './template'
export { Value, Hash, Template, FilterImplOptions, Tag, Filter, Output, Variable, VariableLocation, VariableSegments, Variables, analyze, analyzeSync } from './template'
export { Token, TopLevelToken, TagToken, ValueToken } from './tokens'
export { TokenKind, Tokenizer, ParseStream } from './parser'
export { filters } from './filters'
Expand Down
2 changes: 1 addition & 1 deletion src/tags/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default class extends Tag {
: renderCurrent
}

public children (): Iterable<Template> {
public * children (): Generator<unknown, Template[]> {
return this.templates
}

Expand Down
2 changes: 1 addition & 1 deletion src/tags/capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export default class extends Tag {
ctx.bottom()[this.variable] = html
}

public children (): Iterable<Template> {
public * children (): Generator<unknown, Template[]> {
return this.templates
}

Expand Down
8 changes: 4 additions & 4 deletions src/tags/case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,11 @@ export default class extends Tag {
yield * this.branches.flatMap(b => b.values)
}

public * children (): Iterable<Template> {
yield * this.branches.flatMap(b => b.templates)

public * children (): Generator<unknown, Template[]> {
const templates = this.branches.flatMap(b => b.templates)
if (this.elseTemplates) {
yield * this.elseTemplates
templates.push(...this.elseTemplates)
}
return templates
}
}
6 changes: 4 additions & 2 deletions src/tags/echo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ export default class extends Tag {
emitter.write(val)
}

public arguments (): Arguments {
return this.value ? [this.value] : []
public * arguments (): Arguments {
if (this.value) {
yield this.value
}
}
}
8 changes: 4 additions & 4 deletions src/tags/for.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,12 @@ export default class extends Tag {
ctx.pop()
}

public * children (): Iterable<Template> {
yield * this.templates

public * children (): Generator<unknown, Template[]> {
const templates = this.templates.slice()
if (this.elseTemplates) {
yield * this.elseTemplates
templates.push(...this.elseTemplates)
}
return templates
}

public * arguments (): Arguments {
Expand Down
8 changes: 4 additions & 4 deletions src/tags/if.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,12 @@ export default class extends Tag {
yield r.renderTemplates(this.elseTemplates || [], ctx, emitter)
}

public * children (): Iterable<Template> {
yield * this.branches.flatMap(b => b.templates)

public * children (): Generator<unknown, Template[]> {
const templates = this.branches.flatMap(b => b.templates)
if (this.elseTemplates) {
yield * this.elseTemplates
templates.push(...this.elseTemplates)
}
return templates
}

public arguments (): Arguments {
Expand Down
10 changes: 3 additions & 7 deletions src/tags/include.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Template, ValueToken, TopLevelToken, Liquid, Tag, assert, evalToken, Ha
import { BlockMode, Scope } from '../context'
import { Parser } from '../parser'
import { Arguments, PartialScope } from '../template'
import { isString, isValueToken, toValueSync } from '../util'
import { isString, isValueToken } from '../util'
import { parseFilePath, renderFilePath } from './render'

export default class extends Tag {
Expand Down Expand Up @@ -43,14 +43,10 @@ export default class extends Tag {
ctx.restoreRegister(saved)
}

public children (partials: boolean): Iterable<Template> {
public * children (partials: boolean, sync: boolean): Generator<unknown, Template[]> {
if (partials && isString(this['file'])) {
// TODO: async
// TODO: throw error if this.file does not exist?
return toValueSync(this.liquid._parsePartialFile(this['file'], true, this['currentFile']))
return (yield this.liquid._parsePartialFile(this['file'], sync, this['currentFile'])) as Template[]
}

// XXX: We're silently ignoring dynamically named partial templates.
return []
}

Expand Down
11 changes: 6 additions & 5 deletions src/tags/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { parseFilePath, renderFilePath, ParsedFileName } from './render'
import { BlankDrop } from '../drop'
import { Parser } from '../parser'
import { Arguments, PartialScope } from '../template'
import { isString, isValueToken, toValueSync } from '../util'
import { isString, isValueToken } from '../util'

export default class extends Tag {
args: Hash
Expand Down Expand Up @@ -44,13 +44,14 @@ export default class extends Tag {
ctx.pop()
}

public * children (partials: boolean): Iterable<Template> {
yield * this.templates
public * children (partials: boolean): Generator<unknown, Template[]> {
const templates = this.templates.slice()

if (partials && isString(this.file)) {
// TODO: async
yield * toValueSync(this.liquid._parsePartialFile(this.file, true, this['currentFile']))
templates.push(...(yield this.liquid._parsePartialFile(this.file, true, this['currentFile'])) as Template[])
}

return templates
}

public * arguments (): Arguments {
Expand Down
2 changes: 1 addition & 1 deletion src/tags/liquid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default class extends Tag {
yield this.liquid.renderer.renderTemplates(this.templates, ctx, emitter)
}

public children (): Iterable<Template> {
public * children (): Generator<unknown, Template[]> {
return this.templates
}
}
10 changes: 3 additions & 7 deletions src/tags/render.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { __assign } from 'tslib'
import { ForloopDrop } from '../drop'
import { isString, isValueToken, toEnumerable, toValueSync } from '../util'
import { isString, isValueToken, toEnumerable } from '../util'
import { TopLevelToken, assert, Liquid, Token, Template, evalQuotedToken, TypeGuards, Tokenizer, evalToken, Hash, Emitter, TagToken, Context, Tag } from '..'
import { Parser } from '../parser'
import { Arguments, PartialScope } from '../template'
Expand Down Expand Up @@ -77,14 +77,10 @@ export default class extends Tag {
}
}

public children (partials: boolean): Iterable<Template> {
public * children (partials: boolean, sync: boolean): Generator<unknown, Template[]> {
if (partials && isString(this['file'])) {
// TODO: async
// TODO: throw error if this.file does not exist?
return toValueSync(this.liquid._parsePartialFile(this['file'], true, this['currentFile']))
return (yield this.liquid._parsePartialFile(this['file'], sync, this['currentFile'])) as Template[]
}

// XXX: We're silently ignoring dynamically named partial templates.
return []
}

Expand Down
2 changes: 1 addition & 1 deletion src/tags/tablerow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export default class extends Tag {
ctx.pop()
}

public children (): Template[] {
public * children (): Generator<unknown, Template[]> {
return this.templates
}

Expand Down
4 changes: 1 addition & 3 deletions src/tags/unless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,11 @@ export default class extends Tag {
yield r.renderTemplates(this.elseTemplates, ctx, emitter)
}

public children (): Template[] {
public * children (): Generator<unknown, Template[]> {
const children = this.branches.flatMap(b => b.templates)

if (this.elseTemplates) {
children.push(...this.elseTemplates)
}

return children
}

Expand Down
6 changes: 6 additions & 0 deletions src/template/analysis.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,10 @@ describe('Variable map', () => {
mapping.push(v)
expect(mapping.has(v)).toBe(true)
})

it('should return an empty array if a variable is not in the map', () => {
const v = new Variable(['foo', 'bar'], { row: 1, col: 1, file: undefined })
const mapping = new VariableMap()
expect(mapping.get(v)).toStrictEqual([])
})
})
66 changes: 46 additions & 20 deletions src/template/analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import {
isRangeToken,
isString,
isValueToken,
isWordToken
isWordToken,
toPromise,
toValueSync
} from '../util'

/**
Expand Down Expand Up @@ -53,7 +55,7 @@ export class VariableMap extends Map<Variable | string, Variable[]> {
if (!this.has(k)) {
this.set(k, [])
}
return super.get(k) || []
return super.get(k) as Variable[]
}

has (key: string | Variable): boolean {
Expand All @@ -73,7 +75,7 @@ export class VariableMap extends Map<Variable | string, Variable[]> {
}

/**
* The result of calling `analyze()`.
* The result of calling `analyze()` or `analyzeSync()`.
*/
export interface StaticAnalysis {
/**
Expand All @@ -100,10 +102,18 @@ export interface StaticAnalysis {
locals: Variables;
}

/**
* Statically analyze a template and report variable usage.
*/
export function analyzeSync (templates: Template[], partials = true): StaticAnalysis {
export interface StaticAnalysisOptions {
/**
* When `true` (the default), try to load partial templates and analyze them too.
*/
partials?: boolean;
}

export const defaultStaticAnalysisOptions: StaticAnalysisOptions = {
partials: true
}

function * _analyze (templates: Template[], partials: boolean, sync: boolean): Generator<unknown, StaticAnalysis> {
const variables = new VariableMap()
const globals = new VariableMap()
const locals = new VariableMap()
Expand All @@ -114,7 +124,7 @@ export function analyzeSync (templates: Template[], partials = true): StaticAnal
// Names of partial templates that we've already analyzed.
const seen: Set<string | undefined> = new Set()

function updateVariables (variable: Variable, scope: DummyScope): void {
function * updateVariables (variable: Variable, scope: DummyScope): Generator<unknown, void> {
variables.push(variable)

// Variables that are not in scope are assumed to be global, that is,
Expand All @@ -127,16 +137,16 @@ export function analyzeSync (templates: Template[], partials = true): StaticAnal
// recurse for nested Variables
for (const segment of variable.segments.slice(1)) {
if (segment instanceof Variable) {
updateVariables(segment, scope)
yield updateVariables(segment, scope)
}
}
}

function visit (template: Template, scope: DummyScope): void {
function * visit (template: Template, scope: DummyScope): Generator<unknown, void> {
if (template.arguments) {
for (const arg of template.arguments()) {
for (const variable of extractVariables(arg)) {
updateVariables(variable, scope)
yield updateVariables(variable, scope)
}
}
}
Expand All @@ -155,8 +165,8 @@ export function analyzeSync (templates: Template[], partials = true): StaticAnal

if (partial === undefined) {
// Layouts, for example, can have children that are not partials.
for (const child of template.children(partials)) {
visit(child, scope)
for (const child of (yield template.children(partials, sync)) as Template[]) {
yield visit(child, scope)
}
return
}
Expand All @@ -167,8 +177,8 @@ export function analyzeSync (templates: Template[], partials = true): StaticAnal
? new DummyScope(new Set(partial.scope))
: scope.push(new Set(partial.scope))

for (const child of template.children(partials)) {
visit(child, partialScope)
for (const child of (yield template.children(partials, sync)) as Template[]) {
yield visit(child, partialScope)
seen.add(partial.name)
}

Expand All @@ -178,8 +188,8 @@ export function analyzeSync (templates: Template[], partials = true): StaticAnal
scope.push(new Set(template.blockScope()))
}

for (const child of template.children(partials)) {
visit(child, scope)
for (const child of (yield template.children(partials, sync)) as Template[]) {
yield visit(child, scope)
}

if (template.blockScope) {
Expand All @@ -190,7 +200,7 @@ export function analyzeSync (templates: Template[], partials = true): StaticAnal
}

for (const template of templates) {
visit(template, rootScope)
yield visit(template, rootScope)
}

return {
Expand All @@ -200,14 +210,30 @@ export function analyzeSync (templates: Template[], partials = true): StaticAnal
}
}

/**
* Statically analyze a template and report variable usage.
*/
export function analyze (template: Template[], options: StaticAnalysisOptions = {}): Promise<StaticAnalysis> {
const opts = { ...defaultStaticAnalysisOptions, ...options } as Required<StaticAnalysisOptions>
return toPromise(_analyze(template, opts.partials, false))
}

/**
* Statically analyze a template and report variable usage.
*/
export function analyzeSync (template: Template[], options: StaticAnalysisOptions = {}): StaticAnalysis {
const opts = { ...defaultStaticAnalysisOptions, ...options } as Required<StaticAnalysisOptions>
return toValueSync(_analyze(template, opts.partials, true))
}

/**
* A stack to manage scopes while traversing templates during static analysis.
*/
class DummyScope {
private stack: Array<Set<string>>

constructor (globals?: Set<string>) {
this.stack = globals ? [globals] : []
constructor (globals: Set<string>) {
this.stack = [globals]
}

public has (key: string): boolean {
Expand Down
21 changes: 19 additions & 2 deletions src/template/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,29 @@ import { Value } from './value'

export type Argument = Value | ValueToken
export type Arguments = Iterable<Argument>
export type PartialScope = { name: string, isolated: boolean, scope: Iterable<string> }

/** Scope information used when analyzing partial templates. */
export interface PartialScope {
/**
* The name of the partial template. We need this to make sure we only analyze
* each template once.
* */
name: string;

/**
* If `true`, names in `scope` will be added to a new, isolated scope before
* analyzing any child templates, without access to the parent template's scope.
*/
isolated: boolean;

/** A list of names that will be in scope for the child template. */
scope: Iterable<string>;
}

export interface Template {
token: Token;
render(ctx: Context, emitter: Emitter): any;
children?(partials: boolean): Iterable<Template>;
children?(partials: boolean, sync: boolean): Generator<unknown, Template[]> ;
arguments?(): Arguments;
blockScope?(): Iterable<string>;
localScope?(): Iterable<IdentifierToken | QuotedToken>;
Expand Down
Loading

0 comments on commit c0a19e3

Please sign in to comment.