소스 검색

Custom metadata and defines (documentation, completion) (#10858)

* Add after_generation args context hook

* Add support for user-defined custom metadata (doc + completion)

* Add support for user-defined custom defines (documentation)

* Remove dead code

* Add meta/define source

* A bit of cleanup

* Further cleanup

* Move parse_meta_usage to metaList (via prebuild)

* Sort metadatas before sending to displayEmitter

* Fix metadata/define source

* Display source (when available) in metadata/define documentation

* Send source in completionItem too

* Add from: in custom meta/define source

* Add tests for custom defines/metas

* Expose std path to misc tests

* Fix tests for windows

* Revert "Expose std path to misc tests"

This reverts commit 94179f3f6461e8d1294a2deec7edc28363211ae6.

* Remove expected output for some failing tests

* Rename Compiler metadata/defines functions

* Update tests

* Misc tests: check stdout against .hxml.stdout file if it exists

* Update haxelib

* Revert "Add after_generation args context hook"

This reverts commit 17ad0274b27a60cb411321485221150a7e6a4d97.

* Execute --help-user-metas and --help-user-defines right after init macros

* Add tests for --help-user-xxx commands vs failing compilation

* Some cleanup

* Use origin instead of source

User defined source can still be called 'source' I guess?

* Move user defines to common context

* Move user metas to common context

* Use Platform and MetadataTarget enum abstracts

* Add basic documentation for MetadataDescription / DefineDescription
Rudy Ges 2 년 전
부모
커밋
13a8e48cbc
50개의 변경된 파일487개의 추가작업 그리고 63개의 파일을 삭제
  1. 1 1
      extra/haxelib_src
  2. 15 2
      src-prebuild/prebuild.ml
  3. 18 2
      src/compiler/args.ml
  4. 1 1
      src/compiler/compiler.ml
  5. 1 1
      src/compiler/displayOutput.ml
  6. 5 1
      src/context/common.ml
  7. 2 2
      src/context/display/displayEmitter.ml
  8. 1 1
      src/core/ast.ml
  9. 59 17
      src/core/define.ml
  10. 7 2
      src/core/display/completionItem.ml
  11. 15 0
      src/core/globals.ml
  12. 1 1
      src/core/json/genjson.ml
  13. 54 13
      src/core/meta.ml
  14. 1 1
      src/core/texpr.ml
  15. 54 0
      src/macro/macroApi.ml
  16. 2 0
      src/typing/macroContext.ml
  17. 1 0
      std/haxe/display/Display.hx
  18. 84 0
      std/haxe/macro/Compiler.hx
  19. 1 0
      tests/.haxelib/.repo-version
  20. 1 0
      tests/.haxelib/dummy_doc/.current
  21. 6 0
      tests/.haxelib/dummy_doc/1,0,0/doc/define.json
  22. 17 0
      tests/.haxelib/dummy_doc/1,0,0/doc/meta.json
  23. 16 0
      tests/.haxelib/dummy_doc/1,0,0/haxelib.json
  24. 1 0
      tests/.haxelib/dummy_doc_dep/.current
  25. 7 0
      tests/.haxelib/dummy_doc_dep/1,0,0/doc/define.json
  26. 6 0
      tests/.haxelib/dummy_doc_dep/1,0,0/doc/meta.json
  27. 13 0
      tests/.haxelib/dummy_doc_dep/1,0,0/haxelib.json
  28. 0 0
      tests/misc/projects/Issue10844/Fail.hx
  29. 2 0
      tests/misc/projects/Issue10844/base-fail.hxml
  30. 2 0
      tests/misc/projects/Issue10844/custom-define-nofail.hxml
  31. 2 0
      tests/misc/projects/Issue10844/custom-define-nofail.hxml.stdout
  32. 2 0
      tests/misc/projects/Issue10844/custom-define.hxml
  33. 2 0
      tests/misc/projects/Issue10844/custom-define.hxml.stdout
  34. 2 0
      tests/misc/projects/Issue10844/custom-meta-nofail.hxml
  35. 5 0
      tests/misc/projects/Issue10844/custom-meta-nofail.hxml.stdout
  36. 2 0
      tests/misc/projects/Issue10844/custom-meta.hxml
  37. 5 0
      tests/misc/projects/Issue10844/custom-meta.hxml.stdout
  38. 6 0
      tests/misc/projects/Issue10844/define.json
  39. 6 0
      tests/misc/projects/Issue10844/meta.json
  40. 2 0
      tests/misc/projects/Issue10844/user-defined-define-fail.hxml
  41. 2 0
      tests/misc/projects/Issue10844/user-defined-define-fail.hxml.stderr
  42. 2 0
      tests/misc/projects/Issue10844/user-defined-define-json-fail.hxml
  43. 4 0
      tests/misc/projects/Issue10844/user-defined-defines.hxml
  44. 4 0
      tests/misc/projects/Issue10844/user-defined-defines.hxml.stdout
  45. 2 0
      tests/misc/projects/Issue10844/user-defined-meta-fail.hxml
  46. 2 0
      tests/misc/projects/Issue10844/user-defined-meta-fail.hxml.stderr
  47. 2 0
      tests/misc/projects/Issue10844/user-defined-meta-json-fail.hxml
  48. 4 0
      tests/misc/projects/Issue10844/user-defined-metas.hxml
  49. 8 0
      tests/misc/projects/Issue10844/user-defined-metas.hxml.stdout
  50. 29 18
      tests/misc/src/Main.hx

+ 1 - 1
extra/haxelib_src

@@ -1 +1 @@
-Subproject commit a18b403e8d35bb5c16b92d0e0ae2b0c630918413
+Subproject commit 4e4b03464ae272179692edf92c2f84848901e07d

+ 15 - 2
src-prebuild/prebuild.ml

@@ -259,6 +259,19 @@ type meta_usage =
 	| TTypeParameter
 	| TTypeParameter
 	| TVariable
 	| TVariable
 
 
+let parse_meta_usage = function
+	| \"TClass\" -> TClass
+	| \"TClassField\" -> TClassField
+	| \"TAbstract\" -> TAbstract
+	| \"TAbstractField\" -> TAbstractField
+	| \"TEnum\" -> TEnum
+	| \"TTypedef\" -> TTypedef
+	| \"TAnyField\" -> TAnyField
+	| \"TExpr\" -> TExpr
+	| \"TTypeParameter\" -> TTypeParameter
+	| \"TVariable\" -> TVariable
+	| t -> raise (failwith (\"invalid metadata target \" ^ t))
+
 type meta_parameter =
 type meta_parameter =
 	| HasParam of string
 	| HasParam of string
 	| Platforms of platform list
 	| Platforms of platform list
@@ -276,10 +289,10 @@ match Array.to_list (Sys.argv) with
 		Printf.printf "%s" define_header;
 		Printf.printf "%s" define_header;
 		Printf.printf "type strict_defined =\n";
 		Printf.printf "type strict_defined =\n";
 		Printf.printf "%s" (gen_define_type defines);
 		Printf.printf "%s" (gen_define_type defines);
-		Printf.printf "\n\t| Last\n\n"; (* must be last *)
+		Printf.printf "\n\t| Last\n\t| Custom of string\n\n";
 		Printf.printf "let infos = function\n";
 		Printf.printf "let infos = function\n";
 		Printf.printf "%s" (gen_define_info defines);
 		Printf.printf "%s" (gen_define_info defines);
-		Printf.printf "\n\t| Last -> die \"\" __LOC__\n"
+		Printf.printf "\n\t| Last -> die \"\" __LOC__\n\t| Custom s -> s,(\"\",[])\n"
 	| [_; "meta"; meta_path]->
 	| [_; "meta"; meta_path]->
 		let metas = parse_file_array meta_path parse_meta in
 		let metas = parse_file_array meta_path parse_meta in
 		Printf.printf "%s" meta_header;
 		Printf.printf "%s" meta_header;

+ 18 - 2
src/compiler/args.ml

@@ -146,17 +146,33 @@ let parse_args com =
 			raise (Arg.Help "")
 			raise (Arg.Help "")
 		),"","show extended help information");
 		),"","show extended help information");
 		("Miscellaneous",["--help-defines"],[], Arg.Unit (fun() ->
 		("Miscellaneous",["--help-defines"],[], Arg.Unit (fun() ->
-			let all,max_length = Define.get_documentation_list() in
+			let all,max_length = Define.get_documentation_list com.user_defines in
 			let all = List.map (fun (n,doc) -> Printf.sprintf " %-*s: %s" max_length n (limit_string doc (max_length + 3))) all in
 			let all = List.map (fun (n,doc) -> Printf.sprintf " %-*s: %s" max_length n (limit_string doc (max_length + 3))) all in
 			List.iter (fun msg -> com.print (msg ^ "\n")) all;
 			List.iter (fun msg -> com.print (msg ^ "\n")) all;
 			actx.did_something <- true
 			actx.did_something <- true
 		),"","print help for all compiler specific defines");
 		),"","print help for all compiler specific defines");
+		("Miscellaneous",["--help-user-defines"],[], Arg.Unit (fun() ->
+			com.callbacks#add_after_init_macros (fun() ->
+				let all,max_length = Define.get_user_documentation_list com.user_defines in
+				let all = List.map (fun (n,doc) -> Printf.sprintf " %-*s: %s" max_length n (limit_string doc (max_length + 3))) all in
+				List.iter (fun msg -> com.print (msg ^ "\n")) all;
+				exit 0
+			)
+		),"","print help for all user defines");
 		("Miscellaneous",["--help-metas"],[], Arg.Unit (fun() ->
 		("Miscellaneous",["--help-metas"],[], Arg.Unit (fun() ->
-			let all,max_length = Meta.get_documentation_list() in
+			let all,max_length = Meta.get_documentation_list com.user_metas in
 			let all = List.map (fun (n,doc) -> Printf.sprintf " %-*s: %s" max_length n (limit_string doc (max_length + 3))) all in
 			let all = List.map (fun (n,doc) -> Printf.sprintf " %-*s: %s" max_length n (limit_string doc (max_length + 3))) all in
 			List.iter (fun msg -> com.print (msg ^ "\n")) all;
 			List.iter (fun msg -> com.print (msg ^ "\n")) all;
 			actx.did_something <- true
 			actx.did_something <- true
 		),"","print help for all compiler metadatas");
 		),"","print help for all compiler metadatas");
+		("Miscellaneous",["--help-user-metas"],[], Arg.Unit (fun() ->
+			com.callbacks#add_after_init_macros (fun() ->
+				let all,max_length = Meta.get_user_documentation_list com.user_metas in
+				let all = List.map (fun (n,doc) -> Printf.sprintf " %-*s: %s" max_length n (limit_string doc (max_length + 3))) all in
+				List.iter (fun msg -> com.print (msg ^ "\n")) all;
+				exit 0
+			)
+		),"","print help for all user metadatas");
 	] in
 	] in
 	let adv_args_spec = [
 	let adv_args_spec = [
 		("Optimization",["--dce"],["-dce"],Arg.String (fun mode ->
 		("Optimization",["--dce"],["-dce"],Arg.String (fun mode ->

+ 1 - 1
src/compiler/compiler.ml

@@ -616,4 +616,4 @@ module HighLevel = struct
 		in
 		in
 		let code = loop args in
 		let code = loop args in
 		comm.exit code
 		comm.exit code
-end
+end

+ 1 - 1
src/compiler/displayOutput.ml

@@ -71,7 +71,7 @@ let print_fields fields =
 		| ITPackage(path,_) -> "package",snd path,"",None
 		| ITPackage(path,_) -> "package",snd path,"",None
 		| ITModule path -> "type",snd path,"",None
 		| ITModule path -> "type",snd path,"",None
 		| ITMetadata  meta ->
 		| ITMetadata  meta ->
-			let s,(doc,_) = Meta.get_info meta in
+			let s,(doc,_),_ = Meta.get_info meta in
 			"metadata","@" ^ s,"",doc_from_string doc
 			"metadata","@" ^ s,"",doc_from_string doc
 		| ITTimer(name,value) -> "timer",name,"",doc_from_string value
 		| ITTimer(name,value) -> "timer",name,"",doc_from_string value
 		| ITLiteral s ->
 		| ITLiteral s ->

+ 5 - 1
src/context/common.ml

@@ -365,6 +365,8 @@ type context = {
 	mutable load_extern_type : (string * (path -> pos -> Ast.package option)) list; (* allow finding types which are not in sources *)
 	mutable load_extern_type : (string * (path -> pos -> Ast.package option)) list; (* allow finding types which are not in sources *)
 	callbacks : compiler_callbacks;
 	callbacks : compiler_callbacks;
 	defines : Define.define;
 	defines : Define.define;
+	mutable user_defines : (string, Define.user_define) Hashtbl.t;
+	mutable user_metas : (string, Meta.user_meta) Hashtbl.t;
 	mutable get_macros : unit -> context option;
 	mutable get_macros : unit -> context option;
 	(* typing state *)
 	(* typing state *)
 	shared : shared_context;
 	shared : shared_context;
@@ -546,7 +548,7 @@ let default_config =
 	}
 	}
 
 
 let get_config com =
 let get_config com =
-	let defined f = PMap.mem (fst (Define.infos f)) com.defines.values in
+	let defined f = PMap.mem (Define.get_define_key f) com.defines.values in
 	match com.platform with
 	match com.platform with
 	| Cross ->
 	| Cross ->
 		default_config
 		default_config
@@ -817,6 +819,8 @@ let create compilation_step cs version args =
 			defines_signature = None;
 			defines_signature = None;
 			values = PMap.empty;
 			values = PMap.empty;
 		};
 		};
+		user_defines = Hashtbl.create 0;
+		user_metas = Hashtbl.create 0;
 		get_macros = (fun() -> None);
 		get_macros = (fun() -> None);
 		info = (fun _ _ -> die "" __LOC__);
 		info = (fun _ _ -> die "" __LOC__);
 		warning = (fun _ _ _ -> die "" __LOC__);
 		warning = (fun _ _ _ -> die "" __LOC__);

+ 2 - 2
src/context/display/displayEmitter.ml

@@ -145,7 +145,7 @@ let display_meta com meta p = match com.display.dms_kind with
 		begin match meta with
 		begin match meta with
 		| Meta.Custom _ | Meta.Dollar _ -> ()
 		| Meta.Custom _ | Meta.Dollar _ -> ()
 		| _ ->
 		| _ ->
-			if com.json_out = None then begin match Meta.get_documentation meta with
+			if com.json_out = None then begin match Meta.get_documentation com.user_metas meta with
 				| None -> ()
 				| None -> ()
 				| Some (_,s) ->
 				| Some (_,s) ->
 					raise_metadata ("<metadata>" ^ s ^ "</metadata>")
 					raise_metadata ("<metadata>" ^ s ^ "</metadata>")
@@ -153,7 +153,7 @@ let display_meta com meta p = match com.display.dms_kind with
 				raise_hover (make_ci_metadata meta) None p
 				raise_hover (make_ci_metadata meta) None p
 		end
 		end
 	| DMDefault ->
 	| DMDefault ->
-		let all = Meta.get_all() in
+		let all = Meta.get_all com.user_metas in
 		let all = List.map make_ci_metadata all in
 		let all = List.map make_ci_metadata all in
 		let subject = if meta = Meta.HxCompletion then None else Some (Meta.to_string meta) in
 		let subject = if meta = Meta.HxCompletion then None else Some (Meta.to_string meta) in
 		raise_fields all CRMetadata (make_subject subject p);
 		raise_fields all CRMetadata (make_subject subject p);

+ 1 - 1
src/core/ast.ml

@@ -1198,7 +1198,7 @@ module Expr = struct
 				add "ECheckType";
 				add "ECheckType";
 				loop e1;
 				loop e1;
 			| EMeta((m,_,_),e1) ->
 			| EMeta((m,_,_),e1) ->
-				add ("EMeta " ^ fst (Meta.get_info m));
+				add ("EMeta " ^ (Meta.to_string m));
 				loop e1
 				loop e1
 		in
 		in
 		loop' "" e;
 		loop' "" e;

+ 59 - 17
src/core/define.ml

@@ -6,34 +6,76 @@ type define = {
 	mutable defines_signature : string option;
 	mutable defines_signature : string option;
 }
 }
 
 
+type user_define = {
+	doc : string;
+	flags : define_parameter list;
+	source : string option;
+}
+
+let register_user_define user_defines s data =
+	Hashtbl.replace user_defines s data
+
+type define_origin =
+	| Compiler
+	| UserDefined of string option
+
+let infos ?user_defines d = match (user_defines,d) with
+	| (Some(user_defines), Custom(s)) when (Hashtbl.mem user_defines s) ->
+		let infos = Hashtbl.find user_defines s in
+		(s, (infos.doc, infos.flags), (UserDefined infos.source))
+	| (_, Custom(s)) ->
+		(s, ("", []), Compiler)
+	| _ ->
+		let def,infos = DefineList.infos d in
+		(def, infos, Compiler)
+
 let get_define_key d =
 let get_define_key d =
-	fst (infos d)
+	match (infos d) with (s,_,_) -> s
 
 
-let get_documentation_list() =
+let get_documentation user_defines d =
+	let t, (doc,flags), origin = infos ~user_defines:user_defines d in
+	let params = ref [] and pfs = ref [] in
+	List.iter (function
+		| HasParam s -> params := s :: !params
+		| Platforms fl -> pfs := fl @ !pfs
+		| Link _ -> ()
+	) flags;
+	let params = (match List.rev !params with
+		| [] -> ""
+		| l -> "<" ^ String.concat ">, <" l ^ "> "
+	) in
+	let origin = match origin with
+		| UserDefined Some s -> " (from " ^ s ^ ")"
+		| Compiler | UserDefined None -> ""
+	in
+	let pfs = platform_list_help (List.rev !pfs) in
+	(String.concat "-" (ExtString.String.nsplit t "_")), params ^ doc ^ pfs ^ origin
+
+let get_documentation_list user_defines =
 	let m = ref 0 in
 	let m = ref 0 in
 	let rec loop i =
 	let rec loop i =
 		let d = Obj.magic i in
 		let d = Obj.magic i in
 		if d <> Last then begin
 		if d <> Last then begin
-			let t, (doc,flags) = infos d in
-			let params = ref [] and pfs = ref [] in
-			List.iter (function
-				| HasParam s -> params := s :: !params
-				| Platforms fl -> pfs := fl @ !pfs
-				| Link _ -> ()
-			) flags;
-			let params = (match List.rev !params with
-				| [] -> ""
-				| l -> "<" ^ String.concat ">, <" l ^ "> "
-			) in
-			let pfs = platform_list_help (List.rev !pfs) in
-			if String.length t > !m then m := String.length t;
-			((String.concat "-" (ExtString.String.nsplit t "_")), params ^ doc ^ pfs) :: (loop (i + 1))
+			let (str,desc) = get_documentation user_defines d in
+			if String.length str > !m then m := String.length str;
+			(str,desc) :: loop (i + 1)
 		end else
 		end else
 			[]
 			[]
 	in
 	in
 	let all = List.sort (fun (s1,_) (s2,_) -> String.compare s1 s2) (loop 0) in
 	let all = List.sort (fun (s1,_) (s2,_) -> String.compare s1 s2) (loop 0) in
 	all,!m
 	all,!m
 
 
+let get_user_documentation_list user_defines =
+	let m = ref 0 in
+	let user_defines_list = (Hashtbl.fold (fun d _ acc ->
+		let (str,desc) = get_documentation user_defines (Custom d) in
+		if String.length str > !m then m := String.length str;
+		(str,desc) :: acc
+	) user_defines []) in
+
+	let all = List.sort (fun (s1,_) (s2,_) -> String.compare s1 s2) user_defines_list in
+	all,!m
+
 let raw_defined ctx k =
 let raw_defined ctx k =
 	PMap.mem k ctx.values
 	PMap.mem k ctx.values
 
 
@@ -44,7 +86,7 @@ let raw_defined_value ctx k =
 	PMap.find k ctx.values
 	PMap.find k ctx.values
 
 
 let defined_value ctx v =
 let defined_value ctx v =
-	raw_defined_value ctx (fst (infos v))
+	raw_defined_value ctx (get_define_key v)
 
 
 let defined_value_safe ?default ctx v =
 let defined_value_safe ?default ctx v =
 	try defined_value ctx v
 	try defined_value ctx v

+ 7 - 2
src/core/display/completionItem.ml

@@ -759,7 +759,7 @@ let to_json ctx index item =
 		]
 		]
 		| ITMetadata meta ->
 		| ITMetadata meta ->
 			let open Meta in
 			let open Meta in
-			let name,(doc,params) = Meta.get_info meta in
+			let name,(doc,params),origin = Meta.get_info meta in
 			let name = "@" ^ name in
 			let name = "@" ^ name in
 			let usage_to_string = function
 			let usage_to_string = function
 				| TClass -> "TClass"
 				| TClass -> "TClass"
@@ -773,6 +773,10 @@ let to_json ctx index item =
 				| TTypeParameter -> "TTypeParameter"
 				| TTypeParameter -> "TTypeParameter"
 				| TVariable -> "TVariable"
 				| TVariable -> "TVariable"
 			in
 			in
+			let origin = match origin with
+				| Compiler -> Some "haxe compiler"
+				| UserDefined s -> s
+			in
 			let rec loop internal params platforms targets links l = match l with
 			let rec loop internal params platforms targets links l = match l with
 				| HasParam s :: l -> loop internal (s :: params) platforms targets links l
 				| HasParam s :: l -> loop internal (s :: params) platforms targets links l
 				| Platforms pls :: l -> loop internal params ((List.map platform_name pls) @ platforms) targets links l
 				| Platforms pls :: l -> loop internal params ((List.map platform_name pls) @ platforms) targets links l
@@ -790,6 +794,7 @@ let to_json ctx index item =
 				"targets",jlist jstring targets;
 				"targets",jlist jstring targets;
 				"internal",jbool internal;
 				"internal",jbool internal;
 				"links",jlist jstring links;
 				"links",jlist jstring links;
+				"origin",jopt jstring origin;
 			]
 			]
 		| ITKeyword kwd ->"Keyword",jobject [
 		| ITKeyword kwd ->"Keyword",jobject [
 			"name",jstring (s_keyword kwd)
 			"name",jstring (s_keyword kwd)
@@ -828,4 +833,4 @@ let to_json ctx index item =
 			| Some t ->
 			| Some t ->
 				("type",CompletionType.to_json ctx (snd t)) :: jindex
 				("type",CompletionType.to_json ctx (snd t)) :: jindex
 		)
 		)
-	)
+	)

+ 15 - 0
src/core/globals.ml

@@ -64,6 +64,21 @@ let platform_name = function
 	| Hl -> "hl"
 	| Hl -> "hl"
 	| Eval -> "eval"
 	| Eval -> "eval"
 
 
+let parse_platform = function
+	| "cross" -> Cross
+	| "js" -> Js
+	| "lua" -> Lua
+	| "neko" -> Neko
+	| "flash" -> Flash
+	| "php" -> Php
+	| "cpp" -> Cpp
+	| "cs" -> Cs
+	| "java" -> Java
+	| "python" -> Python
+	| "hl" -> Hl
+	| "eval" -> Eval
+	| p -> raise (failwith ("invalid platform " ^ p))
+
 let platform_list_help = function
 let platform_list_help = function
 	| [] -> ""
 	| [] -> ""
 	| [p] -> " (" ^ platform_name p ^ " only)"
 	| [p] -> " (" ^ platform_name p ^ " only)"

+ 1 - 1
src/core/json/genjson.ml

@@ -177,7 +177,7 @@ and generate_metadata_entry ctx (m,el,p) =
 
 
 and generate_metadata ctx ml =
 and generate_metadata ctx ml =
 	let ml = List.filter (fun (m,_,_) ->
 	let ml = List.filter (fun (m,_,_) ->
-		let (_,(_,flags)) = Meta.get_info m in
+		let (_,(_,flags),_) = Meta.get_info m in
 		not (List.mem UsedInternally flags)
 		not (List.mem UsedInternally flags)
 	) ml in
 	) ml in
 	jlist (generate_metadata_entry ctx) ml
 	jlist (generate_metadata_entry ctx) ml

+ 54 - 13
src/core/meta.ml

@@ -10,14 +10,35 @@ let rec remove m = function
 	| (m2,_,_) :: l when m = m2 -> l
 	| (m2,_,_) :: l when m = m2 -> l
 	| x :: l -> x :: remove m l
 	| x :: l -> x :: remove m l
 
 
-let to_string m = fst (get_info m)
+type user_meta = {
+	doc : string;
+	flags : meta_parameter list;
+	source : string option;
+}
+
+type meta_origin =
+	| Compiler
+	| UserDefined of string option
+
+let register_user_meta user_metas s data =
+	Hashtbl.replace user_metas s data
+
+let get_info ?user_metas m = match (user_metas,m) with
+	| (Some(user_metas), Custom(s)) when (Hashtbl.mem user_metas s) ->
+		let infos = Hashtbl.find user_metas s in
+		(s, (infos.doc, infos.flags), (UserDefined infos.source))
+	| _ ->
+		let meta,infos = MetaList.get_info m in
+		(meta, infos, Compiler)
+
+let to_string m = match (get_info m) with (s,_,_) -> s
 
 
 let hmeta =
 let hmeta =
 	let h = Hashtbl.create 0 in
 	let h = Hashtbl.create 0 in
 	let rec loop i =
 	let rec loop i =
 		let m = Obj.magic i in
 		let m = Obj.magic i in
 		if m <> Last then begin
 		if m <> Last then begin
-			Hashtbl.add h (fst (get_info m)) m;
+			Hashtbl.add h (to_string m) m;
 			loop (i + 1);
 			loop (i + 1);
 		end;
 		end;
 	in
 	in
@@ -32,8 +53,8 @@ let from_string s =
 	| '$' -> Dollar (String.sub s 1 (String.length s - 1))
 	| '$' -> Dollar (String.sub s 1 (String.length s - 1))
 	| _ -> Custom s
 	| _ -> Custom s
 
 
-let get_documentation d =
-	let t, (doc,flags) = get_info d in
+let get_documentation user_metas d =
+	let t, (doc,flags), origin = get_info ~user_metas:user_metas d in
 	if not (List.mem UsedInternally flags) then begin
 	if not (List.mem UsedInternally flags) then begin
 		let params = ref [] and used = ref [] and pfs = ref [] in
 		let params = ref [] and used = ref [] and pfs = ref [] in
 		List.iter (function
 		List.iter (function
@@ -48,16 +69,20 @@ let get_documentation d =
 			| l -> "(<" ^ String.concat ">, <" l ^ ">) "
 			| l -> "(<" ^ String.concat ">, <" l ^ ">) "
 		) in
 		) in
 		let pfs = platform_list_help (List.rev !pfs) in
 		let pfs = platform_list_help (List.rev !pfs) in
+		let origin = match origin with
+			| UserDefined Some s -> " (from " ^ s ^ ")"
+			| Compiler | UserDefined None -> ""
+		in
 		let str = "@" ^ t in
 		let str = "@" ^ t in
-		Some (str,params ^ doc ^ pfs)
+		Some (str,params ^ doc ^ pfs ^ origin)
 	end else
 	end else
 		None
 		None
 
 
-let get_documentation_list () =
+let get_documentation_list user_metas =
 	let m = ref 0 in
 	let m = ref 0 in
 	let rec loop i =
 	let rec loop i =
 		let d = Obj.magic i in
 		let d = Obj.magic i in
-		if d <> Last then begin match get_documentation d with
+		if d <> Last then begin match get_documentation user_metas d with
 			| None -> loop (i + 1)
 			| None -> loop (i + 1)
 			| Some (str,desc) ->
 			| Some (str,desc) ->
 				if String.length str > !m then m := String.length str;
 				if String.length str > !m then m := String.length str;
@@ -68,14 +93,30 @@ let get_documentation_list () =
 	let all = List.sort (fun (s1,_) (s2,_) -> String.compare s1 s2) (loop 0) in
 	let all = List.sort (fun (s1,_) (s2,_) -> String.compare s1 s2) (loop 0) in
 	all,!m
 	all,!m
 
 
-let get_all () =
-	let rec loop i =
+let get_all user_metas =
+	let rec loop i acc =
 		let d = Obj.magic i in
 		let d = Obj.magic i in
-		if d <> Last then d :: loop (i + 1)
-		else []
+		if d <> Last then d :: loop (i + 1) acc
+		else acc
 	in
 	in
-	loop 0
+
+	let all = loop 0 (Hashtbl.fold (fun str _ acc -> (Custom str) :: acc) user_metas []) in
+	List.sort (fun m1 m2 -> String.compare (to_string m1) (to_string m2)) all
+
+let get_user_documentation_list user_metas =
+	let m = ref 0 in
+	let user_meta_list = (Hashtbl.fold (fun meta _ acc ->
+		begin match get_documentation user_metas (Custom meta) with
+			| None -> acc
+			| Some (str, desc) ->
+				if String.length str > !m then m := String.length str;
+				(str,desc) :: acc
+		end
+	) user_metas []) in
+
+	let all = List.sort (fun (s1,_) (s2,_) -> String.compare s1 s2) user_meta_list in
+	all,!m
 
 
 let copy_from_to m src dst =
 let copy_from_to m src dst =
 	try (get m src) :: dst
 	try (get m src) :: dst
-	with Not_found -> dst
+	with Not_found -> dst

+ 1 - 1
src/core/texpr.ml

@@ -760,7 +760,7 @@ let dump_with_pos tabs e =
 			add "TCast";
 			add "TCast";
 			loop e1;
 			loop e1;
 		| TMeta((m,_,_),e1) ->
 		| TMeta((m,_,_),e1) ->
-			add ("TMeta " ^ fst (Meta.get_info m));
+			add ("TMeta " ^ (Meta.to_string m));
 			loop e1
 			loop e1
 	in
 	in
 	loop' tabs e;
 	loop' tabs e;

+ 54 - 0
src/macro/macroApi.ml

@@ -2,6 +2,8 @@ open Ast
 open DisplayTypes.DisplayMode
 open DisplayTypes.DisplayMode
 open Type
 open Type
 open Common
 open Common
+open DefineList
+open MetaList
 
 
 exception Invalid_expr
 exception Invalid_expr
 exception Abort
 exception Abort
@@ -50,6 +52,8 @@ type 'value compiler_api = {
 	format_string : string -> Globals.pos -> Ast.expr;
 	format_string : string -> Globals.pos -> Ast.expr;
 	cast_or_unify : Type.t -> texpr -> Globals.pos -> bool;
 	cast_or_unify : Type.t -> texpr -> Globals.pos -> bool;
 	add_global_metadata : string -> string -> (bool * bool * bool) -> pos -> unit;
 	add_global_metadata : string -> string -> (bool * bool * bool) -> pos -> unit;
+	register_define : string -> Define.user_define -> unit;
+	register_metadata : string -> Meta.user_meta -> unit;
 	add_module_check_policy : string list -> int list -> bool -> int -> unit;
 	add_module_check_policy : string list -> int list -> bool -> int -> unit;
 	decode_expr : 'value -> Ast.expr;
 	decode_expr : 'value -> Ast.expr;
 	encode_expr : Ast.expr -> 'value;
 	encode_expr : Ast.expr -> 'value;
@@ -1747,6 +1751,56 @@ let macro_api ccom get_api =
 			(get_api()).add_global_metadata (decode_string s1) (decode_string s2) (decode_bool b1,decode_bool b2,decode_bool b3) (get_api_call_pos());
 			(get_api()).add_global_metadata (decode_string s1) (decode_string s2) (decode_bool b1,decode_bool b2,decode_bool b3) (get_api_call_pos());
 			vnull
 			vnull
 		);
 		);
+		"register_define_impl", vfun2 (fun d src ->
+			let flags : define_parameter list = [] in
+
+			let platforms = decode_opt_array decode_string (field d "platforms") in
+			let flags = match platforms with
+				| [] -> flags
+				| _ ->(Platforms (List.map (fun p -> (Globals.parse_platform p)) platforms)) :: flags
+			in
+
+			let params = decode_opt_array decode_string (field d "params") in
+			let flags = List.append flags (List.map (fun p -> (HasParam p : define_parameter)) params) in
+
+			let links = decode_opt_array decode_string (field d "links") in
+			let flags = List.append flags (List.map (fun l -> (Link l : define_parameter)) links) in
+
+			(get_api()).register_define (decode_string (field d "define")) {
+				doc = decode_string (field d "doc");
+				flags = flags;
+				source = opt decode_string src;
+			};
+			vnull
+		);
+		"register_metadata_impl", vfun2 (fun m src ->
+			let flags : meta_parameter list = [] in
+
+			let platforms = decode_opt_array decode_string (field m "platforms") in
+			let flags =
+				if (List.length platforms) = 0 then flags
+				else (Platforms (List.map (fun p -> (Globals.parse_platform p)) platforms)) :: flags
+			in
+
+			let targets = decode_opt_array decode_string (field m "targets") in
+			let flags =
+				if (List.length targets) = 0 then flags
+				else (UsedOn (List.map MetaList.parse_meta_usage targets)) :: flags
+			in
+
+			let params = decode_opt_array decode_string (field m "params") in
+			let flags = List.append flags (List.map (fun p -> HasParam p) params) in
+
+			let links = decode_opt_array decode_string (field m "links") in
+			let flags = List.append flags (List.map (fun l -> Link l) links) in
+
+			(get_api()).register_metadata (decode_string (field m "metadata")) {
+				doc = decode_string (field m "doc");
+				flags = flags;
+				source = opt decode_string src;
+			};
+			vnull
+		);
 		"set_custom_js_generator", vfun1 (fun f ->
 		"set_custom_js_generator", vfun1 (fun f ->
 			let f = prepare_callback f 1 in
 			let f = prepare_callback f 1 in
 			(get_api()).set_js_generator (fun js_ctx ->
 			(get_api()).set_js_generator (fun js_ctx ->

+ 2 - 0
src/typing/macroContext.ml

@@ -378,6 +378,8 @@ let make_macro_api ctx p =
 			| MacroContext -> add_macro ctx
 			| MacroContext -> add_macro ctx
 			| NormalAndMacroContext -> add ctx; add_macro ctx;
 			| NormalAndMacroContext -> add ctx; add_macro ctx;
 		);
 		);
+		MacroApi.register_define = (fun s data -> Define.register_user_define ctx.com.user_defines s data);
+		MacroApi.register_metadata = (fun s data -> Meta.register_user_meta ctx.com.user_metas s data);
 		MacroApi.decode_expr = Interp.decode_expr;
 		MacroApi.decode_expr = Interp.decode_expr;
 		MacroApi.encode_expr = Interp.encode_expr;
 		MacroApi.encode_expr = Interp.encode_expr;
 		MacroApi.encode_ctype = Interp.encode_ctype;
 		MacroApi.encode_ctype = Interp.encode_ctype;

+ 1 - 0
std/haxe/display/Display.hx

@@ -294,6 +294,7 @@ typedef Metadata = {
 	var parameters:Array<String>;
 	var parameters:Array<String>;
 	var platforms:Array<Platform>;
 	var platforms:Array<Platform>;
 	var targets:Array<MetadataTarget>;
 	var targets:Array<MetadataTarget>;
+	var origin:String;
 	var internal:Bool;
 	var internal:Bool;
 	var ?links:Array<String>;
 	var ?links:Array<String>;
 }
 }

+ 84 - 0
std/haxe/macro/Compiler.hx

@@ -22,6 +22,7 @@
 
 
 package haxe.macro;
 package haxe.macro;
 
 
+import haxe.display.Display;
 import haxe.macro.Expr;
 import haxe.macro.Expr;
 
 
 /**
 /**
@@ -450,6 +451,44 @@ class Compiler {
 		#end
 		#end
 	}
 	}
 
 
+	/**
+		Reference a json file describing user-defined metadata
+		See https://github.com/HaxeFoundation/haxe/blob/development/src-json/meta.json
+	**/
+	public static function registerMetadataDescriptionFile(path:String, ?source:String):Void {
+		var f = sys.io.File.getContent(path);
+		var content:Array<MetadataDescription> =  haxe.Json.parse(f);
+		for (m in content) registerCustomMetadata(m, source);
+	}
+
+	/**
+		Reference a json file describing user-defined defines
+		See https://github.com/HaxeFoundation/haxe/blob/development/src-json/define.json
+	**/
+	public static function registerDefinesDescriptionFile(path:String, ?source:String):Void {
+		var f = sys.io.File.getContent(path);
+		var content:Array<DefineDescription> =  haxe.Json.parse(f);
+		for (d in content) registerCustomDefine(d, source);
+	}
+
+	/**
+		Register a custom medatada for documentation and completion purposes
+	**/
+	public static function registerCustomMetadata(meta:MetadataDescription, ?source:String):Void {
+		#if (neko || eval)
+		load("register_metadata_impl", 2)(meta, source);
+		#end
+	}
+
+	/**
+		Register a custom define for documentation purposes
+	**/
+	public static function registerCustomDefine(define:DefineDescription, ?source:String):Void {
+		#if (neko || eval)
+		load("register_define_impl", 2)(define, source);
+		#end
+	}
+
 	/**
 	/**
 		Change the default JS output by using a custom generator callback
 		Change the default JS output by using a custom generator callback
 	**/
 	**/
@@ -577,3 +616,48 @@ enum abstract NullSafetyMode(String) to String {
 	**/
 	**/
 	var StrictThreaded;
 	var StrictThreaded;
 }
 }
+
+typedef MetadataDescription = {
+	final metadata:String;
+	final doc:String;
+
+	/**
+		External resources for more information about this metadata.
+	**/
+	@:optional final links:Array<String>;
+
+	/**
+		List (small description) of parameters that this metadata accepts.
+	**/
+	@:optional final params:Array<String>;
+
+	/**
+		Haxe target(s) for which this metadata is used.
+	**/
+	@:optional final platforms:Array<Platform>;
+
+	/**
+		Places where this metadata can be applied.
+	**/
+	@:optional final targets:Array<MetadataTarget>;
+}
+
+typedef DefineDescription = {
+	final define:String;
+	final doc:String;
+
+	/**
+		External resources for more information about this define.
+	**/
+	@:optional final links:Array<String>;
+
+	/**
+		List (small description) of parameters that this define accepts.
+	**/
+	@:optional final params:Array<String>;
+
+	/**
+		Haxe target(s) for which this define is used.
+	**/
+	@:optional final platforms:Array<Platform>;
+}

+ 1 - 0
tests/.haxelib/.repo-version

@@ -0,0 +1 @@
+1

+ 1 - 0
tests/.haxelib/dummy_doc/.current

@@ -0,0 +1 @@
+1.0.0

+ 6 - 0
tests/.haxelib/dummy_doc/1,0,0/doc/define.json

@@ -0,0 +1,6 @@
+[
+  {
+    "define": "no-bullshit",
+    "doc": "Only very important stuff should be compiled"
+  }
+]

+ 17 - 0
tests/.haxelib/dummy_doc/1,0,0/doc/meta.json

@@ -0,0 +1,17 @@
+[
+  {
+    "metadata": ":foo",
+    "doc": "Some documentation for the @:foo metadata for cpp platform",
+    "platforms": ["cpp"]
+  },
+  {
+    "metadata": ":bar",
+    "doc": "Some documentation for the TClass metadata '@:bar'",
+    "targets": ["TClass"]
+  },
+  {
+    "metadata": ":foobar",
+    "doc": "Some documentation for @:foobar",
+    "platforms": ["cpp"]
+  }
+]

+ 16 - 0
tests/.haxelib/dummy_doc/1,0,0/haxelib.json

@@ -0,0 +1,16 @@
+{
+	"name": "dummy_doc",
+	"url" : "http://example.org",
+	"license": "GPL",
+	"description": "This is a dummy lib for test purposes",
+	"version": "1.0.0",
+	"releasenote": "Initial release, everything is working correctly",
+	"documentation": {
+		"metadata": "doc/meta.json",
+		"defines": "doc/define.json"
+	},
+	"contributors": ["haxe"],
+	"dependencies": {
+		"dummy_doc_dep": "1.0.0"
+	}
+}

+ 1 - 0
tests/.haxelib/dummy_doc_dep/.current

@@ -0,0 +1 @@
+1.0.0

+ 7 - 0
tests/.haxelib/dummy_doc_dep/1,0,0/doc/define.json

@@ -0,0 +1,7 @@
+[
+  {
+    "define": "dummy",
+    "doc": "On a scale from 0 to 10",
+    "params": ["level: 10 | 11"]
+  }
+]

+ 6 - 0
tests/.haxelib/dummy_doc_dep/1,0,0/doc/meta.json

@@ -0,0 +1,6 @@
+[
+  {
+    "metadata": ":baz",
+    "doc": "Some documentation for the @:baz metadata"
+  }
+]

+ 13 - 0
tests/.haxelib/dummy_doc_dep/1,0,0/haxelib.json

@@ -0,0 +1,13 @@
+{
+	"name": "dummy_doc_dep",
+	"url" : "http://example.org",
+	"license": "GPL",
+	"description": "This is a dummy lib for test purposes",
+	"version": "1.0.0",
+	"releasenote": "Initial release, everything is working correctly",
+	"documentation": {
+		"metadata": "doc/meta.json",
+		"defines": "doc/define.json"
+	},
+	"contributors": ["haxe"]
+}

+ 0 - 0
tests/misc/projects/Issue10844/Fail.hx


+ 2 - 0
tests/misc/projects/Issue10844/base-fail.hxml

@@ -0,0 +1,2 @@
+-lib dummy_doc
+--main Fail

+ 2 - 0
tests/misc/projects/Issue10844/custom-define-nofail.hxml

@@ -0,0 +1,2 @@
+base-fail.hxml
+--help-user-defines

+ 2 - 0
tests/misc/projects/Issue10844/custom-define-nofail.hxml.stdout

@@ -0,0 +1,2 @@
+ dummy      : <level: 10 | 11> On a scale from 0 to 10 (from dummy_doc_dep)
+ no-bullshit: Only very important stuff should be compiled (from dummy_doc)

+ 2 - 0
tests/misc/projects/Issue10844/custom-define.hxml

@@ -0,0 +1,2 @@
+-lib dummy_doc
+--help-user-defines

+ 2 - 0
tests/misc/projects/Issue10844/custom-define.hxml.stdout

@@ -0,0 +1,2 @@
+ dummy      : <level: 10 | 11> On a scale from 0 to 10 (from dummy_doc_dep)
+ no-bullshit: Only very important stuff should be compiled (from dummy_doc)

+ 2 - 0
tests/misc/projects/Issue10844/custom-meta-nofail.hxml

@@ -0,0 +1,2 @@
+base-fail.hxml
+--help-user-metas

+ 5 - 0
tests/misc/projects/Issue10844/custom-meta-nofail.hxml.stdout

@@ -0,0 +1,5 @@
+ @:bar   : Some documentation for the TClass metadata '@:bar' (from dummy_doc)
+ @:baz   : Some documentation for the @:baz metadata (from dummy_doc_dep)
+ @:foo   : Some documentation for the @:foo metadata for cpp platform (cpp
+           only) (from dummy_doc)
+ @:foobar: Some documentation for @:foobar (cpp only) (from dummy_doc)

+ 2 - 0
tests/misc/projects/Issue10844/custom-meta.hxml

@@ -0,0 +1,2 @@
+-lib dummy_doc
+--help-user-metas

+ 5 - 0
tests/misc/projects/Issue10844/custom-meta.hxml.stdout

@@ -0,0 +1,5 @@
+ @:bar   : Some documentation for the TClass metadata '@:bar' (from dummy_doc)
+ @:baz   : Some documentation for the @:baz metadata (from dummy_doc_dep)
+ @:foo   : Some documentation for the @:foo metadata for cpp platform (cpp
+           only) (from dummy_doc)
+ @:foobar: Some documentation for @:foobar (cpp only) (from dummy_doc)

+ 6 - 0
tests/misc/projects/Issue10844/define.json

@@ -0,0 +1,6 @@
+[
+  {
+    "define": "fromjson",
+    "doc": "Custom user define from json"
+  }
+]

+ 6 - 0
tests/misc/projects/Issue10844/meta.json

@@ -0,0 +1,6 @@
+[
+  {
+    "metadata": ":fromjson",
+    "doc": "Custom user metadata from json"
+  }
+]

+ 2 - 0
tests/misc/projects/Issue10844/user-defined-define-fail.hxml

@@ -0,0 +1,2 @@
+--macro haxe.macro.Compiler.registerCustomDefine({dfine: 'zzz', doc: 'something'}, 'myapp')
+--help-user-defines

+ 2 - 0
tests/misc/projects/Issue10844/user-defined-define-fail.hxml.stderr

@@ -0,0 +1,2 @@
+(unknown) : Object requires field define
+(unknown) : ... For function argument 'define'

+ 2 - 0
tests/misc/projects/Issue10844/user-defined-define-json-fail.hxml

@@ -0,0 +1,2 @@
+--macro haxe.macro.Compiler.registerDefinesDescriptionFile('define.jsno', 'myapp')
+--help-user-defines

+ 4 - 0
tests/misc/projects/Issue10844/user-defined-defines.hxml

@@ -0,0 +1,4 @@
+-lib dummy_doc
+--macro haxe.macro.Compiler.registerDefinesDescriptionFile('define.json', 'myapp')
+--macro haxe.macro.Compiler.registerCustomDefine({define: 'custom', doc: 'Some user define'}, 'myapp')
+--help-user-defines

+ 4 - 0
tests/misc/projects/Issue10844/user-defined-defines.hxml.stdout

@@ -0,0 +1,4 @@
+ custom     : Some user define (from myapp)
+ dummy      : <level: 10 | 11> On a scale from 0 to 10 (from dummy_doc_dep)
+ fromjson   : Custom user define from json (from myapp)
+ no-bullshit: Only very important stuff should be compiled (from dummy_doc)

+ 2 - 0
tests/misc/projects/Issue10844/user-defined-meta-fail.hxml

@@ -0,0 +1,2 @@
+--macro haxe.macro.Compiler.registerCustomMetadata({metdata: ':zzz', doc: 'something'}, 'myapp')
+--help-user-metas

+ 2 - 0
tests/misc/projects/Issue10844/user-defined-meta-fail.hxml.stderr

@@ -0,0 +1,2 @@
+(unknown) : Object requires field metadata
+(unknown) : ... For function argument 'meta'

+ 2 - 0
tests/misc/projects/Issue10844/user-defined-meta-json-fail.hxml

@@ -0,0 +1,2 @@
+--macro haxe.macro.Compiler.registerMetadataDescriptionFile('meta.jsno', 'myapp')
+--help-user-metas

+ 4 - 0
tests/misc/projects/Issue10844/user-defined-metas.hxml

@@ -0,0 +1,4 @@
+-lib dummy_doc
+--macro haxe.macro.Compiler.registerMetadataDescriptionFile('meta.json', 'myapp')
+--macro haxe.macro.Compiler.registerCustomMetadata({metadata: ':custom', doc: 'Some user meta'}, 'myapp')
+--help-user-metas

+ 8 - 0
tests/misc/projects/Issue10844/user-defined-metas.hxml.stdout

@@ -0,0 +1,8 @@
+ @:bar     : Some documentation for the TClass metadata '@:bar' (from
+             dummy_doc)
+ @:baz     : Some documentation for the @:baz metadata (from dummy_doc_dep)
+ @:custom  : Some user meta (from myapp)
+ @:foo     : Some documentation for the @:foo metadata for cpp platform (cpp
+             only) (from dummy_doc)
+ @:foobar  : Some documentation for @:foobar (cpp only) (from dummy_doc)
+ @:fromjson: Custom user metadata from json (from myapp)

+ 29 - 18
tests/misc/src/Main.hx

@@ -44,8 +44,9 @@ class Main {
 					Sys.setCwd(dirPath);
 					Sys.setCwd(dirPath);
 					Sys.println('Running haxe $path');
 					Sys.println('Running haxe $path');
 					var expectFailure = file.endsWith("-fail.hxml");
 					var expectFailure = file.endsWith("-fail.hxml");
+					var expectStdout = if (FileSystem.exists('$file.stdout')) prepareExpectedOutput(File.getContent('$file.stdout')) else null;
 					var expectStderr = if (FileSystem.exists('$file.stderr')) prepareExpectedOutput(File.getContent('$file.stderr')) else null;
 					var expectStderr = if (FileSystem.exists('$file.stderr')) prepareExpectedOutput(File.getContent('$file.stderr')) else null;
-					var result = runCommand("haxe", [file], expectFailure, expectStderr);
+					var result = runCommand("haxe", [file], expectFailure, expectStdout, expectStderr);
 					++count;
 					++count;
 					if (!result.success) {
 					if (!result.success) {
 						failures++;
 						failures++;
@@ -83,7 +84,7 @@ class Main {
 		return p;
 		return p;
 	}
 	}
 
 
-	static function runCommand(command:String, args:Array<String>, expectFailure:Bool, expectStderr:String):{success:Bool, summary:String} {
+	static function runCommand(command:String, args:Array<String>, expectFailure:Bool, expectStdout:String, expectStderr:String):{success:Bool, summary:String} {
 		var summary = [];
 		var summary = [];
 		function println(msg:String) {
 		function println(msg:String) {
 			summary.push(msg);
 			summary.push(msg);
@@ -121,28 +122,38 @@ class Main {
 				false;
 				false;
 		}
 		}
 
 
-		if (stdout.length > 0) {
+		if (result && expectStdout != null) {
+			result = checkOutput(println, "stdout", stdout.toString(), expectStdout);
+		} else if (stdout.length > 0) {
 			println(stdout.toString());
 			println(stdout.toString());
 		}
 		}
 
 
 		if (result && expectStderr != null) {
 		if (result && expectStderr != null) {
-			var stderr = proc.stderr.readAll().toString().replace("\r\n", "\n").trim();
-			var expected = expectStderr.trim();
-			if (stderr != expected) {
-				// "Picked up JAVA_TOOL_OPTIONS: <...>" is printed by JVM sometimes.
-				// @see https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/envvars002.html
-				stderr = stderr.split('\n')
-					.filter(s -> 0 != s.indexOf('Picked up JAVA_TOOL_OPTIONS:'))
-					.join('\n');
-				if(stderr != expected) {
-					println("Actual stderr output doesn't match the expected one");
-					println('Expected:\n"$expectStderr"');
-					println('Actual:\n"$stderr"');
-					result = false;
-				}
-			}
+			result = checkOutput(println, "stderr", proc.stderr.readAll().toString(), expectStderr);
 		}
 		}
+
 		proc.close();
 		proc.close();
 		return {success:result, summary:summary.join('\n')};
 		return {success:result, summary:summary.join('\n')};
 	}
 	}
+
+	static function checkOutput(println:String->Void, name:String, content:String, expected:String):Bool {
+		var content = content.replace("\r\n", "\n").trim();
+		var expected = expected.trim();
+		if (content != expected) {
+			// "Picked up JAVA_TOOL_OPTIONS: <...>" is printed by JVM sometimes.
+			// @see https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/envvars002.html
+			content = content.split('\n')
+				.filter(s -> 0 != s.indexOf('Picked up JAVA_TOOL_OPTIONS:'))
+				.join('\n');
+
+			if (content != expected) {
+				println('Actual $name output doesn\'t match the expected one');
+				println('Expected:\n"$expected"');
+				println('Actual:\n"$content"');
+				return false;
+			}
+		}
+
+		return true;
+	}
 }
 }