Skip to content

Commit 2c99c66

Browse files
committed
PROT-5778: support identifiers that begin with digits
The lexer dispatched any digit-leading token to lexNumber, which consumed the digit run and returned a Number without checking what followed. An identifier like "1_x_coffee_buyer" therefore lexed as Number(1) followed by Ident(_x_coffee_buyer), which then failed to parse ("Unexpected token in statement"). lexNumber now hands off to lexIdent when the token is followed by identifier characters, so the whole thing lexes as a single Ident. The handoff is guarded on the token containing a digit: the dispatcher also routes a bare sign here, so "-foo" keeps its previous behavior. JSON property names are arbitrary customer data rather than programmer-chosen symbols, so leading digits are legitimate. This mirrors the equivalent fix in the Go implementation (segmentio/fql#86) -- both now lex "friends.1.last" identically.
1 parent d8d7c06 commit 2c99c66

2 files changed

Lines changed: 50 additions & 1 deletion

File tree

src/lexer.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,41 @@ test('Lexer passes Number fixtures', () => {
130130
])
131131
})
132132

133+
test('Lexer passes digit-first ident fixtures', () => {
134+
testFixtures([
135+
// Identifiers may begin with digits: JSON property names are arbitrary
136+
// customer data, not programmer-chosen symbols. These used to lex as a
137+
// Number followed by a separate Ident, which then failed to parse.
138+
fix('1val', [t.Ident('1val'), t.EOS()], false),
139+
fix('123audience', [t.Ident('123audience'), t.EOS()], false),
140+
fix('1_x_coffee_buyer', [t.Ident('1_x_coffee_buyer'), t.EOS()], false),
141+
fix('1-x-coffee-buyer', [t.Ident('1-x-coffee-buyer'), t.EOS()], false),
142+
143+
// ...including as a segment of a dotted path
144+
fix(
145+
'properties.123audience',
146+
[t.Ident('properties'), t.Dot(), t.Ident('123audience'), t.EOS()],
147+
false
148+
),
149+
fix(
150+
'properties.1_x_coffee_buyer = true',
151+
[
152+
t.Ident('properties'),
153+
t.Dot(),
154+
t.Ident('1_x_coffee_buyer'),
155+
t.Operator('='),
156+
t.Ident('true'),
157+
t.EOS()
158+
],
159+
false
160+
),
161+
162+
// A purely numeric token is still a Number, not an Ident
163+
fix('123', [t.Number('123'), t.EOS()], false),
164+
fix('123 456', [t.Number('123'), t.Number('456'), t.EOS()], false)
165+
])
166+
})
167+
133168
test('Lexer passes Strings fixtures', () => {
134169
testFixtures([
135170
// Strings

src/lexer.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,21 @@ export class Lexer {
176176
comingUp = this.peek()
177177
}
178178

179-
return t.Number(previous + str)
179+
const token = previous + str
180+
181+
// What looked like a number is actually an identifier that begins with
182+
// digits (e.g. "123audience", "1_x_coffee_buyer"). Hand the consumed
183+
// digits to lexIdent as its prefix rather than emitting a Number and
184+
// letting the rest of the identifier lex as a separate token.
185+
//
186+
// Guarded on the token containing a digit: the dispatcher also routes a
187+
// bare sign here, so this keeps "-foo" behaving as it did instead of
188+
// turning it into an identifier.
189+
if (isIdent(this.peek()) && /[0-9]/.test(token)) {
190+
return this.lexIdent(token)
191+
}
192+
193+
return t.Number(token)
180194
}
181195

182196
private lexOperatorOrConditional(previous: string): Token {

0 commit comments

Comments
 (0)