Skip to content

Commit 97e7ed8

Browse files
authored
Merge pull request #53 from getlang-dev/fix-mod-context
fix modifier context
2 parents 15da8a2 + ecd87ab commit 97e7ed8

File tree

8 files changed

+75
-29
lines changed

8 files changed

+75
-29
lines changed

.changeset/loose-baboons-take.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@getlang/parser": patch
3+
"@getlang/get": patch
4+
---
5+
6+
fix modifier context

packages/get/src/calls.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,14 @@ export async function callModifier(
1616
) {
1717
const entry = await registry.importMod(mod)
1818
if (entry) {
19-
const ctx =
20-
context && entry.materialize ? materialize(context) : context?.data
19+
let ctx: any
20+
if (entry?.useContext) {
21+
if (context && entry.materialize) {
22+
ctx = materialize(context)
23+
} else {
24+
ctx = context?.data
25+
}
26+
}
2127
return entry.mod(ctx, args)
2228
}
2329

packages/get/src/registry.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ type ModEntry = {
1313

1414
type Info = {
1515
ast: Program
16-
imports: Set<string>
16+
imports: string[]
17+
contextMods: string[]
1718
isMacro: boolean
1819
}
1920

@@ -86,15 +87,17 @@ export class Registry {
8687
this.info[module] ??= Promise.resolve().then(async () => {
8788
const source = await this.hooks.import(module)
8889
const ast = parse(source)
89-
const { imports, ...info } = analyze(ast)
90-
let isMacro = info.hasUnboundSelector
91-
for (const [mod, unbound] of info.modifiers) {
92-
if (unbound && !isMacro) {
93-
const entry = await this.importMod(mod)
94-
isMacro = entry?.useContext || false
90+
const info = analyze(ast)
91+
const imports = [...info.imports]
92+
const contextMods: string[] = []
93+
for (const mod of info.modifiers) {
94+
const entry = await this.importMod(mod)
95+
if (entry?.useContext ?? true) {
96+
contextMods.push(mod)
9597
}
9698
}
97-
return { ast, imports, isMacro }
99+
const isMacro = info.hasUnboundSelector || contextMods.length > 0
100+
return { ast, imports, contextMods, isMacro }
98101
})
99102
return this.info[module]
100103
}
@@ -106,15 +109,15 @@ export class Registry {
106109
}
107110
const key = buildImportKey(module, contextType)
108111
this.entries[key] ??= Promise.resolve().then(async () => {
109-
const { ast, imports } = await this.getInfo(module)
110-
const macros: string[] = []
112+
const { ast, imports, contextMods } = await this.getInfo(module)
113+
const contextual = [...contextMods]
111114
for (const i of imports) {
112115
const depInfo = await this.getInfo(i)
113116
if (depInfo.isMacro) {
114-
macros.push(i)
117+
contextual.push(i)
115118
}
116119
}
117-
const simplified = desugar(ast, macros)
120+
const simplified = desugar(ast, contextual)
118121
const { inputs, calls, modifiers } = analyze(simplified)
119122

120123
const returnTypes: Record<string, TypeInfo> = {}

packages/parser/src/passes/analyze.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export function analyze(ast: Program) {
55
const scope = new ScopeTracker()
66
const inputs = new Set<string>()
77
const calls = new Set<string>()
8-
const modifiers = new Map<string, boolean>()
8+
const modifiers = new Set<string>()
99
const imports = new Set<string>()
1010
let hasUnboundSelector = false
1111

@@ -22,9 +22,7 @@ export function analyze(ast: Program) {
2222
hasUnboundSelector ||= !scope.context
2323
},
2424
ModifierExpr(node) {
25-
const mod = node.modifier.value
26-
const unbound = modifiers.get(mod) || !scope.context
27-
modifiers.set(mod, unbound)
25+
modifiers.add(node.modifier.value)
2826
},
2927
})
3028

packages/parser/src/passes/desugar.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export type DesugarPass = (
1111
ast: Program,
1212
tools: {
1313
parsers: RequestParsers
14-
macros: string[]
14+
contextual: string[]
1515
},
1616
) => Program
1717

@@ -23,13 +23,13 @@ const visitors = [
2323
dropDrills,
2424
]
2525

26-
export function desugar(ast: Program, macros: string[] = []): Program {
26+
export function desugar(ast: Program, contextual: string[] = []): Program {
2727
const parsers = new RequestParsers()
2828
const program = visitors.reduce((ast, pass) => {
2929
parsers.reset()
30-
return pass(ast, { parsers, macros })
30+
return pass(ast, { parsers, contextual })
3131
}, ast)
3232
// inference pass `registerCalls` is included in the desugar phase
3333
// it produces the list of called modules required for type inference
34-
return registerCalls(program, macros)
34+
return registerCalls(program, contextual)
3535
}

packages/parser/src/passes/desugar/context.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { QuerySyntaxError } from '@getlang/lib/errors'
33
import { ScopeTracker, transform } from '@getlang/walker'
44
import type { DesugarPass } from '../desugar.js'
55

6-
export const resolveContext: DesugarPass = (ast, { parsers, macros }) => {
6+
export const resolveContext: DesugarPass = (ast, { parsers, contextual }) => {
77
const scope = new ScopeTracker()
88

99
const program = transform(ast, {
@@ -34,16 +34,17 @@ export const resolveContext: DesugarPass = (ast, { parsers, macros }) => {
3434

3535
ModifierExpr(node) {
3636
const ctx = scope.context
37-
if (ctx?.kind === 'RequestExpr') {
38-
const mod = node.modifier.value
37+
const mod = node.modifier.value
38+
if (contextual.includes(mod) && ctx?.kind === 'RequestExpr') {
3939
// replace modifier with shared parser
4040
return parsers.lookup(ctx, mod)
4141
}
4242
},
4343

4444
ModuleExpr(node, path) {
4545
const ctx = scope.context
46-
if (macros.includes(node.module.value) && ctx?.kind === 'RequestExpr') {
46+
const module = node.module.value
47+
if (contextual.includes(module) && ctx?.kind === 'RequestExpr') {
4748
path.insertBefore(parsers.lookup(ctx))
4849
}
4950
},

packages/parser/src/passes/inference/calls.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import { isToken } from '@getlang/ast'
33
import { transform } from '@getlang/walker'
44
import { LineageTracker } from '../lineage.js'
55

6-
export function registerCalls(ast: Program, macros: string[] = []): Program {
6+
export function registerCalls(
7+
ast: Program,
8+
contextual: string[] = [],
9+
): Program {
710
const scope = new LineageTracker()
811

912
function registerCall(node: Expr) {
@@ -38,7 +41,7 @@ export function registerCalls(ast: Program, macros: string[] = []): Program {
3841
},
3942

4043
ModuleExpr(node) {
41-
if (macros.includes(node.module.value)) {
44+
if (contextual.includes(node.module.value)) {
4245
return { ...node, call: true }
4346
}
4447
},

test/modifiers.spec.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,21 @@ import { describe, expect, test } from 'bun:test'
22
import type { Modifier, ModifierHook } from '@getlang/lib'
33
import { invariant } from '@getlang/lib'
44
import { ValueTypeError } from '@getlang/lib/errors'
5+
import type { Fetch } from './helpers.js'
56
import { execute as exec } from './helpers.js'
67

78
function execute(
89
source: string | Record<string, string>,
910
name: string,
1011
fn: Modifier,
1112
useContext?: boolean,
13+
fetch?: Fetch,
1214
) {
1315
const modifier: ModifierHook = mod => {
1416
expect(mod).toEqual(name)
1517
return { modifier: fn, useContext }
1618
}
17-
return exec(source, {}, { hooks: { modifier } })
19+
return exec(source, {}, { fetch, hooks: { modifier } })
1820
}
1921

2022
describe('modifiers', () => {
@@ -31,10 +33,37 @@ describe('modifiers', () => {
3133
expect(ctx).toEqual(1)
3234
return ctx + 1
3335
},
36+
true,
3437
)
3538
expect(result).toEqual(2)
3639
})
3740

41+
test('without context', async () => {
42+
const result = await execute(
43+
`
44+
GET http://example.com
45+
46+
set url = $ -> url
47+
set value = @nocontext
48+
49+
extract { $url, $value }
50+
`,
51+
'nocontext',
52+
ctx => {
53+
expect(ctx).toBeUndefined()
54+
return 123
55+
},
56+
false,
57+
() =>
58+
new Response('<!doctype html><h1>test</h1>', {
59+
headers: {
60+
'content-type': 'text/html',
61+
},
62+
}),
63+
)
64+
expect(result).toEqual({ url: 'http://example.com/', value: 123 })
65+
})
66+
3867
test('with args', async () => {
3968
const result = await execute(
4069
'extract @product({ a: 7, b: 6 })',

0 commit comments

Comments
 (0)