Skip to content

Commit adf4e97

Browse files
committed
Diagnose unbalanced closing ')'s
Previously we just silently stopped parsing at these due to them being a termination condition for recursive parsing. Pick them up and diagnose when not doing a recursive parse.
1 parent 2098c07 commit adf4e97

File tree

3 files changed

+30
-6
lines changed

3 files changed

+30
-6
lines changed

Sources/_MatchingEngine/Regex/Parse/Diagnostics.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,11 @@ enum ParseError: Error, Hashable {
5555
case emptyProperty
5656

5757
case expectedGroupSpecifier
58+
case unbalancedEndOfGroup
5859
case expectedGroupName
5960
case groupNameMustBeAlphaNumeric
6061
case groupNameCannotStartWithNumber
62+
6163
case cannotRemoveTextSegmentOptions
6264
}
6365

@@ -116,6 +118,8 @@ extension ParseError: CustomStringConvertible {
116118
return "empty property"
117119
case .expectedGroupSpecifier:
118120
return "expected group specifier"
121+
case .unbalancedEndOfGroup:
122+
return "closing ')' does not balance any groups openings"
119123
case .expectedGroupName:
120124
return "expected group name"
121125
case .groupNameMustBeAlphaNumeric:

Sources/_MatchingEngine/Regex/Parse/Parse.swift

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,29 @@ extension Parser {
114114
}
115115

116116
extension Parser {
117-
/// Parse a regular expression
117+
/// Parse a top-level regular expression. Do not use for recursive calls, use
118+
/// `parseNode()` instead.
118119
///
119-
/// Regex -> '' | Alternation
120-
/// Alternation -> Concatenation ('|' Concatenation)*
120+
/// Regex -> RegexNode
121121
///
122122
mutating func parse() throws -> AST {
123+
let ast = try parseNode()
124+
guard source.isEmpty else {
125+
if let loc = source.tryEatWithLoc(")") {
126+
throw Source.LocatedError(ParseError.unbalancedEndOfGroup, loc)
127+
}
128+
fatalError("Unhandled termination condition")
129+
}
130+
return ast
131+
}
132+
133+
/// Parse a regular expression node. This should be used instead of `parse()`
134+
/// for recursive calls.
135+
///
136+
/// RegexNode -> '' | Alternation
137+
/// Alternation -> Concatenation ('|' Concatenation)*
138+
///
139+
mutating func parseNode() throws -> AST {
123140
let _start = source.currentPosition
124141

125142
if source.isEmpty { return .empty(.init(loc(_start))) }
@@ -203,7 +220,7 @@ extension Parser {
203220
mutating func parseConditionalBranches(
204221
start: Source.Position, _ cond: AST.Conditional.Condition
205222
) throws -> AST {
206-
let child = try parse()
223+
let child = try parseNode()
207224
let trueBranch: AST, falseBranch: AST, pipe: SourceLocation?
208225
switch child {
209226
case .alternation(let a):
@@ -237,7 +254,7 @@ extension Parser {
237254
) throws -> AST.Group {
238255
context.recordGroup(kind.value)
239256

240-
let child = try parse()
257+
let child = try parseNode()
241258
// An implicit scoped group has already consumed its closing paren.
242259
if !kind.value.hasImplicitScope {
243260
try source.expect(")")

Tests/RegexTests/ParseTests.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1312,9 +1312,12 @@ extension RegexTests {
13121312
}
13131313

13141314
func testParseErrors() {
1315-
// MARK: Closing delimiters.
1315+
// MARK: Unbalanced delimiters.
13161316

13171317
diagnosticTest("(", .expected(")"))
1318+
diagnosticTest(")", .unbalancedEndOfGroup)
1319+
diagnosticTest(")))", .unbalancedEndOfGroup)
1320+
diagnosticTest("())()", .unbalancedEndOfGroup)
13181321

13191322
diagnosticTest(#"\u{5"#, .expected("}"))
13201323
diagnosticTest(#"\x{5"#, .expected("}"))

0 commit comments

Comments
 (0)