123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202(* Yoann Padioleau
*
* Copyright (C) 2019 r2c
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation, with the
* special exception on linking described in file license.txt.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the file
* license.txt for more details.
*)openCommonmoduleAst=Ast_genericmoduleLib=Lib_ast_genericmoduleMV=Metavars_genericmoduleMVGen=Metavars_fuzzy(*****************************************************************************)(* Prelude *)(*****************************************************************************)(*
* This module makes it possible to match one AST against another AST
* providing a kind of grep but at a syntactical level.
*)letverbose=reffalse(*****************************************************************************)(* Wrappers *)(*****************************************************************************)letpr2,_pr2_once=Common2.mk_pr2_wrappersverbose(*****************************************************************************)(* The functor argument *)(*****************************************************************************)moduleXMATCH=struct(* ------------------------------------------------------------------------*)(* Combinators history *)(* ------------------------------------------------------------------------*)(*
* version0:
* type ('a, 'b) matcher = 'a -> 'b -> bool
*
* This just lets you know if you matched something.
*
* version1:
* type ('a, 'b) matcher = 'a -> 'b -> unit -> ('a, 'b) option
*
* The Maybe monad.
*
* version2:
* type ('a, 'b) matcher = 'a -> 'b -> binding -> binding list
*
* Why not returning a binding option ? because I may need at some
* point to return multiple possible bindings for one matching code.
* For instance with the pattern do 'f(..., X, ...)', X could be binded
* to different parts of the code.
*
* Note that the empty list means a match failure.
*)typetin=MV.metavars_bindingtype'xtout=('x*MV.metavars_binding)listtype('a,'b)matcher='a->'b->tin->('a*'b)toutlet((>>=):(tin->('a*'b)tout)->(('a*'b)->(tin->('c*'d)tout))->(tin->('c*'d)tout))=funm1m2->funtin->(* old:
match m1 tin with
| None -> None
| Some (a,b) ->
m2 (a, b) tin
*)(* let's get a list of possible environment match (could be
* the empty list when it didn't match, playing the role None
* had before)
*)letxs=m1tinin(* try m2 on each possible returned bindings *)letxxs=xs|>List.map(fun((a,b),binding)->m2(a,b)binding)inList.flattenxxslet(>||>)m1m2=funtin->(* CHOICE
let xs = m1 tin in
if null xs
then m2 tin
else xs
*)(* opti? use set instead of list *)m1tin@m2tinletreturn(a,b)=funtin->(* old: Some (a,b) *)[(a,b),tin]letfail=fun_tin->(* old: None *)[](* ------------------------------------------------------------------------*)(* Environment *)(* ------------------------------------------------------------------------*)(* pre: both 'a' and 'b' contains only regular JS code. There is no
* metavariables in them.
*)letequal_ast_binded_codeab=matcha,bwith|Ast.E_,Ast.E_|Ast.N_,Ast.N_|Ast.S_,Ast.S_->(* Note that because we want to retain the position information
* of the matched code in the environment (e.g. for the -pvar
* sgrep command line argument), we can not just use the
* generic '=' OCaml operator as 'a' and 'b' may represent
* the same code but they will contain leaves in their AST
* with different position information. So before doing
* the comparison we just need to remove/abstract-away
* the line number information in each ASTs.
*)leta=Lib.abstract_position_info_anyainletb=Lib.abstract_position_info_anybina=*=b|_,_->falseletcheck_and_add_metavar_binding((mvar:MV.mvar),valu)=funtin->matchCommon2.assoc_optmvartinwith|Somevalu'->(* Should we use php_vs_php itself for comparing the binded code ?
* Hmmm, we can't because it leads to a circular dependencies.
* Moreover here we know both valu and valu' are regular PHP code,
* not PHP patterns, so we can just use the generic '=' of OCaml.
*)ifequal_ast_binded_codevaluvalu'thenSometinelseNone|None->(* first time the metavar is binded, just add it to the environment *)Some(Common2.insert_assoc(mvar,valu)tin)let(envf:(MV.mvarAst.wrap,Ast.any)matcher)=fun(mvar,imvar)any->funtin->matchcheck_and_add_metavar_binding(mvar,any)tinwith|None->pr2(spf"envf: fail, %s"mvar);failtin|Somenew_binding->pr2(spf"envf: success, %s"mvar);return((mvar,imvar),any)new_bindinglet(envf2:(MV.mvarAst.wrap,Ast.any*Ast.any)matcher)=fun(mvar,imvar)(any1,any2)->funtin->matchcheck_and_add_metavar_binding(mvar,any1)tinwith|None->pr2(spf"envf2: fail, %s"mvar);failtin|Somenew_binding->pr2(spf"envf2: success, %s"mvar);return((mvar,imvar),(any1,any2))new_bindinglettokenfab=(* dont care about position, space/indent/comment isomorphism *)return(a,b)end(*****************************************************************************)(* Entry point *)(*****************************************************************************)moduleMATCH=Generic_vs_generic.GENERIC_VS_GENERIC(XMATCH)type('a,'b)matcher='a->'b->MV.metavars_bindinglistlet(extract_bindings:'aXMATCH.tout->MV.metavars_bindinglist)=funtout->tout|>List.map(fun(_term,env)->env)(* todo: should maybe have a match_any_any *)letmatch_e_epatterne=letenv=MVGen.empty_environment()inMATCH.m_exprpatterneenv|>extract_bindingsletmatch_st_stpatterne=letenv=MVGen.empty_environment()inMATCH.m_stmtpatterneenv|>extract_bindings