Skip to content

Commit 95e727b

Browse files
committed
fix(parser): /a{,3}/ is interpreted literally
Apparently, lower bounds are not optional at least when an upper bound is defined.
1 parent b558a43 commit 95e727b

File tree

3 files changed

+21
-24
lines changed

3 files changed

+21
-24
lines changed

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ class RegexBuilder {
174174
* ```typescript
175175
* RB('a').repeat(4) // a{4}
176176
* RB('a').repeat({ min: 3, max: 5 }) // a{3,5}
177-
* RB('a').repeat({ max: 5 }) // a{,5}
177+
* RB('a').repeat({ max: 5 }) // a{0,5}
178178
* RB('a').repeat({ min: 3 }) // a{3,}
179179
* RB('a').repeat({ min: 0, max: 1 }) // a?
180180
* RB('a').repeat({ min: 0 }) // a*

src/regex-parser.ts

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -139,34 +139,30 @@ const group = P.choice([
139139

140140
// Need to backtrack on bounded quantifier because if the curly bracket is
141141
// not terminated (e.g. "a{2,3") then all characters are interpreted literally.
142+
// The same if min value is missing but max value is given (e.g. "a{,3}").
143+
//
142144
// FIXME: However, this breaks something else. E.g. "a*{3}" must still be rejected as
143145
// invalid and not interpreted as "a*" and then literal charactesr "{3}".
144146
const boundedQuantifier: P.Expr.UnaryOperator<AST.RegExpAST> = P.tryElseBacktrack(
145147
P.between(
146148
P.string('{'),
147149
P.string('}'),
148-
P.optional(P.decimal).andThen(min => {
149-
if (min === undefined)
150-
// e.g. a{,5}
151-
return P.string(',')
152-
.andThen(_ => P.decimal)
153-
.map(max => inner => AST.repeat(inner, { max }))
154-
else
155-
return P.optional(P.string(',')).andThen(comma => {
156-
if (comma === undefined)
157-
// e.g. a{3}
158-
return P.pure(inner => AST.repeat(inner, min))
159-
else
160-
return P.optional(P.decimal).map(max => inner => {
161-
if (max === undefined)
162-
// e.g. a{3,}
163-
return AST.repeat(inner, { min })
164-
else
165-
// e.g. a{3,5}
166-
return AST.repeat(inner, { min, max })
167-
})
168-
})
169-
})
150+
P.decimal.andThen(min =>
151+
P.optional(P.string(',')).andThen(comma => {
152+
if (comma === undefined)
153+
// e.g. a{3}
154+
return P.pure(inner => AST.repeat(inner, min))
155+
else
156+
return P.optional(P.decimal).map(max => inner => {
157+
if (max === undefined)
158+
// e.g. a{3,}
159+
return AST.repeat(inner, { min })
160+
else
161+
// e.g. a{3,5}
162+
return AST.repeat(inner, { min, max })
163+
})
164+
})
165+
)
170166
)
171167
)
172168

test/regex-parser.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,11 @@ describe('parseRegExp', () => {
3838
// bounded quantifier:
3939
[/a{3}/, AST.repeat(char('a'), 3)],
4040
[/a{3,}/, AST.repeat(char('a'), { min: 3 })],
41-
[/a{,5}/, AST.repeat(char('a'), { max: 5 })],
4241
[/a{3,5}/, AST.repeat(char('a'), { min: 3, max: 5 })],
4342
// if curly bracket is not terminated the whole thing is interpreted literally:
4443
[/a{3,5/, str('a{3,5')],
44+
// same if max value is given but min value is missing:
45+
[/a{,5}/, str('a{,5}')],
4546
// char classes / escaping:
4647
[/\w/, AST.literal(CharSet.wordChars)],
4748
[/\W/, AST.literal(CharSet.nonWordChars)],

0 commit comments

Comments
 (0)