123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537(*
* Copyright (c) 2018 Thomas Gazagnaire <thomas@gazagnaire.org>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*)openUtil.Result.Infixletloc_error~loc fmt=Format.kasprintf(funs->Error (`Msgs))("%a: invalid code block: "^^fmt)Stable_printer.Location.pplocletlocate_error_msg~locs=Format.asprintf"%a: invalid code block: %s"Stable_printer.Location.pplocsletlocate_errors~locr=Result.map_error(funl->List.map(fun(`Msgm)->`Msg(locate_error_msg~locm))l)rmoduleHeader=structtypet=Shellof[`Sh|`Bash]|OCaml|Otherofstringletppppf=function|Shell `Sh->Fmt.stringppf"sh"|Shell `Bash->Fmt.stringppf"bash"|OCaml->Fmt.stringppf"ocaml"|Others->Fmt.stringppfsletof_string=function|""->None|"sh"->Some(Shell`Sh)|"bash"->Some(Shell`Bash)|"ocaml"->SomeOCaml|s->Some(Others)letinfer_from_filefile=matchFilename.(remove_extension(basenamefile),extensionfile)with|("dune"|"dune-project"),_->Some(Other"scheme")|_,(".ml"|".mli"|".mlt"|".eliom"|".eliomi")->SomeOCaml|_,".sh"->Some(Shell`Sh)|_->Noneendtypesection=int*stringmoduleRaw=structtypet=|Includeof{loc:Location.t;section:sectionoption;labels:string}|Anyof{loc:Location.t;section:sectionoption;header :string;contents:stringlist;label_cmt :stringoption;legacy_labels :string;errors:Output.tlist;}let make ~loc~section~header ~contents~label_cmt~legacy_labels ~errors =Any{loc;section;header;contents;label_cmt;legacy_labels;errors }letmake_include ~loc~section~labels =Include{loc;section;labels}endtypecram_value={language:[`Sh|`Bash];non_det:Label.non_det option}typeocaml_value ={env:Ocaml_env.t;non_det:Label.non_detoption;errors:Output.tlist;header:Header.toption;}typetoplevel_value={env:Ocaml_env.t;non_det:Label.non_det option}typeinclude_ocaml_file ={part_included:stringoption}typeinclude_other_file ={header:Header.toption}typeinclude_file_kind =|Fk_ocamlofinclude_ocaml_file|Fk_otherofinclude_other_filetypeinclude_value ={file_included:string;file_kind:include_file_kind }typeraw_value={header:Header.toption}typevalue=|Rawofraw_value|OCamlofocaml_value|Cramofcram_value|Topleveloftoplevel_value|Includeofinclude_valuetypet={loc:Location.t;section:section option;dir:stringoption;labels:Label.tlist;legacy_labels:bool;contents:stringlist;skip:bool;version_enabled:bool;os_type_enabled:bool;set_variables:(string*string)list;unset_variables:stringlist;delim:stringoption;value:value;}letdump_section=Fmt.(Dump.pairint string)letheadert=matcht.valuewith|Raw{header;_}->header|OCaml{header;_}->header|Cram{language;_}->Some(Header.Shelllanguage)|Toplevel_->SomeHeader.OCaml|Include{file_kind=Fk_ocaml_;_}->SomeHeader.OCaml|Include{file_kind=Fk_otherb;_}->b.headerletdump_valueppf=function|Raw_->Fmt.stringppf"Raw"|OCaml_->Fmt.stringppf"OCaml"|Cram_->Fmt.stringppf"Cram"|Toplevel_->Fmt.stringppf"Toplevel"|Include_->Fmt.stringppf"Include"letdumpppf({loc;section;labels;contents;value;_}asb)=Fmt.pfppf"{@[loc: %a;@ section: %a;@ labels: %a;@ header: %a;@ contents: %a;@ \
value: %a@]}"Stable_printer.Location.pplocFmt.(Dump.optiondump_section)sectionFmt.Dump.(listLabel.pp)labelsFmt.(Dump.optionHeader.pp)(header b)Fmt.Dump.(liststring)contentsdump_valuevalueletpp_contents?syntax:_ppft=Fmt.(list~sep:(any"\n")string)ppft.contentsletrecerror_padding=function|[]->[]|[o;`Outputpadding]whenUtil.String.all_blankpadding->[o]|x::xs->letxs=error_paddingxsinx::xsletcompute_delimiter~base_delimoutputs =lets=Format.asprintf"%a"(Format.pp_print_list(Output.pp~pad:0))outputsinletis_inadequatedelim=Astring.String.is_infix~affix:("]"^delim^"}")sinletrecloopn=letdelim=matchnwith 0->base_delim|n->Format.sprintf"%s_%d"base_delimninifis_inadequate delimthenloop(n+1)elsedeliminloop0letpp_error?syntax?delimppfoutputs=match syntaxwith|SomeSyntax.Markdown->Fmt.pfppf"```\n```mdx-error\n%a\n"Fmt.(list~sep:(any"\n")Output.pp)outputs|SomeSyntax.Mli|SomeSyntax.Mld->leterr_delim=compute_delimiter~base_delim:"err"outputsinFmt.pfppf"]%a[\n{%s@mdx-error[\n%a\n]%s}"Fmt.(optionstring)delim err_delimFmt.(list~sep:(any"\n")Output.pp)outputserr_delim|_->()lethas_output t=matcht.valuewith|OCaml{errors=[];_}->false|OCaml{errors=_;_}->true|_->falseletpp_value?syntaxppft=letdelim=t.deliminmatch t.valuewith|OCaml{errors=[];_}->()|OCaml{errors;_}->leterrors=error_paddingerrorsinpp_error?syntax?delimppferrors|_->()letpp_footer ?syntaxppft=letdelim=ifhas_outputtthen(pp_value?syntaxppft;None)elset.deliminmatchsyntaxwith|SomeSyntax.Mli|SomeSyntax.Mld->Fmt.pfppf"]%a}"Fmt.(optionstring)delim|SomeSyntax.Cram->Fmt.stringppf"\n"|SomeSyntax.Markdown|None->Fmt.stringppf"```\n"letpp_legacy_labelsppf=function|[]->()|l->Fmt.pfppf" %a"Fmt.(list~sep:(any",")Label.pp)lletpp_labels?syntaxppflabels=match syntax with|SomeSyntax.Mli|SomeSyntax.Mld->Fmt.(list~sep:(any",")Label.pp)ppflabels|SomeSyntax.Cram ->(matchlabelswith|[]->()|[Non_detNone]->Fmt.pfppf"<-- non-deterministic\n"|[Non_det(SomeNd_output)]->Fmt.pfppf"<-- non-deterministic output\n"|[Non_det(SomeNd_command)]->Fmt.pfppf"<-- non-deterministic command\n"|_->failwith"cannot happen: checked during parsing")|SomeSyntax.Markdown|None->(matchlabelswith|[]->()|l->Fmt.pfppf"<!-- $MDX %a -->\n"Fmt.(list~sep:(any",")Label.pp)l)letpp_header?syntaxppft=matchsyntaxwith|SomeSyntax.Mli|SomeSyntax.Mld->letlang_headers,other_labels=List.partition(function Label.Language_tag_->true|_->false)t.labelsinletpp_lang_headerppf=function|[]->()|[l]->Fmt.pfppf"@%a"Label.ppl|_->failwith "Multiple language tags, unsupported"inletpp_labelsppf=function|[]->()|labels->Fmt.pfppf" %a"(pp_labels?syntax)labelsinFmt.pfppf"{%a%a%a["Fmt.(optionstring)t.delimpp_lang_headerlang_headerspp_labelsother_labels|SomeSyntax.Cram->pp_labels?syntaxppft.labels|Some Syntax.Markdown|None->ift.legacy_labelsthenFmt.pfppf"```%a%a"Fmt.(optionHeader.pp)(headert)pp_legacy_labelst.labelselseFmt.pfppf"%a```%a"(pp_labels?syntax)t.labelsFmt.(optionHeader.pp)(headert)letpp?syntaxppfb=pp_header?syntax ppfb;pp_contents?syntaxppfb;pp_footer?syntax ppfbletdirectoryt=t.dirletfilet=matcht.valuewithIncludet->Somet.file_included|_->Noneletnon_dett=matcht.valuewith|OCamlb->b.non_det|Cramb->b.non_det|Toplevelb->b.non_det|Include_|Raw_->Noneletskipt=t.skipletset_variables t=t.set_variablesletunset_variablest=t.unset_variablesletvaluet=t.valueletsectiont=t.sectionletguess_ocaml_kindcontents=letrecaux=function|[]->`Code|h::t->leth=String.trimhinifh=""thenauxtelseifString.lengthh>1&&h.[0]='#'then`Toplevelelse`Codeinauxcontentsletrecends_by_semi_semi=function|[]->false|[h]->Astring.String.is_suffix~affix:";;"h|_::xs->ends_by_semi_semixsletversion_enabledversions=let+curr_version=Ocaml_version.of_stringSys.ocaml_versioninList.for_all(fun(op,v)->Label.Relation.compareop(Ocaml_version.comparecurr_versionv)0)versionsletos_type_enabledos_type=matchos_typewith|Some(op,v)->Label.Relation.compareop(String.compare(String.lowercase_asciiSys.os_type)(String.lowercase_asciiv))0|None->trueletget_labelf(labels:Label.tlist)=Util.List.find_mapflabelsletlabel_not_allowed ~loc~label~kind=loc_error ~loc"`%s` label is not allowed for %s blocks."labelkindletlabel_required~loc~label~kind=loc_error ~loc"`%s` label is required for %s blocks."labelkindletcheck_not_set~locmsg=function|Some_->loc_error~loc"%s"msg|None->Ok()letcheck_no_errors~loc=function|[]->Ok()|_::_->loc_error~loc"error block cannot be attached to a non-OCaml block"typeblock_config={non_det:Label.non_detoption;part:stringoption;env:stringoption;dir:stringoption;skip:bool;version :(Label.Relation.t*Ocaml_version.t)list;os_type:(Label.Relation.t*string)option;set_variables:(string*string)list;unset_variables:stringlist;file_inc:stringoption;}letget_block_config l={non_det=get_label(function|Non_det(Somex)->Somex|Non_detNone->SomeLabel.default_non_det|_->None)l;part=get_label(functionPartx->Somex|_->None)l;env=get_label(functionEnvx->Somex|_->None)l;dir=get_label(functionDirx->Somex|_->None)l;skip=List.exists(functionLabel.Skip->true|_->false)l;version=List.filter_map(functionLabel.Version(x,y)->Some(x,y)|_->None)l;os_type=get_label(functionOs_type(x,y)->Some(x,y)|_->None)l;set_variables=List.filter_map(functionLabel.Set(v,x)->Some(v,x)|_->None)l;unset_variables=List.filter_map(functionLabel.Unsetx->Somex|_->None)l;file_inc=get_label(functionFilex->Somex|_->None)l;}letmk_ocaml~loc~config~header~contents ~errors=letkind="OCaml" inmatch configwith|{file_inc=None;part=None;env;non_det;_}->((* TODO: why does this call guess_ocaml_kind when infer_block already did? *)matchguess_ocaml_kindcontentswith|`Code->Ok(OCaml{env=Ocaml_env.mkenv;non_det;errors;header})|`Toplevel->loc_error~loc"toplevel syntax is not allowed in OCaml blocks.")|{file_inc=Some_;_}->label_not_allowed~loc~label:"file"~kind|{part=Some_;_}->label_not_allowed~loc~label:"part"~kindletmk_cram~loc?language~config~header~errors()=letkind="shell"inmatch configwith|{file_inc=None;part=None;env=None;non_det;_}->let+()=check_no_errors~locerrorsinletlanguage =Util.Option.valuelanguage~default:(matchheaderwith|Some(Header.Shelllanguage)->language|_->`Sh)inCram{language;non_det}|{file_inc =Some_;_}->label_not_allowed~loc~label:"file"~kind|{part=Some_;_}->label_not_allowed~loc~label:"part"~kind|{env=Some_;_}->label_not_allowed~loc~label:"env"~kindletmk_toplevel~loc~config~contents~errors=letkind="toplevel" inmatchconfigwith|{file_inc=None;part=None;env;non_det;_}->(match guess_ocaml_kindcontentswith|`Code->loc_error~loc"invalid toplevel syntax in toplevel blocks."|`Toplevel->let+()=check_no_errors~locerrorsinToplevel {env=Ocaml_env.mkenv;non_det})|{file_inc=Some_;_}->label_not_allowed~loc~label:"file"~kind|{part=Some_;_}->label_not_allowed~loc~label:"part"~kindletmk_include~loc~config~header~errors =letkind="include" inmatch configwith|{file_inc=Somefile_included;part;non_det=None;env =None;_}->(let*()=check_no_errors~locerrorsinmatch header with|SomeHeader.OCaml->letfile_kind=Fk_ocaml{part_included=part}inOk(Include{file_included;file_kind})|_->(matchpartwith|None ->letfile_kind=Fk_other{header}inOk(Include{file_included;file_kind})|Some _->label_not_allowed~loc~label:"part"~kind:"non-OCaml include"))|{file_inc=None;_}->label_required~loc~label:"file"~kind|{non_det=Some _;_}->label_not_allowed~loc~label:"non-deterministic"~kind|{env=Some_;_}->label_not_allowed~loc~label:"env"~kindletinfer_block~loc~config~header~contents ~errors=matchconfigwith|{file_inc=Some_;_}->mk_include~loc~config~header~errors|{file_inc =None;part;_}->(match headerwith|Some(Header.Shelllanguage)->mk_cram ~loc~language~config~header~errors()|SomeHeader.OCaml->(matchguess_ocaml_kindcontentswith|`Code->mk_ocaml~loc~config~header~contents ~errors|`Toplevel ->mk_toplevel~loc~config~contents~errors)|_->let*()=check_not_set~loc"`part` label requires a `file` label."partinlet+()=check_no_errors~locerrorsinRaw{header})letmk~loc~section ~labels~legacy_labels~header~delim~contents~errors=letblock_kind=get_label(functionBlock_kindx->Somex|_->None)labelsinletconfig=get_block_configlabelsinlet*value=matchblock_kindwith|SomeOCaml ->mk_ocaml~loc~config~header~contents ~errors|SomeCram->mk_cram~loc~config~header~errors ()|Some Toplevel ->mk_toplevel~loc~config~contents~errors|SomeInclude->mk_include~loc~config~header~errors|None->infer_block~loc~config~header~contents ~errorsinlet+ version_enabled=version_enabledconfig.versioninletos_type_enabled =os_type_enabledconfig.os_typein{loc;section;dir=config.dir;labels;legacy_labels;contents;skip=config.skip;version_enabled;os_type_enabled;set_variables=config.set_variables;unset_variables=config.unset_variables;delim;value;}let mk_include~loc~section~labels=matchget_label(functionFilex->Somex|_->None)labelswith|Somefile_inc->letheader=Header.infer_from_filefile_incinmk~loc~section~labels~legacy_labels:false~header~contents:[]~errors:[]~delim:None|None->label_required~loc~label:"file"~kind:"include"letparse_labels~label_cmt~legacy_labels=match(label_cmt,legacy_labels)with|Somelabel_cmt,""->let+labels=Label.of_stringlabel_cmtin(labels,false)|Some_,_->Error[`Msg"cannot mix both block labels syntax"]|None,l->let+labels=Label.of_stringlin(labels,true)letfrom_rawraw=matchrawwith|Raw.Include{loc;section;labels}->let*labels=locate_errors~loc(Label.of_stringlabels)inUtil.Result.to_error_list@@mk_include~loc~section~labels|Raw.Any {loc;section;header;contents;label_cmt;legacy_labels;errors }->letheader=Header.of_stringheaderinlet*labels,legacy_labels=locate_errors ~loc(parse_labels~label_cmt~legacy_labels)inUtil.Result.to_error_list@@mk~loc~section~header~contents~labels~legacy_labels~errors~delim:Noneletis_active?section:st=let active=matchswith|Somep->(matcht.sectionwith|Somes->Re.execp(Re.Perl.compile_patp)(snds)|None->Re.execp(Re.Perl.compile_patp)"")|None->trueinactive&&t.version_enabled&&t.os_type_enabled&¬t.skip