Skip to content

Commit 2e69487

Browse files
committed
add error recovery at block level
1 parent 3b1399b commit 2e69487

10 files changed

Lines changed: 443 additions & 271 deletions

File tree

src/Terrabuild.Common/Errors.fs

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
module Errors
22
open System
33
open System.Runtime.ExceptionServices
4+
open System.Collections.Generic
5+
open System.Threading
46

57
[<RequireQualifiedAccess>]
68
type ErrorArea =
@@ -16,6 +18,52 @@ type TerrabuildException(msg, area, ?innerException: Exception) =
1618
inherit Exception(msg, innerException |> Option.toObj)
1719
member _.Area: ErrorArea = area
1820

21+
type private ParseErrorCollector =
22+
{ Errors: ResizeArray<TerrabuildException>
23+
PosProvider: (unit -> (int * int) option) option }
24+
25+
let private parseErrorCollector = AsyncLocal<ParseErrorCollector option>()
26+
27+
let beginParseErrorCollection (posProvider: unit -> (int * int) option) =
28+
parseErrorCollector.Value <- Some { Errors = ResizeArray(); PosProvider = Some posProvider }
29+
30+
let endParseErrorCollection () =
31+
let errors =
32+
match parseErrorCollector.Value with
33+
| Some collector -> collector.Errors |> Seq.toList
34+
| None -> []
35+
36+
parseErrorCollector.Value <- None
37+
errors
38+
39+
let private formatParseError (msg: string) (pos: (int * int) option) =
40+
if msg.StartsWith("Parse error at", StringComparison.Ordinal) then
41+
msg
42+
else
43+
match pos with
44+
| Some (line, col) -> sprintf "Parse error at (%d,%d): %s" line col msg
45+
| None -> msg
46+
47+
let private tryCollectParseError (msg: string) (inner: Exception option) (pos: (int * int) option) =
48+
match parseErrorCollector.Value with
49+
| Some collector ->
50+
let pos =
51+
match pos with
52+
| Some _ -> pos
53+
| None ->
54+
collector.PosProvider
55+
|> Option.bind (fun provider -> provider())
56+
57+
let fullMsg = formatParseError msg pos
58+
let ex =
59+
match inner with
60+
| Some inner -> TerrabuildException(fullMsg, ErrorArea.Parse, inner)
61+
| None -> TerrabuildException(fullMsg, ErrorArea.Parse)
62+
63+
collector.Errors.Add(ex)
64+
true
65+
| None -> false
66+
1967

2068
let raiseInvalidArg(msg) =
2169
TerrabuildException(msg, ErrorArea.InvalidArg) |> raise
@@ -24,10 +72,23 @@ let forwardInvalidArg(msg, innerException) =
2472
TerrabuildException(msg, ErrorArea.InvalidArg, innerException) |> raise
2573

2674
let raiseParseError(msg) =
27-
TerrabuildException(msg, ErrorArea.Parse) |> raise
75+
if not (tryCollectParseError msg None None) then
76+
TerrabuildException(msg, ErrorArea.Parse) |> raise
77+
Unchecked.defaultof<'T>
2878

2979
let forwardParseError(msg, innerException) =
30-
TerrabuildException(msg, ErrorArea.Parse, innerException) |> raise
80+
if not (tryCollectParseError msg (Some innerException) None) then
81+
TerrabuildException(msg, ErrorArea.Parse, innerException) |> raise
82+
Unchecked.defaultof<'T>
83+
84+
let reportParseError(msg) =
85+
if not (tryCollectParseError msg None None) then
86+
TerrabuildException(msg, ErrorArea.Parse) |> raise
87+
88+
let reportParseErrorAt(line, col, msg) =
89+
if not (tryCollectParseError msg None (Some (line, col))) then
90+
let fullMsg = formatParseError msg (Some (line, col))
91+
TerrabuildException(fullMsg, ErrorArea.Parse) |> raise
3192

3293
let raiseTypeError(msg) =
3394
TerrabuildException(msg, ErrorArea.Type) |> raise
@@ -74,5 +135,3 @@ let tryInvoke action =
74135
None
75136
with
76137
exn -> ExceptionDispatchInfo.Capture(exn) |> Some
77-
78-

src/Terrabuild.Lang.Tests/Lang.fs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,15 @@ let invalidScopeIdentifier() =
140140
let invalidScopedIdentifier() =
141141
let content = File.ReadAllText("TestFiles/Error_InvalidScopedIdentifier")
142142
(fun () -> FrontEnd.parse content |> ignore) |> should (throwWithMessage "Parse error at (2,21): invalid resource identifier '@value'") typeof<Errors.TerrabuildException>
143+
144+
[<Test>]
145+
let errorRecoveryCollectsMultipleErrors() =
146+
let content = File.ReadAllText("TestFiles/Error_RecoveryMultiple")
147+
try
148+
FrontEnd.parse content |> ignore
149+
Assert.Fail("Expected parse errors")
150+
with
151+
| :? Errors.TerrabuildException as ex ->
152+
ex.Message |> should contain "Parse errors:"
153+
ex.Message |> should contain "Parse error at (2,"
154+
ex.Message |> should contain "invalid attribute name '^badattr'"
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
badblock {
2+
attribute1 "missing_equal"
3+
}
4+
5+
goodblock {
6+
^badattr = 1
7+
}

src/Terrabuild.Lang/AST.fs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ with
1212

1313
static member Append (attributes: Attribute list) (attribute: Attribute) =
1414
if attributes |> List.exists (fun a -> a.Name = attribute.Name) then
15-
Errors.raiseParseError $"duplicated attribute '{attribute.Name}'"
15+
Errors.reportParseError $"duplicated attribute '{attribute.Name}'"
16+
attributes
1617
else
1718
attributes @ [attribute]
1819

@@ -35,4 +36,3 @@ type [<RequireQualifiedAccess>] File =
3536
with
3637
static member Build blocks =
3738
{ File.Blocks = blocks }
38-

src/Terrabuild.Lang/FrontEnd.fs

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,32 @@ let parse txt =
2121
token
2222

2323
let lexbuf = LexBuffer<_>.FromString txt
24+
beginParseErrorCollection (fun () -> Some (lexbuf.StartPos.Line + 1, lexbuf.StartPos.Column + 1))
25+
let mutable result = Unchecked.defaultof<_>
26+
let mutable fatalException: exn option = None
2427
try
25-
Parser.File switchableLexer lexbuf
28+
result <- Parser.File switchableLexer lexbuf
2629
with
2730
| :? TerrabuildException as exn ->
28-
let err = sprintf "Parse error at (%d,%d): %s"
29-
(lexbuf.StartPos.Line + 1) (lexbuf.StartPos.Column + 1)
30-
exn.Message
31-
forwardParseError(err, exn)
31+
fatalException <- Some exn
32+
reportParseError exn.Message
3233
| exn ->
33-
let err = sprintf "Unexpected token '%s' at (%d,%d): %s"
34-
(LexBuffer<_>.LexemeString lexbuf |> string)
35-
(lexbuf.StartPos.Line + 1) (lexbuf.StartPos.Column + 1)
34+
fatalException <- Some exn
35+
let err = sprintf "Unexpected token '%s': %s"
36+
(LexBuffer<_>.LexemeString lexbuf |> string)
3637
exn.Message
37-
forwardParseError(err, exn)
38+
reportParseError err
39+
40+
let errors = endParseErrorCollection()
41+
match errors with
42+
| [] ->
43+
match fatalException with
44+
| Some exn -> raise exn
45+
| None -> result
46+
| [single] ->
47+
TerrabuildException(single.Message, ErrorArea.Parse, single.InnerException) |> raise
48+
| errors ->
49+
let msg =
50+
"Parse errors:\n"
51+
+ (errors |> List.map (fun e -> e.Message) |> String.concat "\n")
52+
TerrabuildException(msg, ErrorArea.Parse) |> raise

src/Terrabuild.Lang/Gen/Lexer.fs

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -370,84 +370,91 @@ and token lexerMode lexbuf =
370370
)
371371
| 32 -> (
372372
# 99 "Lexer.fsl"
373-
Errors.raiseParseError $"unrecognized input: '{lexeme lexbuf}'"
374-
# 374 "Gen/Lexer.fs"
373+
374+
Errors.reportParseError $"unrecognized input: '{lexeme lexbuf}'"
375+
token lexerMode lexbuf
376+
377+
# 377 "Gen/Lexer.fs"
375378
)
376379
| _ -> failwith "token"
377380
// Rule singleLineComment
378381
and singleLineComment lexerMode lexbuf =
379382
match _fslex_tables.Interpret(13,lexbuf) with
380383
| 0 -> (
381-
# 102 "Lexer.fsl"
384+
# 105 "Lexer.fsl"
382385
lexbuf.EndPos <- lexbuf.EndPos.NextLine; token lexerMode lexbuf
383-
# 383 "Gen/Lexer.fs"
386+
# 386 "Gen/Lexer.fs"
384387
)
385388
| 1 -> (
386-
# 103 "Lexer.fsl"
389+
# 106 "Lexer.fsl"
387390
EOF
388-
# 388 "Gen/Lexer.fs"
391+
# 391 "Gen/Lexer.fs"
389392
)
390393
| 2 -> (
391-
# 104 "Lexer.fsl"
394+
# 107 "Lexer.fsl"
392395
singleLineComment lexerMode lexbuf
393-
# 393 "Gen/Lexer.fs"
396+
# 396 "Gen/Lexer.fs"
394397
)
395398
| _ -> failwith "singleLineComment"
396399
// Rule interpolatedString
397400
and interpolatedString (acc: StringBuilder) lexerMode lexbuf =
398401
match _fslex_tables.Interpret(0,lexbuf) with
399402
| 0 -> (
400-
# 107 "Lexer.fsl"
401-
raiseParseError "newline encountered in string"
402-
# 402 "Gen/Lexer.fs"
403+
# 110 "Lexer.fsl"
404+
405+
Errors.reportParseError "newline encountered in string"
406+
lexbuf.EndPos <- lexbuf.EndPos.NextLine
407+
interpolatedString acc lexerMode lexbuf
408+
409+
# 409 "Gen/Lexer.fs"
403410
)
404411
| 1 -> (
405-
# 108 "Lexer.fsl"
412+
# 115 "Lexer.fsl"
406413

407414
acc.Append("\"") |> ignore
408415
interpolatedString acc lexerMode lexbuf
409416

410-
# 410 "Gen/Lexer.fs"
417+
# 417 "Gen/Lexer.fs"
411418
)
412419
| 2 -> (
413-
# 112 "Lexer.fsl"
420+
# 119 "Lexer.fsl"
414421

415422
acc.Append("{") |> ignore
416423
interpolatedString acc lexerMode lexbuf
417424

418-
# 418 "Gen/Lexer.fs"
425+
# 425 "Gen/Lexer.fs"
419426
)
420427
| 3 -> (
421-
# 116 "Lexer.fsl"
428+
# 123 "Lexer.fsl"
422429

423430
acc.Append("}") |> ignore
424431
interpolatedString acc lexerMode lexbuf
425432

426-
# 426 "Gen/Lexer.fs"
433+
# 433 "Gen/Lexer.fs"
427434
)
428435
| 4 -> (
429-
# 120 "Lexer.fsl"
436+
# 127 "Lexer.fsl"
430437

431438
lexerMode |> pop |> ignore
432439
STRING_END (acc.ToString())
433440

434-
# 434 "Gen/Lexer.fs"
441+
# 441 "Gen/Lexer.fs"
435442
)
436443
| 5 -> (
437-
# 124 "Lexer.fsl"
444+
# 131 "Lexer.fsl"
438445

439446
lexerMode |> push LexerMode.Default
440447
EXPRESSION_START (acc.ToString())
441448

442-
# 442 "Gen/Lexer.fs"
449+
# 449 "Gen/Lexer.fs"
443450
)
444451
| 6 -> (
445-
# 128 "Lexer.fsl"
452+
# 135 "Lexer.fsl"
446453

447454
lexbuf |> lexeme |> acc.Append |> ignore
448455
interpolatedString acc lexerMode lexbuf
449456

450-
# 450 "Gen/Lexer.fs"
457+
# 457 "Gen/Lexer.fs"
451458
)
452459
| _ -> failwith "interpolatedString"
453460

0 commit comments

Comments
 (0)