Browse Source

[wip] Improve parser error handling (#7912)

* [parser] start on parser error refactoring

* [parser] remove all `in_display` checks from grammar for now

* [parser] remove Parser.display_error

* [parser] start on parse_result

* [parser] remove some hard errors

* [tests] add test for broken syntax diagnostics

see #7793

* [parser] unbreak eval-debugger completion

* [parser] adjust Context.parse(InlineString)

* [parser] adjust `parse_metadata`

* [parser] adjust type_patch parsing

* [parser] adjust import.hx parsing

* [parser] adjust module parsing

* [parser] fix special } resume case

* [parser] handle module parsing errors properly

* [macro] fix bad macro display settings handling

* [tests] add toString to HaxeInvocationException

and hope that it gets called

* [submodule] try something

* [parser] fix syntax_errors handling

* [server] remove unused binding

* [parser] don't lose exact interpolation parser message

* [tests] remove unrelated change
Simon Krajewski 6 years ago
parent
commit
ef34514304

+ 1 - 1
libs

@@ -1 +1 @@
-Subproject commit 101516896900a046e19e32737b9a09a870da2f94
+Subproject commit d004775e9c6b80b5d85ac182d549d050397857f5

+ 0 - 1
src/compiler/main.ml

@@ -483,7 +483,6 @@ try
 	com.warning <- (fun msg p -> message ctx (CMWarning(msg,p)));
 	com.error <- error ctx;
 	if CompilationServer.runs() then com.run_command <- run_command ctx;
-	Parser.display_error := (fun e p -> com.error (Parser.error_msg e) p);
 	com.class_path <- get_std_class_paths ();
 	com.std_path <- List.filter (fun p -> ExtString.String.ends_with p "std/" || ExtString.String.ends_with p "std\\") com.class_path;
 	let define f = Arg.Unit (fun () -> Common.define com f) in

+ 18 - 18
src/compiler/server.ml

@@ -109,7 +109,6 @@ let ssend sock str =
 let rec wait_loop process_params verbose accept =
 	if verbose then ServerMessage.enable_all ();
 	Sys.catch_break false; (* Sys can never catch a break *)
-	let has_parse_error = ref false in
 	let cs = CompilationServer.create () in
 	MacroContext.macro_enable_cache := true;
 	let current_stdin = ref None in
@@ -128,24 +127,26 @@ let rec wait_loop process_params verbose accept =
 				try
 					let cfile = CompilationServer.find_file cs fkey in
 					if cfile.c_time <> ftime then raise Not_found;
-					cfile.c_package,cfile.c_decls
+					Parser.ParseSuccess(cfile.c_package,cfile.c_decls)
 				with Not_found ->
-					has_parse_error := false;
-					let data = TypeloadParse.parse_file com2 file p in
-					let info,is_unusual = if !has_parse_error then "not cached, has parse error",true
-						else if is_display_file then "not cached, is display file",true
-						else begin try
-							(* We assume that when not in display mode it's okay to cache stuff that has #if display
-							   checks. The reasoning is that non-display mode has more information than display mode. *)
-							if not com2.display.dms_display then raise Not_found;
-							let ident = Hashtbl.find Parser.special_identifier_files ffile in
-							Printf.sprintf "not cached, using \"%s\" define" ident,true
-						with Not_found ->
-							CompilationServer.cache_file cs fkey ftime data;
-							"cached",false
-					end in
+					let parse_result = TypeloadParse.parse_file com2 file p in
+					let info,is_unusual = match parse_result with
+						| ParseError(_,_,_) -> "not cached, has parse error",true
+						| ParseDisplayFile _ -> "not cached, is display file",true
+						| ParseSuccess data ->
+							begin try
+								(* We assume that when not in display mode it's okay to cache stuff that has #if display
+								checks. The reasoning is that non-display mode has more information than display mode. *)
+								if not com2.display.dms_display then raise Not_found;
+								let ident = Hashtbl.find Parser.special_identifier_files ffile in
+								Printf.sprintf "not cached, using \"%s\" define" ident,true
+							with Not_found ->
+								CompilationServer.cache_file cs fkey ftime data;
+								"cached",false
+							end
+					in
 					if is_unusual then ServerMessage.parsed com2 "" (ffile,info);
-					data
+					parse_result
 			) () in
 			data
 	);
@@ -416,7 +417,6 @@ let rec wait_loop process_params verbose accept =
 				ServerMessage.defines ctx.com "";
 				ServerMessage.signature ctx.com "" sign;
 				ServerMessage.display_position ctx.com "" (!DisplayPosition.display_position);
-				Parser.display_error := (fun e p -> has_parse_error := true; ctx.com.error (Parser.error_msg e) p);
 				(* Special case for diagnostics: It's not treated as a display mode, but we still want to invalidate the
 				   current file in order to run diagnostics on it again. *)
 				if ctx.com.display.dms_display || (match ctx.com.display.dms_kind with DMDiagnostics _ -> true | _ -> false) then begin

+ 3 - 1
src/macro/eval/evalDebugMisc.ml

@@ -80,7 +80,9 @@ exception Parse_expr_error of string
 
 let parse_expr ctx s p =
 	let error s = raise (Parse_expr_error s) in
-	ParserEntry.parse_expr_string (ctx.curapi.get_com()).Common.defines s p error true
+	match ParserEntry.parse_expr_string (ctx.curapi.get_com()).Common.defines s p error true with
+	| ParseSuccess data | ParseDisplayFile(data,_) -> data
+	| ParseError(_,(msg,_),_) -> error (Parser.error_msg msg)
 
 (* Vars *)
 

+ 77 - 106
src/syntax/grammar.mly

@@ -45,9 +45,12 @@ let pignore f =
 	with Stream.Error _ | Stream.Failure ->
 		()
 
-let expect_unless_resume_p f = parser
-	| [< p = f >] -> p
-	| [< s >] -> if !in_display then pos (next_token s) else serror()
+let expect_unless_resume_p t s = match Stream.peek s with
+	| Some (t',p) when t' = t ->
+		Stream.junk s;
+		p
+	| _ ->
+		syntax_error (Expected [s_token t]) s (next_pos s)
 
 let ident = parser
 	| [< '(Const (Ident i),p) >] -> i,p
@@ -83,24 +86,9 @@ let questionable_dollar_ident s =
 		| None ->
 			false,(name,p)
 		| Some p' ->
-			if p.pmin <> p'.pmax then error (Custom (Printf.sprintf "Invalid usage of ?, use ?%s instead" name)) p';
+			if p.pmin <> p'.pmax then syntax_error (Custom (Printf.sprintf "Invalid usage of ?, use ?%s instead" name)) s ~pos:(Some p') ();
 			true,(name,p)
 
-let bropen = parser
-	| [< '(BrOpen,_) >] -> ()
-
-let comma = parser
-	| [< '(Comma,_) >] -> ()
-
-let colon = parser
-	| [< '(DblDot,p) >] -> p
-
-let pclose = parser
-	| [< '(PClose,p) >] -> p
-
-let bkclose = parser
-	| [< '(BkClose,p) >] -> p
-
 let question_mark = parser
 	| [< '(Question,p) >] -> p
 
@@ -113,8 +101,7 @@ let semicolon s =
 		match s with parser
 		| [< '(Semicolon,p) >] -> p
 		| [< s >] ->
-			let pos = snd (last_token s) in
-			if !in_display then pos else error Missing_semicolon pos
+			syntax_error Missing_semicolon s (next_pos s)
 
 let parsing_macro_cond = ref false
 
@@ -151,15 +138,17 @@ and parse_type_decls mode pmax pack acc s =
 			| _ -> ()
 		) acc;
 		raise (TypePath (pack,Some(name,true),b,p))
-	| Stream.Error _ when !in_display ->
-		ignore(resume false false s);
-		parse_type_decls mode (last_pos s).pmax pack acc s
+	| Stream.Error msg when !in_display_file ->
+		syntax_error (StreamError msg) s (
+			ignore(resume false false s);
+			parse_type_decls mode (last_pos s).pmax pack acc s
+		)
 
 and parse_abstract doc meta flags = parser
 	| [< '(Kwd Abstract,p1); name = type_name; tl = parse_constraint_params; st = parse_abstract_subtype; sl = plist parse_abstract_relations; s >] ->
 		let fl,p2 = match s with parser
 			| [< '(BrOpen,_); fl, p2 = parse_class_fields false p1 >] -> fl,p2
-			| [< >] -> if !in_display then [],last_pos s else serror()
+			| [< >] -> syntax_error (Expected ["{"]) s ([],last_pos s)
 		in
 		let flags = List.map decl_flag_to_abstract_flag flags in
 		let flags = (match st with None -> flags | Some t -> AbOver t :: flags) in
@@ -208,10 +197,8 @@ and parse_type_decl mode s =
 					check_display p0 {p1 with pmin = p0.pmax; pmax = p1.pmin};
 					List.rev acc
 				| [< >] ->
-					if not (!in_display) then serror() else begin
-						check_display p0 {p1 with pmin = p0.pmax; pmax = (next_pos s).pmax};
-						List.rev acc
-					end
+					check_display p0 {p1 with pmin = p0.pmax; pmax = (next_pos s).pmax};
+					syntax_error (Expected ["extends";"implements";"{"]) s (List.rev acc)
 			in
 			let hl = loop false (last_pos s) [] in
 			let fl, p2 = parse_class_fields false p1 s in
@@ -272,10 +259,8 @@ and parse_import s p1 =
 			| [< '(Binop OpMult,_); '(Semicolon,p2) >] ->
 				p2, List.rev acc, IAll
 			| [< >] ->
-				if !in_display then begin
-					ignore(popt semicolon s);
-					p,List.rev acc,INormal
-				end else serror()
+				ignore(popt semicolon s);
+				syntax_error (Expected ["identifier"]) s (p,List.rev acc,INormal)
 			end
 		| [< '(Semicolon,p2) >] ->
 			p2, List.rev acc, INormal
@@ -284,11 +269,11 @@ and parse_import s p1 =
 		| [< '(Const (Ident "as"),_); '(Const (Ident name),pname); '(Semicolon,p2) >] ->
 			p2, List.rev acc, IAsName(name,pname)
 		| [< >] ->
-			if !in_display then (last_pos s),List.rev acc,INormal else serror()
+			syntax_error (Expected [".";";";"as"]) s ((last_pos s),List.rev acc,INormal)
 	in
 	let p2, path, mode = (match s with parser
 		| [< '(Const (Ident name),p) >] -> loop p [name,p]
-		| [< >] -> if would_skip_display_position p1 s then p1, [], INormal else serror()
+		| [< >] -> syntax_error (Expected ["identifier"]) s (p1,[],INormal)
 	) in
 	(EImport (path,mode),punion p1 p2)
 
@@ -305,16 +290,16 @@ and parse_using s p1 =
 			| [< '(Kwd Extern,p) >] ->
 				loop pn (("extern",p) :: acc)
 			| [< >] ->
-				if !in_display then p,List.rev acc else serror()
+				syntax_error (Expected ["identifier"]) s (p,List.rev acc);
 			end
 		| [< '(Semicolon,p2) >] ->
 			p2,List.rev acc
 		| [< >] ->
-			if !in_display then (last_pos s),List.rev acc else serror()
+			syntax_error (Expected [".";";"]) s ((last_pos s),List.rev acc)
 	in
 	let p2, path = (match s with parser
 		| [< '(Const (Ident name),p) >] -> loop p [name,p]
-		| [< >] -> if would_skip_display_position p1 s then p1, [] else serror()
+		| [< >] -> syntax_error (Expected ["identifier"]) s (p1,[])
 	) in
 	(EUsing path,punion p1 p2)
 
@@ -342,7 +327,9 @@ and parse_class_fields tdecl p1 s =
 	let l = parse_class_field_resume tdecl s in
 	let p2 = (match s with parser
 		| [< '(BrClose,p2) >] -> p2
-		| [< >] -> if !in_display then pos (last_token s) else serror()
+		| [< >] ->
+			(* We don't want to register this as a syntax error because it's part of the logic in display mode *)
+			if !in_display then (pos (last_token s)) else error (Expected ["}"]) (next_pos s)
 	) in
 	l, p2
 
@@ -409,7 +396,7 @@ and resume tdecl fdecl s =
 	loop 1
 
 and parse_class_field_resume tdecl s =
-	if not (!in_display) then
+	if not (!in_display_file) then
 		plist (parse_class_field tdecl) s
 	else try
 		let c = parse_class_field tdecl s in
@@ -438,7 +425,7 @@ and parse_meta_argument_expr s =
 
 and parse_meta_params pname s = match s with parser
 	| [< '(POpen,p) when p.pmin = pname.pmax; params = psep Comma parse_meta_argument_expr; >] ->
-		ignore(expect_unless_resume_p pclose s);
+		ignore(expect_unless_resume_p PClose s);
 		params
 	| [< >] -> []
 
@@ -525,7 +512,7 @@ and parse_structural_extension = parser
 		| [< t = parse_type_path >] ->
 			begin match s with parser
 				| [< '(Comma,_) >] -> t
-				| [< >] -> if !in_display then t else serror()
+				| [< >] -> syntax_error (Expected [","]) s t
 			end;
 		| [< >] ->
 			if would_skip_display_position p1 s then begin
@@ -594,8 +581,7 @@ and parse_type_path2 p0 pack name p1 s =
 				begin match s with parser
 				| [<'(Binop OpGt,p2) >] -> l,p2
 				| [< >] ->
-					if !in_display then l,pos (last_token s)
-					else serror()
+					syntax_error (Expected [">"]) s (l,pos (last_token s))
 				end
 			| [< >] -> [],p2
 		) in
@@ -607,9 +593,9 @@ and parse_type_path2 p0 pack name p1 s =
 		},punion (match p0 with None -> p1 | Some p -> p) p2
 
 and type_name = parser
-	| [< '(Const (Ident name),p) >] ->
+	| [< '(Const (Ident name),p); s >] ->
 		if is_lower_ident name then
-			error (Custom "Type name should start with an uppercase letter") p
+			syntax_error (Custom "Type name should start with an uppercase letter") ~pos:(Some p) s (name,p)
 		else
 			name,p
 	| [< '(Dollar name,p) >] -> "$" ^ name,p
@@ -622,7 +608,7 @@ and parse_type_path_or_const = parser
 	| [< '(Kwd True,p) >] -> TPExpr (EConst (Ident "true"),p)
 	| [< '(Kwd False,p) >] -> TPExpr (EConst (Ident "false"),p)
 	| [< e = expr >] -> TPExpr e
-	| [< >] -> if !in_display then raise Stream.Failure else serror()
+	| [< >] -> if !in_display_file then raise Stream.Failure else serror()
 
 and parse_complex_type_next (t : type_hint) s =
 	let make_fun t2 p2 = match t2 with
@@ -696,8 +682,7 @@ and parse_type_anonymous s =
 			| [< l,p2 = parse_type_anonymous >] -> next l,punion p1 p2
 			| [< >] -> serror());
 		| [< >] ->
-			if !in_display then next [],p2
-			else serror()
+			syntax_error (Expected [",";"}"]) s (next [],p2)
 		end
 	| [< >] ->
 		if p0 = None then raise Stream.Failure else serror()
@@ -734,7 +719,7 @@ and parse_function_field doc meta al = parser
 	| [< '(Kwd Function,p1); name = parse_fun_name; pl = parse_constraint_params; '(POpen,_); args = psep Comma parse_fun_param; '(PClose,_); t = popt parse_type_hint; s >] ->
 		let e, p2 = (match s with parser
 			| [< e = expr; s >] ->
-				(try ignore(semicolon s) with Error (Missing_semicolon,p) -> !display_error Missing_semicolon p);
+				ignore(semicolon s);
 				Some e, pos e
 			| [< p = semicolon >] -> None, p
 			| [< >] -> serror()
@@ -770,7 +755,7 @@ and parse_class_field tdecl s =
 	| [< meta = parse_meta; al = plist parse_cf_rights; s >] ->
 		let check_optional opt name =
 			if opt then begin
-				if not tdecl then error (Custom "?var syntax is only allowed in structures") (pos name);
+				if not tdecl then syntax_error (Custom "?var syntax is only allowed in structures") ~pos:(Some (pos name)) s ();
 				(Meta.Optional,[],null_pos) :: meta
 			end else
 				meta
@@ -935,12 +920,9 @@ and block_with_pos' acc f p s =
 	with
 		| Stream.Failure ->
 			List.rev acc,p
-		| Stream.Error _ when !in_display ->
-			let tk , pos = next_token s in
-			(!display_error) (Unexpected tk) pos;
-			block_with_pos acc pos s
-		| Error (e,p) ->
-			(!display_error) e p;
+		| Stream.Error msg when !in_display_file ->
+			syntax_error (StreamError msg) s (block_with_pos acc (next_pos s) s)
+		| Error (e,p) when !in_display_file ->
 			block_with_pos acc p s
 
 and block_with_pos acc p s =
@@ -981,8 +963,7 @@ and parse_obj_decl name e p0 s =
 				| [< '(DblDot,_) >] ->
 					next_expr key
 				| [< >] ->
-					if !in_display then next_expr key
-					else serror()
+					syntax_error (Expected [":"]) s (next_expr key)
 			in
 			begin match s with parser
 				| [< name,p = ident >] -> next (name,p,NoQuotes)
@@ -997,10 +978,10 @@ and parse_obj_decl name e p0 s =
 
 and parse_array_decl p1 s =
 	let resume_or_fail p1 =
-		if !in_display then begin
+		syntax_error (Expected ["expr";"]"]) s (
 			let p = punion_next p1 s in
 			[mk_null_expr p],p
-		end else serror()
+		)
 	in
 	let el,p2 = match s with parser
 		| [< '(BkClose,p2) >] -> [],p2
@@ -1011,11 +992,11 @@ and parse_array_decl p1 s =
 						| [< '(BkClose,p2) >] -> acc,p2
 						| [< e = secure_expr >] -> loop (e :: acc)
 						| [< >] ->
-							if !in_display then acc,pk else serror()
+							syntax_error (Expected ["expr";"]"]) s (acc,pk)
 					end
 				| [< '(BkClose,p2) >] -> acc,p2
 				| [< >] ->
-					if !in_display then acc,next_pos s else serror()
+					syntax_error (Expected [",";"]"]) s (acc,next_pos s)
 			in
 			loop [e0]
 		| [< >] -> resume_or_fail p1
@@ -1027,7 +1008,7 @@ and parse_var_decl_head final = parser
 
 and parse_var_assignment = parser
 	| [< '(Binop OpAssign,p1); s >] ->
-		Some (expr_or_fail (fun () -> error (Custom "expression expected after =") p1) s)
+		Some (secure_expr s)
 	| [< >] -> None
 
 and parse_var_assignment_resume final vl name pn t s =
@@ -1099,10 +1080,10 @@ and arrow_ident_checktype e = (match e with
 	| ECheckType((EConst(Ident n),p),(t,pt)),_ -> (n,p),(Some (t,pt))
 	| _ -> serror())
 
-and arrow_first_param e =
+and arrow_first_param e s =
 	(match fst e with
 	| EConst(Ident ("true" | "false" | "null" | "this" | "super")) ->
-		error (Custom "Invalid argument name") (pos e)
+		syntax_error (Custom "Invalid argument name") ~pos:(Some (pos e)) s (("",null_pos),false,[],None,None)
 	| EConst(Ident n) ->
 		(n,snd e),false,[],None,None
 	| EBinop(OpAssign,e1,e2)
@@ -1131,7 +1112,7 @@ and expr = parser
 				| [< '(BrClose,p2) >] -> p2
 				| [< >] ->
 					(* Ignore missing } if we are resuming and "guess" the last position. *)
-					if !in_display then pos (next_token s) else serror()
+					syntax_error (Expected ["}"]) s (pos (next_token s))
 			in
 			let e = (b,punion p1 p2) in
 			(match b with
@@ -1176,8 +1157,7 @@ and expr = parser
 		begin match s with parser
 		| [< '(POpen,po); e = parse_call_params (fun el p2 -> (ENew(t,el)),punion p1 p2) po >] -> expr_next e s
 		| [< >] ->
-			if !in_display then (ENew(t,[]),punion p1 (pos t))
-			else serror()
+			syntax_error (Expected ["("]) s (ENew(t,[]),punion p1 (pos t))
 		end
 	| [< '(POpen,p1); s >] -> (match s with parser
 		| [< '(PClose,p2); er = arrow_expr; >] ->
@@ -1188,7 +1168,7 @@ and expr = parser
 		| [<  e = expr; s >] -> (match s with parser
 			| [< '(PClose,p2); s >] -> expr_next (EParenthesis e, punion p1 p2) s
 			| [< '(Comma,pc); al = psep Comma parse_fun_param; '(PClose,_); er = arrow_expr; >] ->
-				arrow_function p1 ((arrow_first_param e) :: al) er
+				arrow_function p1 ((arrow_first_param e s) :: al) er
 			| [< t,pt = parse_type_hint; s >] -> (match s with parser
 				| [< '(PClose,p2); s >] -> expr_next (EParenthesis (ECheckType(e,(t,pt)),punion p1 p2), punion p1 p2) s
 				| [< '(Comma,pc); al = psep Comma parse_fun_param; '(PClose,_); er = arrow_expr; >] ->
@@ -1209,8 +1189,7 @@ and expr = parser
 				| [< >] -> serror())
 			| [< '(Const (Ident "is"),p_is); t = parse_type_path; '(PClose,p2); >] -> expr_next (make_is e t (punion p1 p2) p_is) s
 			| [< >] ->
-				if !in_display then expr_next (EParenthesis e, punion p1 (pos e)) s
-				else serror())
+				syntax_error (Expected [")";",";":";"is"]) s (expr_next (EParenthesis e, punion p1 (pos e)) s))
 		)
 	| [< '(BkOpen,p1); e = parse_array_decl p1; s >] -> expr_next e s
 	| [< '(Kwd Function,p1); e = parse_function p1 false; >] -> e
@@ -1228,16 +1207,14 @@ and expr = parser
 		let e = match s with parser
 			| [< '(PClose,_); e = secure_expr >] -> e
 			| [< >] ->
-				if !in_display then mk_null_expr (pos it)
-				else serror()
+				syntax_error (Expected [")"]) s (mk_null_expr (pos it))
 		in
 		(EFor (it,e),punion p (pos e))
 	| [< '(Kwd If,p); '(POpen,_); cond = secure_expr; s >] ->
 		let e1 = match s with parser
 			| [< '(PClose,_); e1 = secure_expr >] -> e1
 			| [< >] ->
-				if !in_display then mk_null_expr (pos cond)
-				else serror()
+				syntax_error (Expected [")"]) s (mk_null_expr (pos cond))
 		in
 		let e2 = (match s with parser
 			| [< '(Kwd Else,_); e2 = secure_expr; s >] -> Some e2
@@ -1264,25 +1241,22 @@ and expr = parser
 		let e = match s with parser
 			| [< '(PClose,_); e = secure_expr >] -> e
 			| [< >] ->
-				if !in_display then mk_null_expr (pos cond)
-				else serror()
+				syntax_error (Expected [")"]) s (mk_null_expr (pos cond))
 		in
 		(EWhile (cond,e,NormalWhile),punion p1 (pos e))
 	| [< '(Kwd Do,p1); e = secure_expr; s >] ->
 		begin match s with parser
 			| [< '(Kwd While,_); '(POpen,_); cond = secure_expr; s >] ->
-				let p2 = expect_unless_resume_p pclose s in
+				let p2 = expect_unless_resume_p PClose s in
 				(EWhile (cond,e,DoWhile),punion p1 p2)
 			| [< >] ->
-				if !in_display then e (* ignore do *)
-				else serror()
+				syntax_error (Expected ["while"]) s e (* ignore do *)
 		end
 	| [< '(Kwd Switch,p1); e = secure_expr; s >] ->
 		begin match s with parser
 			| [< '(BrOpen,_); cases , def = parse_switch_cases e []; '(BrClose,p2); s >] -> (ESwitch (e,cases,def),punion p1 p2)
 			| [< >] ->
-				if !in_display then (ESwitch(e,[],None),punion p1 (pos e))
-				else serror()
+				syntax_error (Expected ["{"]) s (ESwitch(e,[],None),punion p1 (pos e))
 		end
 	| [< '(Kwd Try,p1); e = secure_expr; cl,p2 = parse_catches e [] (pos e) >] -> (ETry (e,cl),punion p1 p2)
 	| [< '(IntInterval i,p1); e2 = expr >] -> make_binop OpInterval (EConst (Int i),p1) e2
@@ -1293,8 +1267,8 @@ and expr = parser
 and expr_next e1 s =
 	try
 		expr_next' e1 s
-	with Stream.Error _ when !in_display ->
-		e1
+	with Stream.Error msg ->
+		syntax_error (StreamError msg) s e1
 
 and expr_next' e1 = parser
 	| [< '(BrOpen,p1) when is_dollar_ident e1; eparam = expr; '(BrClose,p2); s >] ->
@@ -1304,12 +1278,12 @@ and expr_next' e1 = parser
 	| [< '(Dot,p); e = parse_field e1 p >] -> e
 	| [< '(POpen,p1); e = parse_call_params (fun el p2 -> (ECall(e1,el)),punion (pos e1) p2) p1; s >] -> expr_next e s
 	| [< '(BkOpen,p1); e2 = secure_expr; s >] ->
-		let p2 = expect_unless_resume_p bkclose s in
+		let p2 = expect_unless_resume_p BkClose s in
 		let e2 = check_signature_mark e2 p1 p2 in
 		expr_next (EArray (e1,e2), punion (pos e1) p2) s
 	| [< '(Arrow,pa); s >] ->
 		let er = expr s in
-		arrow_function (snd e1) [arrow_first_param e1] er
+		arrow_function (snd e1) [arrow_first_param e1 s] er
 	| [< '(Binop OpGt,p1); s >] ->
 		(match s with parser
 		| [< '(Binop OpGt,p2) when p1.pmax = p2.pmin; s >] ->
@@ -1330,7 +1304,7 @@ and expr_next' e1 = parser
 	| [< '(Question,_); e2 = expr; s >] ->
 		begin match s with parser
 		| [< '(DblDot,_); e3 = expr >] -> (ETernary (e1,e2,e3),punion (pos e1) (pos e3))
-		| [< >] -> if !in_display then e2 else serror()
+		| [< >] -> syntax_error (Expected [":"]) s e2
 		end
 	| [< '(Kwd In,_); e2 = expr >] ->
 		make_binop OpIn e1 e2
@@ -1369,13 +1343,13 @@ and parse_switch_cases eswitch cases = parser
 			| _ -> let p = punion p1 p2 in Some ((EBlock b,p)),p
 		in
 		let l , def = parse_switch_cases eswitch cases s in
-		(match def with None -> () | Some _ -> error Duplicate_default p1);
+		(match def with None -> () | Some _ -> syntax_error Duplicate_default ~pos:(Some p1) s ());
 		l , Some b
 	| [< '(Kwd Case,p1); el = psep Comma expr_or_var; eg = popt parse_guard; s >] ->
-		let pdot = expect_unless_resume_p colon s in
+		let pdot = expect_unless_resume_p DblDot s in
 		if !was_auto_triggered then check_resume pdot (fun () -> ()) (fun () -> ());
 		(match el with
-		| [] -> error (Custom "case without a pattern is not allowed") p1
+		| [] -> syntax_error (Custom "case without a pattern is not allowed") ~pos:(Some p1) s ([],None)
 		| _ ->
 			let b,p2 = (block_with_pos [] p1 s) in
 			let b,p = match b with
@@ -1430,25 +1404,22 @@ and parse_call_params f p1 s =
 		| [< >] -> parse_next_param [] p1
 	end
 
-and secure_expr s =
-	expr_or_fail serror s
-
 (* Tries to parse a toplevel expression and defaults to a null expression when in display mode.
    This function always accepts in display mode and should only be used for expected expressions,
    not accepted ones! *)
-and expr_or_fail fail s =
-	match s with parser
+and secure_expr = parser
 	| [< e = expr >] -> e
-	| [< >] -> if !in_display then begin
-		let last = last_token s in
-		let plast = pos last in
-		let offset = match fst last with
-			| Const _ | Kwd _ | Dollar _ -> 1
-			| _ -> 0
-		in
-		let plast = {plast with pmin = plast.pmax + offset} in
-		mk_null_expr (punion_next plast s)
-	end else fail()
+	| [< s >] ->
+		syntax_error (Expected ["expression"]) s (
+			let last = last_token s in
+			let plast = pos last in
+			let offset = match fst last with
+				| Const _ | Kwd _ | Dollar _ -> 1
+				| _ -> 0
+			in
+			let plast = {plast with pmin = plast.pmax + offset} in
+			mk_null_expr (punion_next plast s)
+		)
 
 let rec validate_macro_cond e = match fst e with
 	| EConst (Ident _)

+ 38 - 16
src/syntax/parser.ml

@@ -30,6 +30,8 @@ type error_msg =
 	| Unclosed_macro
 	| Unimplemented
 	| Missing_type
+	| Expected of string list
+	| StreamError of string
 	| Custom of string
 
 type decl_flag =
@@ -60,13 +62,26 @@ let error_msg = function
 	| Unclosed_macro -> "Unclosed macro"
 	| Unimplemented -> "Not implemented for current platform"
 	| Missing_type -> "Missing type declaration"
+	| Expected sl -> "Expected " ^ (String.concat " or " sl)
+	| StreamError s -> s
 	| Custom s -> s
 
+type parse_data = string list * (type_def * pos) list
+
+type parse_error = (error_msg * pos)
+
+type 'a parse_result =
+	(* Parsed display file. There can be errors. *)
+	| ParseDisplayFile of 'a * parse_error list
+	(* Parsed non-display-file without errors. *)
+	| ParseSuccess of 'a
+	(* Parsed non-display file with errors *)
+	| ParseError of 'a * parse_error * parse_error list
+
 let syntax_completion kind p =
 	raise (SyntaxCompletion(kind,p))
 
 let error m p = raise (Error (m,p))
-let display_error : (error_msg -> pos -> unit) ref = ref (fun _ _ -> assert false)
 
 let special_identifier_files : (string,string) Hashtbl.t = Hashtbl.create 0
 
@@ -95,6 +110,22 @@ module TokenCache = struct
 		(fun () -> cache := old_cache)
 end
 
+let last_token s =
+	let n = Stream.count s in
+	TokenCache.get (if n = 0 then 0 else n - 1)
+
+let last_pos s = pos (last_token s)
+
+let next_token s = match Stream.peek s with
+	| Some (Eof,p) ->
+		(Eof,{p with pmax = max_int})
+	| Some tk -> tk
+	| None ->
+		let last_pos = pos (last_token s) in
+		(Eof,{last_pos with pmax = max_int})
+
+let next_pos s = pos (next_token s)
+
 (* Global state *)
 
 let in_display = ref false
@@ -119,12 +150,13 @@ let reset_state () =
 
 let in_display_file = ref false
 let last_doc : (string * int) option ref = ref None
+let syntax_errors = ref []
 
-let last_token s =
-	let n = Stream.count s in
-	TokenCache.get (if n = 0 then 0 else n - 1)
-
-let last_pos s = pos (last_token s)
+let syntax_error error_msg ?(pos=None) s v =
+	let p = (match pos with Some p -> p | None -> next_pos s) in
+	if not !in_display then error error_msg p;
+	syntax_errors := (error_msg,p) :: !syntax_errors;
+	v
 
 let get_doc s =
 	(* do the peek first to make sure we fetch the doc *)
@@ -232,16 +264,6 @@ let handle_xml_literal p1 =
 	let e = make_meta Meta.Markup [] e p1 in
 	e
 
-let next_token s = match Stream.peek s with
-	| Some (Eof,p) ->
-		(Eof,{p with pmax = max_int})
-	| Some tk -> tk
-	| None ->
-		let last_pos = pos (last_token s) in
-		(Eof,{last_pos with pmax = max_int})
-
-let next_pos s = pos (next_token s)
-
 let punion_next p1 s =
 	let _,p2 = next_token s in
 	{

+ 23 - 11
src/syntax/parserEntry.ml

@@ -81,6 +81,7 @@ let parse ctx code file =
 	code_ref := code;
 	in_display := !display_position <> null_pos;
 	in_display_file := !in_display && Path.unique_full_path file = !display_position.pfile;
+	syntax_errors := [];
 	let restore =
 		(fun () ->
 			restore_cache ();
@@ -159,7 +160,7 @@ let parse ctx code file =
 		| Sharp "if" ->
 			skip_tokens_loop p test (skip_tokens p false)
 		| Eof ->
-			if !in_display then tk else error Unclosed_macro p
+			syntax_error Unclosed_macro ~pos:(Some p) sraw tk
 		| _ ->
 			skip_tokens p test
 
@@ -173,10 +174,16 @@ let parse ctx code file =
 	) in
 	try
 		let l = parse_file s in
-		(match !mstack with p :: _ when not !in_display -> error Unclosed_macro p | _ -> ());
+		(match !mstack with p :: _ -> syntax_error Unclosed_macro ~pos:(Some p) sraw () | _ -> ());
+		let was_display_file = !in_display_file in
 		restore();
 		Lexer.restore old;
-		l
+		if was_display_file then
+			ParseDisplayFile(l,!syntax_errors)
+		else begin match List.rev !syntax_errors with
+			| [] -> ParseSuccess l
+			| error :: errors -> ParseError(l,error,errors)
+		end
 	with
 		| Stream.Error _
 		| Stream.Failure ->
@@ -193,8 +200,9 @@ let parse_string com s p error inlined =
 	let old = Lexer.save() in
 	let old_file = (try Some (Hashtbl.find Lexer.all_files p.pfile) with Not_found -> None) in
 	let old_display = !display_position in
-	let old_de = !display_error in
 	let old_in_display_file = !in_display_file in
+	let old_syntax_errors = !syntax_errors in
+	syntax_errors := [];
 	let restore() =
 		(match old_file with
 		| None -> ()
@@ -203,16 +211,15 @@ let parse_string com s p error inlined =
 			display_position := old_display;
 			in_display_file := old_in_display_file;
 		end;
-		Lexer.restore old;
-		display_error := old_de
+		syntax_errors := old_syntax_errors;
+		Lexer.restore old
 	in
 	Lexer.init p.pfile true;
-	display_error := (fun e p -> raise (Error (e,p)));
 	if not inlined then begin
 		display_position := null_pos;
 		in_display_file := false;
 	end;
-	let pack, decls = try
+	let result = try
 		parse com (Sedlexing.Utf8.from_string s) p.pfile
 	with Error (e,pe) ->
 		restore();
@@ -222,12 +229,17 @@ let parse_string com s p error inlined =
 		error (Lexer.error_msg e) (if inlined then pe else p)
 	in
 	restore();
-	pack,decls
+	result
 
 let parse_expr_string com s p error inl =
 	let head = "class X{static function main() " in
 	let head = (if p.pmin > String.length head then head ^ String.make (p.pmin - String.length head) ' ' else head) in
 	let rec loop e = let e = Ast.map_expr loop e in (fst e,p) in
+	let extract_expr (_,decls) = match decls with
+		| [EClass { d_data = [{ cff_name = "main",null_pos; cff_kind = FFun { f_expr = Some e } }]},_] -> (if inl then e else loop e)
+		| _ -> raise Exit
+	in
 	match parse_string com (head ^ s ^ ";}") p error inl with
-	| _,[EClass { d_data = [{ cff_name = "main",null_pos; cff_kind = FFun { f_expr = Some e } }]},_] -> if inl then e else loop e
-	| _ -> raise Exit
+	| ParseSuccess data -> ParseSuccess(extract_expr data)
+	| ParseError(data,error,errors) -> ParseError(extract_expr data,error,errors)
+	| ParseDisplayFile(data,errors) -> ParseDisplayFile(extract_expr data,errors)

+ 27 - 8
src/typing/macroContext.ml

@@ -125,12 +125,23 @@ let load_macro_ref : (typer -> bool -> path -> string -> pos -> (typer * ((strin
 
 let make_macro_api ctx p =
 	let parse_expr_string s p inl =
-		typing_timer ctx false (fun() -> try ParserEntry.parse_expr_string ctx.com.defines s p error inl with Exit -> raise MacroApi.Invalid_expr)
+		typing_timer ctx false (fun() ->
+			try
+				begin match ParserEntry.parse_expr_string ctx.com.defines s p error inl with
+					| ParseSuccess data -> data
+					| ParseDisplayFile(data,_) when inl -> data (* ignore errors when inline-parsing in display file *)
+					| ParseDisplayFile _ -> assert false (* cannot happen because ParserEntry.parse_string sets `display_position := null_pos;` *)
+					| ParseError _ -> raise MacroApi.Invalid_expr
+				end
+			with Exit ->
+				raise MacroApi.Invalid_expr)
 	in
 	let parse_metadata s p =
 		try
 			match ParserEntry.parse_string ctx.com.defines (s ^ " typedef T = T") null_pos error false with
-			| _,[ETypedef t,_] -> t.d_meta
+			| ParseSuccess(_,[ETypedef t,_]) -> t.d_meta
+			| ParseDisplayFile _ -> assert false (* cannot happen because null_pos is used *)
+			| ParseError(_,_,_) -> error "Malformed metadata string" p
 			| _ -> assert false
 		with _ ->
 			error "Malformed metadata string" p
@@ -223,7 +234,9 @@ let make_macro_api ctx p =
 			typing_timer ctx false (fun() ->
 				let v = (match v with None -> None | Some s ->
 					match ParserEntry.parse_string ctx.com.defines ("typedef T = " ^ s) null_pos error false with
-					| _,[ETypedef { d_data = ct },_] -> Some ct
+					| ParseSuccess(_,[ETypedef { d_data = ct },_]) -> Some ct
+					| ParseDisplayFile _ -> assert false (* cannot happen because null_pos is used *)
+					| ParseError(_,(msg,p),_) -> Parser.error msg p (* p is null_pos, but we don't have anything else here... *)
 					| _ -> assert false
 				) in
 				let tp = get_type_patch ctx t (Some (f,s)) in
@@ -470,7 +483,8 @@ let get_macro_context ctx p =
 		ctx.com.get_macros <- (fun() -> Some com2);
 		com2.package_rules <- PMap.empty;
 		com2.main_class <- None;
-		com2.display <- DisplayTypes.DisplayMode.create DMNone;
+		(* Inherit most display settings, but require normal typing. *)
+		com2.display <- {ctx.com.display with dms_kind = DMNone; dms_display = false; dms_full_typing = true };
 		List.iter (fun p -> com2.defines.Define.values <- PMap.remove (Globals.platform_name p) com2.defines.Define.values) Globals.platforms;
 		com2.defines.Define.defines_signature <- None;
 		com2.class_path <- List.filter (fun s -> not (ExtString.String.exists s "/_std/")) com2.class_path;
@@ -491,6 +505,7 @@ let load_macro_module ctx cpath display p =
 	let api, mctx = get_macro_context ctx p in
 	let m = (try Hashtbl.find ctx.g.types_module cpath with Not_found -> cpath) in
 	(* Temporarily enter display mode while typing the macro. *)
+	let old = mctx.com.display in
 	if display then mctx.com.display <- ctx.com.display;
 	let mloaded = TypeloadModule.load_module mctx m p in
 	api.MacroApi.current_macro_module <- (fun() -> mloaded);
@@ -502,7 +517,7 @@ let load_macro_module ctx cpath display p =
 		wildcard_packages = [];
 		module_imports = [];
 	};
-	mloaded
+	mloaded,(fun () -> mctx.com.display <- old)
 
 let load_macro ctx display cpath f p =
 	let api, mctx = get_macro_context ctx p in
@@ -513,7 +528,7 @@ let load_macro ctx display cpath f p =
 	) in
 	let (meth,mloaded) = try Hashtbl.find mctx.com.cached_macros (cpath,f) with Not_found ->
 		let t = macro_timer ctx ["typing";s_type_path cpath ^ "." ^ f] in
-		let mloaded = load_macro_module ctx cpath display p in
+		let mloaded,restore = load_macro_module ctx cpath display p in
 		let mt = Typeload.load_type_def mctx p { tpackage = fst cpath; tname = snd cpath; tparams = []; tsub = sub } in
 		let cl, meth = (match mt with
 			| TClassDecl c ->
@@ -525,7 +540,7 @@ let load_macro ctx display cpath f p =
 		if not (Common.defined ctx.com Define.NoDeprecationWarnings) then
 			DeprecationCheck.check_cf mctx.com meth p;
 		let meth = (match follow meth.cf_type with TFun (args,ret) -> (args,ret,cl,meth),mloaded | _ -> error "Macro call should be a method" p) in
-		mctx.com.display <- DisplayTypes.DisplayMode.create DMNone;
+		restore();
 		if not ctx.in_macro then flush_macro_context mint ctx;
 		Hashtbl.add mctx.com.cached_macros (cpath,f) meth;
 		mctx.m <- {
@@ -734,7 +749,11 @@ let call_macro ctx path meth args p =
 let call_init_macro ctx e =
 	let p = { pfile = "--macro " ^ e; pmin = -1; pmax = -1 } in
 	let e = try
-		ParserEntry.parse_expr_string ctx.com.defines e p error false
+		begin match ParserEntry.parse_expr_string ctx.com.defines e p error false with
+		| ParseSuccess data -> data
+		| ParseError(_,(msg,p),_) -> (Parser.error msg p)
+		| ParseDisplayFile _ -> assert false (* cannot happen *)
+		end
 	with err ->
 		display_error ctx ("Could not parse `" ^ e ^ "`") p;
 		raise err

+ 5 - 1
src/typing/typeloadModule.ml

@@ -899,7 +899,11 @@ let handle_import_hx ctx m decls p =
 			r
 		with Not_found ->
 			if Sys.file_exists path then begin
-				let _,r = TypeloadParse.parse_file ctx.com path p in
+				let _,r = match TypeloadParse.parse_file ctx.com path p with
+					| ParseSuccess data -> data
+					| ParseDisplayFile(data,_) -> data
+					| ParseError(_,(msg,p),_) -> Parser.error msg p
+				in
 				List.iter (fun (d,p) -> match d with EImport _ | EUsing _ -> () | _ -> error "Only import and using is allowed in import.hx files" p) r;
 				add_dependency m (make_import_module path r);
 				r

+ 30 - 8
src/typing/typeloadParse.ml

@@ -21,6 +21,7 @@
 
 open Globals
 open Ast
+open DisplayTypes.DiagnosticsSeverity
 open DisplayTypes.DisplayMode
 open Common
 open Typecore
@@ -30,7 +31,7 @@ let parse_file_from_lexbuf com file p lexbuf =
 	let t = Timer.timer ["parsing"] in
 	Lexer.init file true;
 	incr stats.s_files_parsed;
-	let data = try
+	let parse_result = try
 		ParserEntry.parse com.defines lexbuf file
 	with
 		| Sedlexing.MalFormed ->
@@ -40,9 +41,9 @@ let parse_file_from_lexbuf com file p lexbuf =
 			t();
 			raise e
 	in
-	begin match !Parser.display_mode with
-		| DMModuleSymbols (Some "") -> ()
-		| DMModuleSymbols filter when filter = None && DisplayPosition.is_display_file file ->
+	begin match !Parser.display_mode,parse_result with
+		| DMModuleSymbols (Some ""),_ -> ()
+		| DMModuleSymbols filter,(ParseSuccess data | ParseDisplayFile(data,_)) when filter = None && DisplayPosition.is_display_file file ->
 			let ds = DocumentSymbols.collect_module_symbols (filter = None) data in
 			DisplayException.raise_module_symbols (DocumentSymbols.Printer.print_module_symbols com [file,ds] filter);
 		| _ ->
@@ -50,7 +51,7 @@ let parse_file_from_lexbuf com file p lexbuf =
 	end;
 	t();
 	Common.log com ("Parsed " ^ file);
-	data
+	parse_result
 
 let parse_file_from_string com file p string =
 	parse_file_from_lexbuf com file p (Sedlexing.Utf8.from_string string)
@@ -111,7 +112,7 @@ let resolve_module_file com m remap p =
 		if List.exists (fun path -> ExtString.String.starts_with file (try Path.unique_full_path path with _ -> path)) com.std_path then raise Not_found;
 	| _ -> ());
 	if !forbid then begin
-		let _, decls = (!parse_hook) com file p in
+		let parse_result = (!parse_hook) com file p in
 		let rec loop decls = match decls with
 			| ((EImport _,_) | (EUsing _,_)) :: decls -> loop decls
 			| (EClass d,_) :: _ -> d.d_meta
@@ -120,7 +121,10 @@ let resolve_module_file com m remap p =
 			| (ETypedef d,_) :: _ -> d.d_meta
 			| [] -> []
 		in
-		let meta = loop decls in
+		let meta =  match parse_result with
+			| ParseSuccess(_,decls) | ParseDisplayFile((_,decls),_) -> loop decls
+			| ParseError _ -> []
+		in
 		if not (Meta.has Meta.NoPackageRestrict meta) then begin
 			let x = (match fst m with [] -> assert false | x :: _ -> x) in
 			raise (Forbid_package ((x,m,p),[],platform_name_macro com));
@@ -143,7 +147,25 @@ let resolve_module_file com m remap p =
 let parse_module' com m p =
 	let remap = ref (fst m) in
 	let file = resolve_module_file com m remap p in
-	let pack, decls = (!parse_hook) com file p in
+	let handle_parser_error msg p =
+		let msg = Parser.error_msg msg in
+		match com.display.dms_error_policy with
+			| EPShow -> error msg p
+			| EPIgnore -> ()
+			| EPCollect -> add_diagnostics_message com msg p Error
+	in
+	let pack,decls = match (!parse_hook) com file p with
+		| ParseSuccess data -> data
+		| ParseDisplayFile(data,errors) ->
+			begin match errors with
+			| (msg,p) :: _ -> handle_parser_error msg p
+			| [] -> ()
+			end;
+			data
+		| ParseError(data,(msg,p),_) ->
+			handle_parser_error msg p;
+			data
+	in
 	file,remap,pack,decls
 
 let parse_module ctx m p =

+ 7 - 2
src/typing/typer.ml

@@ -1540,8 +1540,13 @@ and format_string ctx s p =
 		if slen > 0 then begin
 			let e =
 				let ep = { p with pmin = !pmin + pos + 2; pmax = !pmin + send + 1 } in
-				try ParserEntry.parse_expr_string ctx.com.defines scode ep error true
-				with Exit -> error "Invalid interpolated expression" ep
+				try
+					begin match ParserEntry.parse_expr_string ctx.com.defines scode ep error true with
+						| ParseSuccess data | ParseDisplayFile(data,_) -> data
+						| ParseError(_,(msg,p),_) -> error (Parser.error_msg msg) p
+					end
+				with Exit ->
+					error "Invalid interpolated expression" ep
 			in
 			add_expr e slen
 		end;

+ 4 - 0
tests/display/src/DisplayTestContext.hx

@@ -14,6 +14,10 @@ class HaxeInvocationException {
 		this.arguments = arguments;
 		this.source = source;
 	}
+
+	public function toString() {
+		return 'HaxeInvocationException($message, $fieldName, $arguments, $source])';
+	}
 }
 
 class DisplayTestContext {

+ 1 - 2
tests/misc/projects/Issue3699/compile-var-fail.hxml.stderr

@@ -1,2 +1 @@
-MainVar.hx:3: characters 11-12 : expression expected after =
-MainVar.hx:3: characters 13-14 : Unexpected ;
+MainVar.hx:3: characters 13-14 : Expected expression

+ 2 - 3
tests/runci/targets/Macro.hx

@@ -10,9 +10,8 @@ class Macro {
 	static public function run(args:Array<String>) {
 		runCommand("haxe", ["compile-macro.hxml"].concat(args));
 
-		// TODO: enable this again at some point
-		// changeDirectory(displayDir);
-		// runCommand("haxe", ["build.hxml"]);
+		changeDirectory(displayDir);
+		runCommand("haxe", ["build.hxml"]);
 
 		changeDirectory(sourcemapsDir);
 		runCommand("haxe", ["run.hxml"]);

+ 16 - 1
tests/server/src/HaxeServerTestCase.hx

@@ -9,6 +9,7 @@ using Lambda;
 
 class TestContext {
 	public var messages:Array<String> = []; // encapsulation is overrated
+	public var errorMessages = [];
 	public var displayServerConfig:DisplayServerConfigBase;
 
 	public function new(config:DisplayServerConfigBase) {
@@ -54,6 +55,7 @@ class HaxeServerTestCase implements ITest {
 
 	function runHaxe(args:Array<String>, storeTypes = false, done:Void->Void) {
 		context.messages = [];
+		context.errorMessages = [];
 		storedTypes = [];
 		if (storeTypes) {
 			args = args.concat(['--display', '{ "method": "typer/compiledTypes", "id": 1 }']);
@@ -64,7 +66,7 @@ class HaxeServerTestCase implements ITest {
 			}
 			done();
 		}, function(message) {
-			Assert.fail(message);
+			context.errorMessages.push(message);
 			done();
 		});
 	}
@@ -82,6 +84,15 @@ class HaxeServerTestCase implements ITest {
 		return false;
 	}
 
+	function hasErrorMessage<T>(msg:String) {
+		for (message in context.errorMessages) {
+			if (message.endsWith(msg)) {
+				return true;
+			}
+		}
+		return false;
+	}
+
 	function getStoredType(typePackage:String, typeName:String) {
 		for (type in storedTypes) {
 			if (type.pack.join(".") == typePackage && type.name == typeName) {
@@ -91,6 +102,10 @@ class HaxeServerTestCase implements ITest {
 		return null;
 	}
 
+	function assertErrorMessage(message:String, ?p:haxe.PosInfos) {
+		Assert.isTrue(hasErrorMessage(message), p);
+	}
+
 	function assertHasPrint(line:String, ?p:haxe.PosInfos) {
 		Assert.isTrue(hasMessage("Haxe print: " + line), null, p);
 	}

+ 11 - 0
tests/server/src/Main.hx

@@ -77,6 +77,17 @@ class ServerTests extends HaxeServerTestCase {
 		assertSkipping("BuiltClass", "BuildMacro");
 		assertSkipping("BuildMacro");
 	}
+
+	function testBrokenSyntaxDiagnostics() {
+		vfs.putContent("BrokenSyntax.hx", getTemplate("BrokenSyntax.hx"));
+		vfs.putContent("Empty.hx", getTemplate("Empty.hx"));
+		var args = ["-main", "BrokenSyntax.hx", "--interp", "--no-output"];
+		runHaxe(args);
+		assertErrorMessage("Expected }");
+		runHaxe(args.concat(["--display", "Empty.hx@0@diagnostics"]));
+		runHaxe(args);
+		assertErrorMessage("Expected }");
+	}
 }
 
 class Main {

+ 6 - 0
tests/server/test/templates/BrokenSyntax.hx

@@ -0,0 +1,6 @@
+class BrokenSyntax {
+	static public function main() {
+		{}.foo();
+		trace("ok");
+	}
+}