123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402openPpxlibopenPpxx.HelperopenPpxx.Utilsletexp_of_positionp=letopenLexinginletmklids=at(lident"Lexing"$.s)inletopenExpinrecord[mklid"pos_fname",Exp.stringp.pos_fname;mklid"pos_lnum",Exp.intp.pos_lnum;mklid"pos_bol",Exp.intp.pos_bol;mklid"pos_cnum",Exp.intp.pos_cnum]Noneletexp_of_locationl=letopenLocationinletmklids=at~loc:(ghostl)(Lident"Ppx_test"$."Location"$.s)inletopenExpinrecord~loc:l[mklid"loc_start",exp_of_positionl.loc_start;mklid"loc_end",exp_of_positionl.loc_end;mklid"loc_ghost",booll.loc_ghost]Noneletrecexp_of_longident=letopenExpinletmklids=at(Lident"Ppx_test"$."Longident"$.s)infunction|Lidents->construct(mklid"Lident")(Some(strings))|Ldot(t,s)->construct(mklid"Ldot")(Some(tuple[exp_of_longidentt;strings]))|Lapply(t1,t2)->construct(mklid"Lapply")(Some(tuple[exp_of_longidentt1;exp_of_longidentt2]))(* __FOR_PACKAGE__ special value *)classextend_package=objectinheritPpxlib.Ast_traverse.mapassupermethod!expressione=matchewith|{pexp_desc=Pexp_ident{txt=Lident"__FOR_PACKAGE__";loc}}->beginmatch!Ocaml_common.Clflags.for_packagewith|None->letopenExpinconstraint_~loc(construct~loc{txt=Lident"None";loc}None)Typ.(constr{txt=Lident"option";loc}[constr{txt=Lident"string";loc}[]])|Somes->Exp.construct~loc{txt=Lident"Some";loc}&Some(Exp.strings)end|_->super#expressioneendletwith_refrefupdatef=letback=!refinupdate();letres=f()inref:=back;resletcurrent_structure_or_signature:[`Sigofsignature|`Strofstructure]optionref=refNonelet_with_current_structure_or_signaturex=with_refcurrent_structure_or_signature(fun()->current_structure_or_signature:=Somex)(* It lacks the top module name, which should be obtained from Location *)moduleCurrent_module_path=structletx=refNonelettop_module()=match!Ocaml_common.Location.input_namewith|"//toplevel//"ass->Lidents|fname->letbase=Filename.basenamefnameinletchopped=tryFilename.chop_extensionbasewith_->baseinletmn=String.capitalize_asciichoppedinmatch!Ocaml_common.Clflags.for_packagewith|None->Lidentmn|Somep->Ldot(Lidentp,mn)letsety=x:=Someyletget()=match!xwith|None->lety=top_module()inx:=Somey;y|Somey->yletwith_xf=letback=get()insetx;letres=f()insetback;resend(* This may creates an attribute with illegal expression ex.:
[@ppx_test_module_path Test.F(A)]
where Text.F(A) : Path.t is illegal for an exp.
If we keep it in the PPX output, it is rejected by OCaml 4.03,
so we must clean it.
*)letattr_module_path?locmp={attr_name=at"ppx_test_module_path";attr_payload=PStr[Str.mk(Pstr_eval(Exp.ident(atmp),[]))];attr_loc=def_locloc}letget_module_path_from_attr=function|{attr_name={txt="ppx_test_module_path"};attr_payload=PStr[{pstr_desc=Pstr_eval({pexp_desc=Pexp_ident{txt}},[])}];attr_loc=_}->Sometxt|_->Noneletget_module_path_from_attrsats=matchList.partition_map(funa->matchget_module_path_from_attrawith|Sometxt->`Righttxt|None->`Lefta)atswith|(rest,[x])->Somex,rest|(rest,[])->None,rest|_->assertfalseletrecannotate_module_exprmpmexp=letaddmexp=with_locmexp.pmod_loc&fun()->{mexpwithpmod_attributes=attr_module_pathmp::mexp.pmod_attributes}inmatchmexp.pmod_descwith|Pmod_structure_->addmexp|Pmod_constraint(mexp,mty)->{mexpwithpmod_desc=Pmod_constraint(annotate_module_exprmpmexp,annotate_module_typempmty)}|Pmod_functor(Named(({txt=Sometxt}asn),mtyo),mexp')->letmp=Lapply(mp,Lidenttxt)in{mexpwithpmod_desc=Pmod_functor(Named(n,mtyo),annotate_module_exprmpmexp')}|Pmod_functor(_,_mexp')->assertfalse(* XXX *)|_->mexpandannotate_module_typempmty=letaddmty={mtywithpmty_attributes=attr_module_pathmp::mty.pmty_attributes}inmatchmty.pmty_descwith|Pmty_signature_->addmty|_->mtyclassextend_module_path_tracking=object(* just list up named structures and signatures *)inheritextend_packageassupermethod!module_bindingmb=matchmb.pmb_name.txtwith|None->assertfalse(* XXX *)|Sometxt->letmp=Ldot(Current_module_path.get(),txt)inletmb={mbwithpmb_expr=annotate_module_exprmpmb.pmb_expr}inCurrent_module_path.with_mp&fun()->super#module_bindingmbmethod!module_declarationmd=matchmd.pmd_name.txtwith|None->assertfalse(* XXX *)|Sometxt->letmp=Ldot(Current_module_path.get(),txt)inletmd={mdwithpmd_type=annotate_module_typempmd.pmd_type}inCurrent_module_path.with_mp&fun()->super#module_declarationmdmethod!module_exprmexp=matchget_module_path_from_attrsmexp.pmod_attributeswith|Somemp,rest->Current_module_path.with_mp&fun()->super#module_expr{mexpwithpmod_attributes=rest}|None,rest->letmp=Ldot(Current_module_path.get(),"_")inCurrent_module_path.with_mp&fun()->super#module_expr{mexpwithpmod_attributes=rest}method!module_typemty=matchget_module_path_from_attrsmty.pmty_attributeswith|Somemp,rest->Current_module_path.with_mp&fun()->super#module_type{mtywithpmty_attributes=rest}|None,rest->letmp=Ldot(Current_module_path.get(),"_")inCurrent_module_path.with_mp&fun()->super#module_type{mtywithpmty_attributes=rest}end(* [let %TEST name = e] *)typetest_type=Unit|Bool|Failletdrop_tests=reffalseletwarn_dupes=reffalselettop_name=refNoneletreclident_concatl1=function|Lidents->Ldot(l1,s)|Ldot(t,s)->Ldot(lident_concatl1t,s)|Lapply(t1,t2)->Lapply(lident_concatl1t1,t2)letreturn_typelid=matchlidwith|Lidents|Ldot(_,s)->ifs.[String.lengths-1]='_'thenUnitelseifString.(lengths>5&&subs(lengths-5)5="_fail")thenFailelseBool|_->Boolletadd_top_namelid=match!top_namewith|None->lid|Sometop_lid->lident_concattop_lidlidmoduleTests=Stdlib.Set.Make(structtypet=Longident.tletcompare=compareend)lettests=refTests.emptyletadd_testloclid=(* Reject registeration of duped name *)letanon=function|Lident"_"->true|Lident_->false|Ldot(_,"_")->true|Ldot(_,_)->false|Lapply_->assertfalseinifanonlidthen()elseifTests.memlid!teststhenbeginif!warn_dupesthenwarnf"%a: ppx_test: Test name %s is already defined"Location.formatloc(Format.sprintf"%a"Longident.formatlid)endelsetests:=Tests.addlid!testslettest_itemmappertest=function|Pstr_eval(e,_attr)->(* unnamed boolean test *)letname=letlid=Current_module_path.get()inletmpath=Ldot(lid,"_")inadd_top_name&mpathinlettest="test"in(* Build [Ppx_test.test/test_ ~loc name (fun () -> e)] *)letopenExpinStr.valueNonrecursive[Vb.mk(Pat.unit())&open_lid(lid"PTest.TestTool")&apply(ident&at&Lident"PTest"$.test)[Nolabel,exp_of_locatione.pexp_loc;Nolabel,exp_of_longidentname;Nolabel,fun_NolabelNone(Pat.unit())(mapper#expressione)]]|Pstr_value(Nonrecursive,vbs)->letfvb=letloc=vb.pvb_locinletname=matchvb.pvb_patwith|{ppat_desc=Ppat_any}->letlid=Current_module_path.get()inletmpath=Ldot(lid,"_")inadd_top_name&mpath|{ppat_desc=Ppat_constant(Pconst_string(name,_,_))}->letmpath=Current_module_path.get()inadd_top_name&lident_concatmpath(Lidentname)|{ppat_desc=Ppat_var{txt=name}}->letlid=Current_module_path.get()inletmpath=Ldot(lid,name)inadd_top_name&mpath|{ppat_desc=Ppat_construct({txt=lid},None)}->letmpath=Current_module_path.get()inadd_top_name&lident_concatmpathlid|_->assertfalsein(* Check duped test names *)add_testlocname;letreturn_type=matchtestwith|"TEST_UNIT"->Unit|"TEST"->return_typename|"TEST_FAIL"->Fail|_->assertfalseinlettest=matchreturn_typewith|Unit->"test_unit"|Bool->"test"|Fail->"test_fail"in(* Build [Ppx_test.test/test_ ~loc name (fun () -> e)] *)letopenExpinVb.mk(Pat.unit())(apply(ident&at&Lident"PTest"$.test)[Nolabel,exp_of_locationloc;Nolabel,exp_of_longidentname;Nolabel,fun_NolabelNone(Pat.unit())(mapper#expressionvb.pvb_expr)])inStr.valueNonrecursive(List.mapfvbs)|_->assertfalseclassextend_let_test=object(self)inheritextend_module_path_tracking(* as super *)methodstructure_item'=function|{pstr_desc=Pstr_extension(({txt=("TEST"|"TEST_UNIT"|"TEST_FAIL")},_),_)}when!drop_tests->[](* let %TEST p = e or [%%TEST ...] *)|{pstr_desc=Pstr_extension(({txt=("TEST"|"TEST_UNIT"|"TEST_FAIL")},_),_)}assitemwhen!drop_tests->[{sitemwithpstr_desc=Pstr_eval(Exp.unit(),[])}](* let %TEST p = e
or [%%TEST let p = e]
*)|{pstr_desc=Pstr_extension(({txt=("TEST"|"TEST_UNIT"|"TEST_FAIL"astest)},PStr[{pstr_desc=(Pstr_value(Nonrecursive,_vbs)asdesc)}]),_)}->[test_itemselftestdesc](* [%%TEST ...], more general case
*)|{pstr_desc=Pstr_extension(({txt=("TEST"astest)},PStrsitems),_)}->List.map(funsitem->test_itemselftestsitem.pstr_desc)sitems|x->[self#structure_itemx]method!structuresitems=List.concat&List.mapself#structure_item'sitemsend(*
let cp = Current_module_path.get () in
(* CR jfuruse: BUG: This is not good. Internal anonymous structure consume the doctest. Actually doctest comment is PStr therefore it consumes itself! *)
let matched, non_matched = List.partition (fun (cp',_tests) -> cp = cp') !doctests in
doctests := non_matched;
let tests = List.(concat_map snd & rev matched) in
let tests = List.map (fun (loc, s) ->
(* CR jfuruse: need to incremtn loc_start *)
let lexbuf = Doctest.Lexing.from_string_with_position s loc.Location.loc_start in
Lexer.init ();
let e = Parser.parse_expression Lexer.token lexbuf in
let open Exp in
let name = None in
Str.value Nonrecursive [
Vb.mk Pat.unit
(apply
(ident & at & Lident "PTest" $. "test")
[ "", exp_of_location e.pexp_loc;
"", option & Option.map exp_of_longident name;
"", fun_ "" None Pat.unit (self.expr self e) ])
]
) tests
in
is @ tests
*)letmake_mapper()=current_structure_or_signature:=None;Current_module_path.x:=None;(* filled lazily when Location.input_name is set *)tests:=Tests.empty;(* no trans module border test name checks *)newextend_let_testletparse_as_lidents=matchLongident.parseswith|Lident""->None|l->Somelletopts=["-drop-tests",Arg.Setdrop_tests,"Drop tests";"-warn-dupes",Arg.Setwarn_dupes,"Warn tests with the same names";"-top-name",Arg.String(funs->matchparse_as_lidentswith|None->raise_errorf"-top-name must take a valid module path"|Somel->top_name:=Somel),"Set the top module name"]let()=List.iter(fun(k,spec,doc)->Driver.add_argkspec~doc)opts;Driver.register_transformation~impl:(make_mapper())#structure~intf:(make_mapper())#signature"ppx_test"