Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: static variable analysis #770

Merged
merged 29 commits into from
Dec 28, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
4f0c2c4
feat: static variable analysis
jg-rp Nov 16, 2024
e7b8559
Accept any iterable from `children`, `arguments`, etc.
jg-rp Nov 17, 2024
a69f33c
Test analysis of standard tags
jg-rp Nov 18, 2024
e5163ba
Merge branch 'harttle:master' into static-analysis-alternate
jg-rp Nov 19, 2024
5705bc6
Use `TagToken.tokenizer` instead of creating a new one
jg-rp Nov 19, 2024
2081083
Test analysis of netsted tags
jg-rp Nov 19, 2024
502a80d
Group variables by their root value
jg-rp Nov 19, 2024
5a9d192
Test analysis of nested globals and locals
jg-rp Nov 19, 2024
2cb9a4f
Analyze included and rendered templates WIP
jg-rp Nov 20, 2024
bc6be99
Use existing tokenizer when constructing `Hash`
jg-rp Nov 21, 2024
7f63cef
Improve test coverage
jg-rp Nov 21, 2024
0d1393b
Analyze variables from `layout` and `block` tags
jg-rp Nov 21, 2024
a1972ab
Test analysis of Jekyll style includes
jg-rp Nov 21, 2024
730ab19
Handle variables that start with a nested variable
jg-rp Nov 21, 2024
c0a19e3
Async analysis
jg-rp Nov 22, 2024
1a79437
Test non-standard tag end to end
jg-rp Nov 23, 2024
d9f47d6
Implement convenience analysis methods on the `Liquid` class
jg-rp Nov 23, 2024
67fdbe5
More analysis convenience methods
jg-rp Nov 23, 2024
cde3b5d
Accept string or template array
jg-rp Nov 23, 2024
a3a93cc
Draft static analysis docs
jg-rp Nov 23, 2024
2bf55db
Deduplicate variables names
jg-rp Nov 23, 2024
3ff787d
Fix isolated scope global variable map
jg-rp Dec 5, 2024
5c76035
Coerce variables to strings instead of extending String
jg-rp Dec 5, 2024
9770ff3
Private map instead of extending Map
jg-rp Dec 5, 2024
ad2333e
Fix e2e test
jg-rp Dec 5, 2024
f73f0d1
Tentatively implement analysis of aliased variables
jg-rp Dec 6, 2024
e9b11f4
Fix nested variable segments array
jg-rp Dec 22, 2024
d1f7bea
Merge branch 'harttle:master' into static-analysis-alternate
jg-rp Dec 28, 2024
e241ef9
Update docs sidebar
jg-rp Dec 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 } from './template'
export { Value, Hash, Template, FilterImplOptions, Tag, Filter, Output, Variable, VariableLocation, VariableSegments, Variables, analyze } 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/render/expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { Context } from '../context'
import type { UnaryOperatorHandler } from '../render'

export class Expression {
private postfix: Token[]
readonly postfix: Token[]

public constructor (tokens: IterableIterator<Token>) {
this.postfix = [...toPostfix(tokens)]
Expand Down
10 changes: 10 additions & 0 deletions src/tags/assign.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Value, Liquid, TopLevelToken, TagToken, Context, Tag } from '..'
import { Arguments } from '../template'

export default class extends Tag {
private key: string
private value: Value
Expand All @@ -17,4 +19,12 @@ export default class extends Tag {
* render (ctx: Context): Generator<unknown, void, unknown> {
ctx.bottom()[this.key] = yield this.value.value(ctx, this.liquid.options.lenientIf)
}

public arguments (): Arguments {
return [this.value]
}

public localScope (): string[] {
return [this.key]
}
}
4 changes: 4 additions & 0 deletions src/tags/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,8 @@ export default class extends Tag {
? (superBlock: BlockDrop, emitter: Emitter) => renderChild(new BlockDrop(() => renderCurrent(superBlock, emitter)), emitter)
: renderCurrent
}

public children (): Template[] {
return this.templates
}
}
8 changes: 8 additions & 0 deletions src/tags/capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,12 @@ export default class extends Tag {
if (quoted) return evalQuotedToken(quoted)
throw this.tokenizer.error('invalid capture name')
}

public children (): Template[] {
return this.templates
harttle marked this conversation as resolved.
Show resolved Hide resolved
}

public localScope (): string[] {
return [this.variable]
}
}
15 changes: 15 additions & 0 deletions src/tags/case.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ValueToken, Liquid, toValue, evalToken, Value, Emitter, TagToken, TopLevelToken, Context, Template, Tag, ParseStream } from '..'
import { Parser } from '../parser'
import { equals } from '../render'
import { Arguments } from '../template'

export default class extends Tag {
value: Value
Expand Down Expand Up @@ -71,4 +72,18 @@ export default class extends Tag {
yield r.renderTemplates(this.elseTemplates, ctx, emitter)
}
}

public arguments (): Arguments {
return this.branches.flatMap(b => b.values)
}

public children (): Template[] {
const children = this.branches.flatMap(b => b.templates)

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

return children
}
}
11 changes: 11 additions & 0 deletions src/tags/cycle.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { TopLevelToken, Liquid, ValueToken, evalToken, Emitter, TagToken, Context, Tag } from '..'
import { Arguments } from '../template'

export default class extends Tag {
private candidates: ValueToken[] = []
Expand Down Expand Up @@ -38,4 +39,14 @@ export default class extends Tag {
groups[fingerprint] = idx
return yield evalToken(candidate, ctx)
}

public arguments (): Arguments {
const args = this.candidates.slice()

if (this.group) {
args.push(this.group)
}

return args
}
harttle marked this conversation as resolved.
Show resolved Hide resolved
}
9 changes: 9 additions & 0 deletions src/tags/decrement.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Tag, Liquid, TopLevelToken, Emitter, TagToken, Context } from '..'
import { Arguments } from '../template'
import { isNumber, stringify } from '../util'

export default class extends Tag {
Expand All @@ -14,4 +15,12 @@ export default class extends Tag {
}
emitter.write(stringify(--scope[this.variable]))
}

public arguments (): Arguments {
return [this.variable]
}

public localScope (): string[] {
return [this.variable]
}
}
5 changes: 5 additions & 0 deletions src/tags/echo.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Liquid, TopLevelToken, Emitter, Value, TagToken, Context, Tag } from '..'
import { Arguments } from '../template'

export default class extends Tag {
private value?: Value
Expand All @@ -15,4 +16,8 @@ export default class extends Tag {
const val = yield this.value.value(ctx, false)
emitter.write(val)
}

public arguments (): Arguments {
return this.value ? [this.value] : []
}
}
29 changes: 28 additions & 1 deletion src/tags/for.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Hash, ValueToken, Liquid, Tag, evalToken, Emitter, TagToken, TopLevelToken, Context, Template, ParseStream } from '..'
import { assertEmpty, toEnumerable } from '../util'
import { assertEmpty, isValueToken, toEnumerable } from '../util'
import { ForloopDrop } from '../drop/forloop-drop'
import { Parser } from '../parser'
import { Arguments } from '../template'

const MODIFIERS = ['offset', 'limit', 'reversed']

Expand Down Expand Up @@ -78,6 +79,32 @@ export default class extends Tag {
}
ctx.pop()
}

public children (): Template[] {
const children = [...this.templates]

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

return children
}

public arguments (): Arguments {
const args: Arguments = [this.collection]

for (const v of Object.values(this.hash.hash)) {
if (isValueToken(v)) {
args.push(v)
}
}

return args
}

public blockScope (): string[] {
return [this.variable, 'forloop']
}
}

function reversed<T> (arr: Array<T>) {
Expand Down
15 changes: 15 additions & 0 deletions src/tags/if.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Liquid, Tag, Value, Emitter, isTruthy, TagToken, TopLevelToken, Context, Template } from '..'
import { Parser } from '../parser'
import { Arguments } from '../template'
import { assert, assertEmpty } from '../util'

export default class extends Tag {
Expand Down Expand Up @@ -44,4 +45,18 @@ export default class extends Tag {
}
yield r.renderTemplates(this.elseTemplates || [], ctx, emitter)
}

public children (): Template[] {
const children = this.branches.flatMap(b => b.templates)

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

return children
}

public arguments (): Arguments {
return this.branches.map(b => b.value)
}
}
34 changes: 34 additions & 0 deletions src/tags/include.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Template, ValueToken, TopLevelToken, Liquid, Tag, assert, evalToken, Hash, Emitter, TagToken, Context } from '..'
import { BlockMode, Scope } from '../context'
import { Parser } from '../parser'
import { Arguments } from '../template'
import { isValueToken } from '../util'
import { parseFilePath, renderFilePath } from './render'

export default class extends Tag {
Expand Down Expand Up @@ -40,4 +42,36 @@ export default class extends Tag {
ctx.pop()
ctx.restoreRegister(saved)
}

public arguments (): Arguments {
const args: Arguments = []

for (const v of Object.values(this.hash.hash)) {
if (isValueToken(v)) {
args.push(v)
}
}

if (isValueToken(this['file'])) {
args.push(this['file'])
}

if (isValueToken(this.withVar)) {
args.push(this.withVar)
}

return args
}

public blockScope (): string[] {
const names: string[] = []

for (const k of Object.keys(this.hash.hash)) {
names.push(k)
}

// TODO: withVar

return names
}
}
9 changes: 9 additions & 0 deletions src/tags/increment.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { isNumber, stringify } from '../util'
import { Tag, Liquid, TopLevelToken, Emitter, TagToken, Context } from '..'
import { Arguments } from '../template'

export default class extends Tag {
private variable: string
Expand All @@ -16,4 +17,12 @@ export default class extends Tag {
scope[this.variable]++
emitter.write(stringify(val))
}

public arguments (): Arguments {
return [this.variable]
}

public localScope (): string[] {
return [this.variable]
}
}
28 changes: 28 additions & 0 deletions src/tags/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { BlockMode } from '../context'
import { parseFilePath, renderFilePath, ParsedFileName } from './render'
import { BlankDrop } from '../drop'
import { Parser } from '../parser'
import { Arguments } from '../template'
import { isValueToken } from '../util'

export default class extends Tag {
args: Hash
Expand Down Expand Up @@ -41,4 +43,30 @@ export default class extends Tag {
yield renderer.renderTemplates(templates, ctx, emitter)
ctx.pop()
}

public arguments (): Arguments {
const args: Arguments = []

for (const v of Object.values(this.args.hash)) {
if (isValueToken(v)) {
args.push(v)
}
}

if (isValueToken(this['file'])) {
args.push(this['file'])
}

return args
}

public blockScope (): string[] {
const names: string[] = []

for (const k of Object.keys(this.args.hash)) {
names.push(k)
}

return names
}
}
4 changes: 4 additions & 0 deletions src/tags/liquid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,8 @@ export default class extends Tag {
* render (ctx: Context, emitter: Emitter): Generator<unknown, void, unknown> {
yield this.liquid.renderer.renderTemplates(this.templates, ctx, emitter)
}

public children (): Template[] {
return this.templates
}
}
47 changes: 46 additions & 1 deletion src/tags/render.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { __assign } from 'tslib'
import { ForloopDrop } from '../drop'
import { toEnumerable } 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 } from '../template'

export type ParsedFileName = Template[] | Token | string | undefined

Expand Down Expand Up @@ -75,6 +76,50 @@ export default class extends Tag {
yield liquid.renderer.renderTemplates(templates, childCtx, emitter)
}
}

public arguments (): Arguments {
const args: Arguments = []

for (const v of Object.values(this.hash.hash)) {
if (isValueToken(v)) {
args.push(v)
}
}

if (isValueToken(this['file'])) {
args.push(this['file'])
}

if (this['for']) {
const { value } = this['for']
if (isValueToken(value)) {
args.push(value)
}
}

return args
}

public blockScope (): string[] {
const names: string[] = []

for (const k of Object.keys(this.hash.hash)) {
names.push(k)
}

if (this['for']) {
const { alias } = this['for']
if (isString(alias)) {
names.push(alias)
} else if (isString(this.file)) {
names.push(this.file)
}

names.push('forloop')
}

return names
}
}

/**
Expand Down
Loading
Loading