mirror of
https://github.com/comby-tools/comby.git
synced 2024-10-05 17:17:35 +03:00
Initial code commit
This commit is contained in:
parent
224430cdff
commit
f5c03e19ef
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
.merlin
|
||||
_build
|
||||
*.install
|
2
LICENSE
2
LICENSE
@ -41,7 +41,7 @@
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
of this License, Derivative Works shall not include works that refoo
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
|
27
Makefile
Normal file
27
Makefile
Normal file
@ -0,0 +1,27 @@
|
||||
all: build comby
|
||||
|
||||
build:
|
||||
@dune build
|
||||
|
||||
comby comby-server:
|
||||
@ln -s _build/install/default/bin/$@ ./$@
|
||||
|
||||
install:
|
||||
@dune install
|
||||
|
||||
doc:
|
||||
@dune build @doc
|
||||
|
||||
test:
|
||||
@dune runtest
|
||||
|
||||
clean:
|
||||
@dune clean
|
||||
|
||||
uninstall:
|
||||
@dune uninstall
|
||||
|
||||
promote:
|
||||
@dune promote
|
||||
|
||||
.PHONY: all build install test clean promote
|
170
README.md
170
README.md
@ -1 +1,171 @@
|
||||
# comby
|
||||
|
||||
## Build
|
||||
|
||||
- Install [opam](https://opam.ocaml.org/doc/Install.html)
|
||||
|
||||
- Create a new switch if you don't have OCaml installed
|
||||
|
||||
```
|
||||
opam init
|
||||
opam switch create 4.05.0 4.05.0
|
||||
```
|
||||
|
||||
- Install dependencies
|
||||
|
||||
### Linux
|
||||
|
||||
```
|
||||
sudo apt-get install pkg-config pcre
|
||||
```
|
||||
|
||||
### Mac
|
||||
|
||||
```
|
||||
brew install pkg-config pcre
|
||||
```
|
||||
|
||||
- Install opam libraries
|
||||
|
||||
```
|
||||
opam install ppx_deriving_yojson
|
||||
opam install core
|
||||
opam install ppxlib
|
||||
opam install ppx_deriving
|
||||
opam install angstrom
|
||||
opam install hack_parallel
|
||||
opam install opium
|
||||
opam install pcre
|
||||
opam install oasis
|
||||
```
|
||||
|
||||
- Install mparser
|
||||
|
||||
```
|
||||
git clone https://github.com/comby-tools/mparser
|
||||
oasis setup
|
||||
ocaml setup.ml -configure --enable-pcre --enable-re
|
||||
ocaml setup.ml -build
|
||||
ocaml setup.ml -install
|
||||
```
|
||||
|
||||
- Build and test
|
||||
|
||||
```
|
||||
make
|
||||
make test
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
`./comby MATCH_TEMPLATE REWRITE_TEMPLATE` by default rewrites all files
|
||||
in-place in the current directory and all subdirectories.
|
||||
|
||||
- Adding `-f .ml` will rewrite all files ending in `.ml`. Adding `-f README.md` will rewrite all files
|
||||
ending in `README.md`
|
||||
|
||||
- Input can also be fed via `-stdin` instead of rewriting files:
|
||||
|
||||
```
|
||||
./comby -stdin 'printf(":[1] :[2]")' 'printf("comby, :[1]")' << EOF 2> /dev/null
|
||||
int main(void) {
|
||||
printf("hello world");
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
Outputs:
|
||||
|
||||
```
|
||||
int main(void) {
|
||||
printf("comby, hello");
|
||||
}
|
||||
```
|
||||
- Adding a `-json` flag will output JSON content of the rewrite:
|
||||
|
||||
```
|
||||
[
|
||||
{
|
||||
"range": {
|
||||
"start": { "offset": 19, "line": -1, "column": -1 },
|
||||
"end": { "offset": 41, "line": -1, "column": -1 }
|
||||
},
|
||||
"replacement_content": "printf(\"comby, hello\")",
|
||||
"environment": [
|
||||
[
|
||||
"1",
|
||||
{
|
||||
"value": "hello",
|
||||
"range": {
|
||||
"start": { "offset": 15, "line": -1, "column": -1 },
|
||||
"end": { "offset": 20, "line": -1, "column": -1 }
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
- Adding a `-match-only` flag will output JSON content of all matches:
|
||||
|
||||
```
|
||||
[
|
||||
{
|
||||
"range": {
|
||||
"start": { "offset": 19, "line": 1, "column": 20 },
|
||||
"end": { "offset": 40, "line": 1, "column": 41 }
|
||||
},
|
||||
"environment": [
|
||||
[
|
||||
"1",
|
||||
{
|
||||
"value": "hello",
|
||||
"range": {
|
||||
"start": { "offset": 27, "line": 1, "column": 28 },
|
||||
"end": { "offset": 32, "line": 1, "column": 33 }
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"2",
|
||||
{
|
||||
"value": "world",
|
||||
"range": {
|
||||
"start": { "offset": 33, "line": 1, "column": 34 },
|
||||
"end": { "offset": 38, "line": 1, "column": 39 }
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"matched": "printf(\"hello world\")"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
See other flags for more options with `./comby -h`:
|
||||
|
||||
```
|
||||
Run a rewrite pass.
|
||||
|
||||
comby [MATCH_TEMPLATE REWRITE_TEMPLATE]
|
||||
|
||||
=== flags ===
|
||||
|
||||
[-directory path] Run on files in a directory. Default is current
|
||||
directory: /Users/rvt/comby
|
||||
[-filter extensions] CSV of extensions to include
|
||||
[-jobs n] Number of worker processes
|
||||
[-json] Output JSON format for matches or rewrite text to stdout
|
||||
[-match-only] Only perform matching (ignore rewrite templates)
|
||||
[-rule rule] Apply rules to matches. Respects -f
|
||||
[-sequential] Run sequentially
|
||||
[-stdin] Read source from stdin
|
||||
[-templates path] CSV of directories containing templates
|
||||
[-timeout seconds] Set match timeout on a source. Default: 3
|
||||
[-verbose] Log to /tmp/comby.out
|
||||
[-build-info] print info about this build and exit
|
||||
[-version] print the version of this build and exit
|
||||
[-help] print this help text and exit
|
||||
(alias: -?)
|
||||
```
|
||||
|
13
comby.opam
Normal file
13
comby.opam
Normal file
@ -0,0 +1,13 @@
|
||||
opam-version: "2.0"
|
||||
version: "0.1.0"
|
||||
maintainer: "rvantonder@gmail.com"
|
||||
authors: ["Rijnard van Tonder"]
|
||||
homepage: "https://github.com/comby-tools/comby"
|
||||
bug-reports: "https://github.com/comby-tools/comby/issues"
|
||||
dev-repo: "git+https://github.com/comby-tools/comby.git"
|
||||
license: "Apache-2.0"
|
||||
build: [["jbuilder" "build" "-p" name "-j" jobs "@install"]]
|
||||
depends: [
|
||||
"mparser"
|
||||
"core"
|
||||
]
|
3
dune
Normal file
3
dune
Normal file
@ -0,0 +1,3 @@
|
||||
(env
|
||||
(dev
|
||||
(flags (:standard -w A-3-4-32-34-39-40-41-42-44-45-48-49-50-57))))
|
2
dune-project
Normal file
2
dune-project
Normal file
@ -0,0 +1,2 @@
|
||||
(lang dune 1.8)
|
||||
(name comby)
|
5
lib/comby.ml
Normal file
5
lib/comby.ml
Normal file
@ -0,0 +1,5 @@
|
||||
module Language = Language
|
||||
module Matchers = Matchers
|
||||
module Match = Match
|
||||
module Rewriter = Rewriter
|
||||
module Statistics = Statistics
|
5
lib/comby.mli
Normal file
5
lib/comby.mli
Normal file
@ -0,0 +1,5 @@
|
||||
module Language = Language
|
||||
module Matchers = Matchers
|
||||
module Match = Match
|
||||
module Rewriter = Rewriter
|
||||
module Statistics = Statistics
|
14
lib/dune
Normal file
14
lib/dune
Normal file
@ -0,0 +1,14 @@
|
||||
(library
|
||||
(name comby)
|
||||
(public_name comby)
|
||||
(preprocess (pps ppx_deriving.show ppx_deriving.eq ppx_sexp_conv))
|
||||
(libraries
|
||||
ppxlib
|
||||
core
|
||||
mparser
|
||||
mparser.pcre
|
||||
comby.matchers
|
||||
comby.rewriter
|
||||
comby.match
|
||||
comby.language
|
||||
comby.statistics))
|
30
lib/language/ast.ml
Normal file
30
lib/language/ast.ml
Normal file
@ -0,0 +1,30 @@
|
||||
open Core
|
||||
|
||||
type atom =
|
||||
| Variable of string
|
||||
| String of string
|
||||
[@@deriving sexp]
|
||||
|
||||
type antecedent = atom
|
||||
[@@deriving sexp]
|
||||
|
||||
type expression =
|
||||
| True
|
||||
| False
|
||||
| Equal of atom * atom
|
||||
| Not_equal of atom * atom
|
||||
| Match of atom * (antecedent * consequent) list
|
||||
| RewriteTemplate of string
|
||||
| Rewrite of atom * (antecedent * consequent) list
|
||||
| Rewrite_old of atom * ((atom * atom * expression list) list)
|
||||
and consequent = expression list
|
||||
[@@deriving sexp]
|
||||
|
||||
let (=) left right = Equal (left, right)
|
||||
|
||||
let (<>) left right = Not_equal (left, right)
|
||||
|
||||
(* Semantics for rewrite_rule with a list more than one expression is
|
||||
undefined *)
|
||||
type t = expression list
|
||||
[@@deriving sexp]
|
5
lib/language/dune
Normal file
5
lib/language/dune
Normal file
@ -0,0 +1,5 @@
|
||||
(library
|
||||
(name language)
|
||||
(public_name comby.language)
|
||||
(preprocess (pps ppx_deriving.show ppx_sexp_conv ppx_sexp_message ppx_deriving_yojson))
|
||||
(libraries comby.parsers comby.match comby.rewriter comby.matchers ppxlib core mparser mparser.pcre yojson ppx_deriving_yojson ppx_deriving_yojson.runtime))
|
25
lib/language/parser.ml
Normal file
25
lib/language/parser.ml
Normal file
@ -0,0 +1,25 @@
|
||||
open Core
|
||||
|
||||
open MParser
|
||||
open MParser_PCRE.Tokens
|
||||
|
||||
open Ast
|
||||
|
||||
let variable_parser s =
|
||||
(string Syntax.variable_left_delimiter
|
||||
>> (many (alphanum <|> char '_') |>> String.of_char_list)
|
||||
<< string Syntax.variable_right_delimiter) s
|
||||
|
||||
let value_parser s =
|
||||
string_literal s
|
||||
|
||||
let operator_parser s =
|
||||
((string Syntax.equal)
|
||||
<|> (string Syntax.not_equal)) s
|
||||
|
||||
let atom_parser s =
|
||||
((variable_parser >>= fun variable -> return (Variable variable))
|
||||
<|> (value_parser >>= fun value -> return (String value))) s
|
||||
|
||||
let rewrite_template_parser s =
|
||||
(value_parser >>= fun value -> return (RewriteTemplate value)) s
|
233
lib/language/rule.ml
Normal file
233
lib/language/rule.ml
Normal file
@ -0,0 +1,233 @@
|
||||
open Core
|
||||
open MParser
|
||||
open MParser_PCRE.Tokens
|
||||
|
||||
open Match
|
||||
open Matchers
|
||||
|
||||
open Rewriter
|
||||
|
||||
open Ast
|
||||
open Parser
|
||||
|
||||
type result = bool * environment option
|
||||
|
||||
let sat = fst
|
||||
|
||||
let result_env = snd
|
||||
|
||||
let match_configuration_of_syntax template =
|
||||
(* decide match configuration based on whether there are holes *)
|
||||
let antecedent_contains_hole_syntax case =
|
||||
String.is_substring case ~substring:Syntax.variable_left_delimiter
|
||||
in
|
||||
if antecedent_contains_hole_syntax template then
|
||||
Configuration.create ~match_kind:Fuzzy ()
|
||||
else
|
||||
Configuration.create ~match_kind:Exact ()
|
||||
|
||||
let merge_match_environments matches environment' =
|
||||
List.map matches ~f:(fun { environment; _ } ->
|
||||
Environment.merge environment environment')
|
||||
|
||||
type rewrite_context =
|
||||
{ variable : string }
|
||||
|
||||
let rec apply ?(matcher = (module Matchers.Generic : Matchers.Matcher)) predicates env =
|
||||
let open Option in
|
||||
let module Matcher = (val matcher : Matchers.Matcher) in
|
||||
|
||||
let equal_in_environment var value env =
|
||||
match Environment.lookup env var with
|
||||
| None -> false, Some env
|
||||
| Some var_value -> String.equal var_value value, Some env
|
||||
in
|
||||
(* accepts only one expression *)
|
||||
let rec rule_match ?(rewrite_context : rewrite_context option) env =
|
||||
function
|
||||
| True -> true, Some env
|
||||
| False -> false, Some env
|
||||
| Equal (Variable var, String value)
|
||||
| Equal (String value, Variable var) ->
|
||||
equal_in_environment var value env
|
||||
| Equal (String left, String right) ->
|
||||
String.equal left right, Some env
|
||||
| Equal (Variable left, Variable right) ->
|
||||
let result =
|
||||
Environment.lookup env left >>= fun left ->
|
||||
Environment.lookup env right >>= fun right ->
|
||||
return (String.equal left right)
|
||||
in
|
||||
Option.value result ~default:false, Some env
|
||||
| Not_equal (left, right) ->
|
||||
let sat, env = rule_match env (Equal (left, right)) in
|
||||
not sat, env
|
||||
| Match (Variable variable, cases) ->
|
||||
let result =
|
||||
Environment.lookup env variable >>= fun source ->
|
||||
List.find_map cases ~f:(fun (template, case_expression) ->
|
||||
match template with
|
||||
| String template ->
|
||||
begin
|
||||
let configuration = match_configuration_of_syntax template in
|
||||
Matcher.all ~configuration ~template ~source |> function
|
||||
| [] -> None
|
||||
| matches ->
|
||||
(* merge environments. overwrite behavior is undefined *)
|
||||
let fold_matches (sat, out) { environment; _ } =
|
||||
let fold_cases (sat, out) predicate =
|
||||
if sat then
|
||||
let env' = Environment.merge env environment in
|
||||
rule_match ?rewrite_context env' predicate
|
||||
else
|
||||
(sat, out)
|
||||
in
|
||||
List.fold case_expression ~init:(sat, out) ~f:fold_cases
|
||||
in
|
||||
List.fold matches ~init:(true, None) ~f:fold_matches
|
||||
|> Option.some
|
||||
end
|
||||
| Variable _ ->
|
||||
failwith "| :[hole] is invalid. Maybe you meant to put quotes")
|
||||
in
|
||||
Option.value_map result ~f:ident ~default:(false, Some env)
|
||||
| Match (String template, cases) ->
|
||||
let source, _ = Rewriter.Rewrite_template.substitute template env in
|
||||
let fresh_var = Uuid.(Fn.compose to_string create ()) in
|
||||
let env = Environment.add env fresh_var source in
|
||||
rule_match env (Match (Variable fresh_var, cases))
|
||||
| RewriteTemplate rewrite_template ->
|
||||
begin
|
||||
match rewrite_context with
|
||||
| None -> false, None
|
||||
| Some { variable; _ } ->
|
||||
(* FIXME(RVT) assumes only contextual rewrite for now. *)
|
||||
let env =
|
||||
Rewrite_template.substitute rewrite_template env
|
||||
|> fst
|
||||
|> fun replacement' ->
|
||||
Environment.update env variable replacement'
|
||||
|> Option.some
|
||||
in
|
||||
true, env
|
||||
end
|
||||
| Rewrite (Variable variable, cases) ->
|
||||
let result =
|
||||
Environment.lookup env variable >>= fun source ->
|
||||
List.find_map cases ~f:(fun (template, case_expression) ->
|
||||
match template with
|
||||
| String template ->
|
||||
begin
|
||||
let configuration = match_configuration_of_syntax template in
|
||||
let matches = Matcher.all ~configuration ~template ~source in
|
||||
if List.is_empty matches then
|
||||
None
|
||||
else
|
||||
let fold_cases (sat, out) predicate =
|
||||
if sat then
|
||||
let env =
|
||||
match out with
|
||||
| Some out -> Environment.merge out env
|
||||
| None -> env
|
||||
in
|
||||
match matches with
|
||||
| { environment; _ } :: _ ->
|
||||
let env = Environment.merge env environment in
|
||||
rule_match ~rewrite_context:{ variable } env predicate
|
||||
| _ ->
|
||||
sat, out
|
||||
else
|
||||
(sat, out)
|
||||
in
|
||||
List.fold case_expression ~init:(true, None) ~f:fold_cases
|
||||
|> Option.some
|
||||
end
|
||||
| Variable _ ->
|
||||
failwith "| :[hole] is invalid. Maybe you meant to put quotes")
|
||||
in
|
||||
Option.value_map result ~f:ident ~default:(false, Some env)
|
||||
| Rewrite _ -> failwith "TODO"
|
||||
| Rewrite_old _ -> failwith "Deprecated"
|
||||
in
|
||||
List.fold predicates ~init:(true, None) ~f:(fun (sat, out) predicate ->
|
||||
if sat then
|
||||
let env =
|
||||
Option.value_map out
|
||||
~f:(fun out -> Environment.merge out env)
|
||||
~default:env
|
||||
in
|
||||
rule_match env predicate
|
||||
else
|
||||
(sat, out))
|
||||
|
||||
|
||||
let make_equality_expression operator left right =
|
||||
if String.equal operator Syntax.equal then
|
||||
return (Equal (left, right))
|
||||
else if
|
||||
String.equal operator Syntax.not_equal then
|
||||
return (Not_equal (left, right))
|
||||
else
|
||||
let message =
|
||||
Format.sprintf
|
||||
"Unhandled operator %s. Did you mean %s or %s?"
|
||||
operator
|
||||
Syntax.equal
|
||||
Syntax.not_equal in
|
||||
fail message
|
||||
|
||||
let create rule =
|
||||
let operator_parser =
|
||||
spaces >> atom_parser >>= fun left ->
|
||||
spaces >> operator_parser >>= fun operator ->
|
||||
spaces >> atom_parser << spaces >>= fun right ->
|
||||
make_equality_expression operator left right << spaces
|
||||
in
|
||||
let true' = spaces >> string Syntax.true' << spaces |>> fun _ -> True in
|
||||
let false' = spaces >> string Syntax.false' << spaces |>> fun _ -> False in
|
||||
let rec expression_parser s =
|
||||
choice
|
||||
[ pattern_parser
|
||||
(* string literals are ambiguous, so attempt to parse operator first *)
|
||||
; attempt operator_parser
|
||||
; rewrite_template_parser
|
||||
; true'
|
||||
; false'
|
||||
]
|
||||
s
|
||||
and pattern_parser s =
|
||||
let case_parser : (atom * expression list, unit) parser =
|
||||
spaces >> string Syntax.pipe_operator >>
|
||||
spaces >> atom_parser << spaces << string Syntax.arrow << spaces >>= fun antecedent ->
|
||||
spaces >> comma_sep expression_parser << spaces |>> fun consequent ->
|
||||
antecedent, consequent
|
||||
in
|
||||
let pattern keyword =
|
||||
string keyword << spaces >> atom_parser << spaces << char '{' << spaces
|
||||
>>= fun atom ->
|
||||
many1 case_parser
|
||||
<< char '}' << spaces
|
||||
>>= fun cases -> return (atom, cases)
|
||||
in
|
||||
let match_pattern =
|
||||
pattern Syntax.start_match_pattern |>> fun (atom, cases) ->
|
||||
(Match (atom, cases))
|
||||
in
|
||||
let rewrite_pattern =
|
||||
pattern Syntax.start_rewrite_pattern |>> fun (atom, cases) ->
|
||||
(Rewrite (atom, cases))
|
||||
in
|
||||
choice [ match_pattern; rewrite_pattern ]
|
||||
s
|
||||
in
|
||||
let rule_parser s =
|
||||
(spaces
|
||||
>> string Syntax.rule_prefix
|
||||
>> spaces
|
||||
>> comma_sep expression_parser
|
||||
<< eof)
|
||||
s
|
||||
in
|
||||
match parse_string rule_parser rule () with
|
||||
| Success rule -> Ok rule
|
||||
| Failed (msg, _) -> Or_error.error_string msg
|
20
lib/language/rule.mli
Normal file
20
lib/language/rule.mli
Normal file
@ -0,0 +1,20 @@
|
||||
open Core
|
||||
|
||||
open Matchers
|
||||
open Match
|
||||
|
||||
open Ast
|
||||
|
||||
type result = bool * environment option
|
||||
|
||||
val sat : result -> bool
|
||||
|
||||
val result_env : result -> environment option
|
||||
|
||||
val create : string -> expression list Or_error.t
|
||||
|
||||
val apply
|
||||
: ?matcher:(module Matcher)
|
||||
-> t
|
||||
-> environment
|
||||
-> result
|
11
lib/language/syntax.ml
Normal file
11
lib/language/syntax.ml
Normal file
@ -0,0 +1,11 @@
|
||||
let rule_prefix = "where"
|
||||
let start_match_pattern = "match"
|
||||
let start_rewrite_pattern = "rewrite"
|
||||
let equal = "=="
|
||||
let not_equal = "!="
|
||||
let variable_left_delimiter = ":["
|
||||
let variable_right_delimiter = "]"
|
||||
let true' = "true"
|
||||
let false' = "false"
|
||||
let pipe_operator = "|"
|
||||
let arrow = "->"
|
5
lib/match/dune
Normal file
5
lib/match/dune
Normal file
@ -0,0 +1,5 @@
|
||||
(library
|
||||
(name match)
|
||||
(public_name comby.match)
|
||||
(preprocess (pps ppx_deriving.show ppx_deriving.eq ppx_sexp_conv ppx_sexp_message ppx_deriving_yojson))
|
||||
(libraries comby.parsers ppxlib core mparser mparser.pcre yojson ppx_deriving_yojson ppx_deriving_yojson.runtime))
|
74
lib/match/environment.ml
Normal file
74
lib/match/environment.ml
Normal file
@ -0,0 +1,74 @@
|
||||
open Core
|
||||
|
||||
module Data = struct
|
||||
type t =
|
||||
{ value : string
|
||||
; range : Range.t
|
||||
}
|
||||
[@@deriving yojson, eq, sexp]
|
||||
end
|
||||
|
||||
open Data
|
||||
type data = Data.t
|
||||
[@@deriving yojson, eq, sexp]
|
||||
|
||||
type t = data Core.String.Map.t
|
||||
|
||||
type t_alist = (string * data) list
|
||||
[@@deriving yojson, eq]
|
||||
|
||||
let create () : t =
|
||||
String.Map.empty
|
||||
|
||||
let vars (env : t) : string list =
|
||||
Map.keys env
|
||||
|
||||
let add ?(range = Range.default) (env : t) (var : string) (value : string) : t =
|
||||
Map.add env ~key:var ~data:{ value; range }
|
||||
|> function
|
||||
| `Duplicate -> env
|
||||
| `Ok env -> env
|
||||
|
||||
let lookup (env : t) (var : string) : string option =
|
||||
Map.find env var
|
||||
|> Option.map ~f:(fun { value; _ } -> value)
|
||||
|
||||
let lookup_range (env : t) (var : string) : Range.t option =
|
||||
Map.find env var
|
||||
|> Option.map ~f:(fun { range; _ } -> range)
|
||||
|
||||
let fold (env : t) =
|
||||
Map.fold env
|
||||
|
||||
let update env var value =
|
||||
Map.change env var ~f:(Option.map ~f:(fun result -> { result with value }))
|
||||
|
||||
let update_range env var range =
|
||||
Map.change env var ~f:(Option.map ~f:(fun result -> { result with range }))
|
||||
|
||||
let to_string env =
|
||||
Map.fold env ~init:"" ~f:(fun ~key:variable ~data:{ value; _ } acc ->
|
||||
Format.sprintf "%s |-> %s\n%s" variable value acc)
|
||||
|
||||
let furthest_match env =
|
||||
Map.fold
|
||||
env
|
||||
~init:0
|
||||
~f:(fun ~key:_ ~data:{ range = { match_start = { offset; _ }; _ }; _ } max ->
|
||||
Int.max offset max)
|
||||
|
||||
let equal env1 env2 =
|
||||
Map.equal Data.equal env1 env2
|
||||
|
||||
let merge env1 env2 =
|
||||
Map.merge_skewed env1 env2 ~combine:(fun ~key:_ v1 _ -> v1)
|
||||
|
||||
let copy env =
|
||||
fold env ~init:(create ()) ~f:(fun ~key ~data:{ value; range } env' ->
|
||||
add ~range env' key value)
|
||||
|
||||
let to_yojson env =
|
||||
Map.to_alist env
|
||||
|> t_alist_to_yojson
|
||||
|
||||
let of_yojson _ = assert false
|
26
lib/match/environment.mli
Normal file
26
lib/match/environment.mli
Normal file
@ -0,0 +1,26 @@
|
||||
type t
|
||||
[@@deriving yojson]
|
||||
|
||||
val create : unit -> t
|
||||
|
||||
val vars : t -> string list
|
||||
|
||||
val add : ?range:Range.t -> t -> string -> string -> t
|
||||
|
||||
val lookup : t -> string -> string option
|
||||
|
||||
val update : t -> string -> string -> t
|
||||
|
||||
val lookup_range : t -> string -> Range.t option
|
||||
|
||||
val update_range : t -> string -> Range.t -> t
|
||||
|
||||
val furthest_match : t -> int
|
||||
|
||||
val equal : t -> t -> bool
|
||||
|
||||
val merge : t -> t -> t
|
||||
|
||||
val copy : t -> t
|
||||
|
||||
val to_string : t -> string
|
14
lib/match/location.ml
Normal file
14
lib/match/location.ml
Normal file
@ -0,0 +1,14 @@
|
||||
open Core
|
||||
|
||||
type t =
|
||||
{ offset : int
|
||||
; line : int
|
||||
; column : int
|
||||
}
|
||||
[@@deriving yojson, eq, sexp]
|
||||
|
||||
let default =
|
||||
{ offset = -1
|
||||
; line = -1
|
||||
; column = -1
|
||||
}
|
6
lib/match/match.ml
Normal file
6
lib/match/match.ml
Normal file
@ -0,0 +1,6 @@
|
||||
module Location = Location
|
||||
module Range = Range
|
||||
module Environment = Environment
|
||||
|
||||
include Types
|
||||
include Match_context
|
67
lib/match/match.mli
Normal file
67
lib/match/match.mli
Normal file
@ -0,0 +1,67 @@
|
||||
module Location : sig
|
||||
type t =
|
||||
{ offset : int
|
||||
; line : int
|
||||
; column : int
|
||||
}
|
||||
[@@deriving yojson, eq, sexp]
|
||||
|
||||
val default : t
|
||||
end
|
||||
|
||||
type location = Location.t
|
||||
[@@deriving yojson, eq, sexp]
|
||||
|
||||
module Range : sig
|
||||
type t =
|
||||
{ match_start : location [@key "start"]
|
||||
; match_end : location [@key "end"]
|
||||
}
|
||||
[@@deriving yojson, eq, sexp]
|
||||
|
||||
val default : t
|
||||
end
|
||||
|
||||
type range = Range.t
|
||||
[@@deriving yojson, eq, sexp]
|
||||
|
||||
module Environment : sig
|
||||
type t
|
||||
[@@deriving yojson, eq]
|
||||
|
||||
val create : unit -> t
|
||||
|
||||
val vars : t -> string list
|
||||
|
||||
val add : ?range:range -> t -> string -> string -> t
|
||||
|
||||
val lookup : t -> string -> string option
|
||||
|
||||
val update : t -> string -> string -> t
|
||||
|
||||
val lookup_range : t -> string -> range option
|
||||
|
||||
val update_range : t -> string -> range -> t
|
||||
|
||||
val furthest_match : t -> int
|
||||
|
||||
val equal : t -> t -> bool
|
||||
|
||||
val copy : t -> t
|
||||
|
||||
val merge : t -> t -> t
|
||||
|
||||
val to_string : t -> string
|
||||
end
|
||||
|
||||
type environment = Environment.t
|
||||
[@@deriving yojson]
|
||||
|
||||
type t =
|
||||
{ range : range
|
||||
; environment : environment
|
||||
; matched : string
|
||||
}
|
||||
[@@deriving yojson]
|
||||
|
||||
val create : unit -> t
|
12
lib/match/match_context.ml
Normal file
12
lib/match/match_context.ml
Normal file
@ -0,0 +1,12 @@
|
||||
type t =
|
||||
{ range : Range.t
|
||||
; environment : Environment.t
|
||||
; matched : string
|
||||
}
|
||||
[@@deriving yojson]
|
||||
|
||||
let create () =
|
||||
{ range = Range.default
|
||||
; environment = Environment.create ()
|
||||
; matched = ""
|
||||
}
|
10
lib/match/range.ml
Normal file
10
lib/match/range.ml
Normal file
@ -0,0 +1,10 @@
|
||||
type t =
|
||||
{ match_start : Location.t [@key "start"]
|
||||
; match_end : Location.t [@key "end"]
|
||||
}
|
||||
[@@deriving yojson, eq, sexp]
|
||||
|
||||
let default =
|
||||
{ match_start = Location.default
|
||||
; match_end = Location.default
|
||||
}
|
8
lib/match/types.ml
Normal file
8
lib/match/types.ml
Normal file
@ -0,0 +1,8 @@
|
||||
type location = Location.t
|
||||
[@@deriving yojson, eq, sexp]
|
||||
|
||||
type range = Range.t
|
||||
[@@deriving yojson, eq, sexp]
|
||||
|
||||
type environment = Environment.t
|
||||
[@@deriving yojson]
|
28
lib/matchers/bash.ml
Normal file
28
lib/matchers/bash.ml
Normal file
@ -0,0 +1,28 @@
|
||||
open MParser
|
||||
|
||||
module Syntax = struct
|
||||
include Generic.Syntax
|
||||
|
||||
let user_defined_delimiters =
|
||||
[ ("if", "fi")
|
||||
; ("case", "esac")
|
||||
]
|
||||
@ Generic.Syntax.user_defined_delimiters
|
||||
|
||||
let escapable_string_literals =
|
||||
[ {|"|}
|
||||
; {|'|}
|
||||
]
|
||||
|
||||
let escape_char =
|
||||
'\\'
|
||||
|
||||
let raw_string_literals =
|
||||
[]
|
||||
|
||||
let comment_parser s =
|
||||
(Parsers.Comments.c_multiline
|
||||
<|> Parsers.Comments.c_newline) s
|
||||
end
|
||||
|
||||
include Matcher.Make(Syntax)
|
22
lib/matchers/c.ml
Normal file
22
lib/matchers/c.ml
Normal file
@ -0,0 +1,22 @@
|
||||
open MParser
|
||||
|
||||
module Syntax = struct
|
||||
include Generic.Syntax
|
||||
|
||||
let escapable_string_literals =
|
||||
[ {|"|}
|
||||
; {|'|}
|
||||
]
|
||||
|
||||
let escape_char =
|
||||
'\\'
|
||||
|
||||
let raw_string_literals =
|
||||
[]
|
||||
|
||||
let comment_parser s =
|
||||
(Parsers.Comments.c_multiline
|
||||
<|> Parsers.Comments.c_newline) s
|
||||
end
|
||||
|
||||
include Matcher.Make(Syntax)
|
4
lib/matchers/c.mli
Normal file
4
lib/matchers/c.mli
Normal file
@ -0,0 +1,4 @@
|
||||
open Types
|
||||
|
||||
module Syntax : Syntax.S
|
||||
include Matcher.S
|
13
lib/matchers/configuration.ml
Normal file
13
lib/matchers/configuration.ml
Normal file
@ -0,0 +1,13 @@
|
||||
type match_kind =
|
||||
| Exact
|
||||
| Fuzzy
|
||||
|
||||
type t =
|
||||
{ match_kind : match_kind
|
||||
; significant_whitespace: bool
|
||||
}
|
||||
|
||||
let create ?(match_kind = Fuzzy) ?(significant_whitespace = false) () =
|
||||
{ match_kind
|
||||
; significant_whitespace
|
||||
}
|
5
lib/matchers/dune
Normal file
5
lib/matchers/dune
Normal file
@ -0,0 +1,5 @@
|
||||
(library
|
||||
(name matchers)
|
||||
(public_name comby.matchers)
|
||||
(preprocess (pps ppx_deriving.show ppx_sexp_conv ppx_sexp_message))
|
||||
(libraries comby.parsers comby.match ppxlib core mparser mparser.pcre))
|
20
lib/matchers/generic.ml
Normal file
20
lib/matchers/generic.ml
Normal file
@ -0,0 +1,20 @@
|
||||
module Syntax = struct
|
||||
(** these are nestable. strings, on the other hand, are not
|
||||
nestable without escapes *)
|
||||
let user_defined_delimiters =
|
||||
[ ("(", ")")
|
||||
; ("{", "}")
|
||||
; ("[", "]")
|
||||
]
|
||||
|
||||
let escapable_string_literals = []
|
||||
|
||||
let escape_char =
|
||||
'\\'
|
||||
|
||||
let raw_string_literals = []
|
||||
|
||||
let comment_parser = MParser.zero
|
||||
end
|
||||
|
||||
include Matcher.Make(Syntax)
|
4
lib/matchers/generic.mli
Normal file
4
lib/matchers/generic.mli
Normal file
@ -0,0 +1,4 @@
|
||||
open Types
|
||||
|
||||
module Syntax : Syntax.S
|
||||
include Matcher.S
|
10
lib/matchers/go.ml
Normal file
10
lib/matchers/go.ml
Normal file
@ -0,0 +1,10 @@
|
||||
module Syntax = struct
|
||||
include C.Syntax
|
||||
|
||||
let raw_string_literals =
|
||||
[ ({|`|}, {|`|})
|
||||
]
|
||||
|
||||
end
|
||||
|
||||
include Matcher.Make(Syntax)
|
4
lib/matchers/go.mli
Normal file
4
lib/matchers/go.mli
Normal file
@ -0,0 +1,4 @@
|
||||
open Types
|
||||
|
||||
module Syntax : Syntax.S
|
||||
include Matcher.S
|
17
lib/matchers/html.ml
Normal file
17
lib/matchers/html.ml
Normal file
@ -0,0 +1,17 @@
|
||||
module Syntax = struct
|
||||
include Generic.Syntax
|
||||
|
||||
let user_defined_delimiters =
|
||||
Generic.Syntax.user_defined_delimiters @
|
||||
[ ("<", ">")
|
||||
]
|
||||
|
||||
|
||||
let escapable_string_literals =
|
||||
[ {|"|}
|
||||
; {|'|}
|
||||
]
|
||||
|
||||
end
|
||||
|
||||
include Matcher.Make(Syntax)
|
401
lib/matchers/matcher.ml
Normal file
401
lib/matchers/matcher.ml
Normal file
@ -0,0 +1,401 @@
|
||||
open Core
|
||||
open MParser
|
||||
|
||||
open Configuration
|
||||
open Match
|
||||
open Range
|
||||
open Location
|
||||
open Types
|
||||
|
||||
let configuration_ref = ref (Configuration.create ())
|
||||
|
||||
let debug = false
|
||||
|
||||
let f _ = return Unit
|
||||
|
||||
let extract_matched_text source { offset = match_start; _ } { offset = match_end; _ } =
|
||||
String.slice source match_start match_end
|
||||
|
||||
let is_not p s =
|
||||
if is_ok (p s) then
|
||||
Empty_failed (unknown_error s)
|
||||
else
|
||||
match read_char s with
|
||||
| Some c -> Consumed_ok (c, advance_state s 1, No_error)
|
||||
| None -> Empty_failed (unknown_error s)
|
||||
|
||||
type 'a literal_parser_callback = contents:string -> left_delimiter:string -> right_delimiter:string -> 'a
|
||||
type 'a nested_delimiter_callback = left_delimiter:string -> right_delimiter:string -> 'a
|
||||
|
||||
module Make (Syntax : Syntax.S) = struct
|
||||
let escapable_string_literal_parser (f : 'a literal_parser_callback) =
|
||||
List.map Syntax.escapable_string_literals ~f:(fun delimiter ->
|
||||
let module M =
|
||||
Parsers.String_literals.Escapable.Make(struct
|
||||
let delimiter = delimiter
|
||||
let escape = Syntax.escape_char
|
||||
end)
|
||||
in
|
||||
M.base_string_literal >>= fun contents ->
|
||||
return (f ~contents ~left_delimiter:delimiter ~right_delimiter:delimiter))
|
||||
|> choice
|
||||
|
||||
|
||||
let raw_string_literal_parser (f : 'a literal_parser_callback) =
|
||||
List.map Syntax.raw_string_literals ~f:(fun (left_delimiter, right_delimiter) ->
|
||||
let module M =
|
||||
Parsers.String_literals.Raw.Make(struct
|
||||
let left_delimiter = left_delimiter
|
||||
let right_delimiter = right_delimiter
|
||||
end)
|
||||
in
|
||||
M.base_string_literal >>= fun contents ->
|
||||
return (f ~contents ~left_delimiter ~right_delimiter))
|
||||
|> choice
|
||||
|
||||
let escapable_literal_grammar ~right_delimiter =
|
||||
(attempt
|
||||
(char Syntax.escape_char
|
||||
>> string right_delimiter
|
||||
>>= fun s -> return (Format.sprintf "%c%s" Syntax.escape_char s))
|
||||
)
|
||||
<|>
|
||||
(attempt
|
||||
(char Syntax.escape_char
|
||||
>> char Syntax.escape_char
|
||||
>> return (Format.sprintf "%c%c" Syntax.escape_char Syntax.escape_char))
|
||||
)
|
||||
<|> (is_not (string right_delimiter) |>> String.of_char)
|
||||
|
||||
let raw_literal_grammar ~right_delimiter =
|
||||
is_not (string right_delimiter) |>> String.of_char
|
||||
|
||||
(** a parser that understands the single hole matching is alphanum and _ *)
|
||||
let generate_single_hole_parser () =
|
||||
(alphanum <|> char '_') |>> String.of_char
|
||||
|
||||
let generate_spaces_parser () =
|
||||
(* at least a space followed by comments and spaces *)
|
||||
(spaces1
|
||||
>> many Syntax.comment_parser << spaces
|
||||
>>= fun result -> f result)
|
||||
<|>
|
||||
(* This case not covered by tests, may not be needed *)
|
||||
(many1 Syntax.comment_parser << spaces >>= fun result -> f result)
|
||||
|
||||
let sequence_chain (plist : ('c, Match.t) parser sexp_list) : ('c, Match.t) parser =
|
||||
List.fold plist ~init:(return Unit) ~f:(>>)
|
||||
|
||||
let nested_delimiters_parser (f : 'a nested_delimiter_callback) =
|
||||
Syntax.user_defined_delimiters
|
||||
|> List.map ~f:(fun (left_delimiter, right_delimiter) ->
|
||||
Parsers.Delimiters.between
|
||||
(f ~left_delimiter ~right_delimiter)
|
||||
left_delimiter right_delimiter)
|
||||
|> choice
|
||||
|
||||
(** All code can have comments interpolated *)
|
||||
let generate_string_token_parser str : ('c, _) parser =
|
||||
many Syntax.comment_parser
|
||||
>> string str
|
||||
>> many Syntax.comment_parser
|
||||
>>= fun result -> f result
|
||||
|
||||
let reserved_delimiters =
|
||||
List.concat_map Syntax.user_defined_delimiters ~f:(fun (from, until) -> [from; until])
|
||||
|> List.append [":["; "]"] (* lazy hole *)
|
||||
|> List.append [":[["; "]]"] (* single token hole *)
|
||||
|> List.map ~f:string
|
||||
|> choice
|
||||
|
||||
let reserved =
|
||||
reserved_delimiters
|
||||
<|> (space |>> Char.to_string)
|
||||
|
||||
let greedy_hole_parser _s =
|
||||
string ":[" >> (many (alphanum <|> char '_') |>> String.of_char_list) << string "]"
|
||||
|
||||
let single_hole_parser _s =
|
||||
string ":[[" >> (many (alphanum <|> char '_') |>> String.of_char_list) << string "]]"
|
||||
|
||||
let until_of_from from =
|
||||
Syntax.user_defined_delimiters
|
||||
|> List.find_map ~f:(fun (from', until) -> if from = from' then Some until else None)
|
||||
|> function
|
||||
| Some until -> until
|
||||
| None -> assert false
|
||||
|
||||
let record_matches identifier p : ('c, Match.t) parser =
|
||||
get_pos >>= fun (pre_index, pre_line, pre_column) ->
|
||||
p >>= fun matched ->
|
||||
get_pos >>= fun (post_index, post_line, post_column) ->
|
||||
update_user_state
|
||||
(fun ({ Match.environment; _ } as result) ->
|
||||
if debug then begin
|
||||
Format.printf "Updating user state:@.";
|
||||
Format.printf "%s |-> %s@." identifier (String.concat matched);
|
||||
Format.printf "ID %s: %d:%d:%d - %d:%d:%d@."
|
||||
identifier
|
||||
pre_index pre_line pre_column
|
||||
post_index post_line post_column;
|
||||
end;
|
||||
let pre_location : Location.t =
|
||||
Location.
|
||||
{ offset = pre_index
|
||||
; line = pre_line
|
||||
; column = pre_column
|
||||
}
|
||||
in
|
||||
let post_location : Location.t =
|
||||
Location.
|
||||
{ offset = post_index
|
||||
; line = post_line
|
||||
; column = post_column
|
||||
}
|
||||
in
|
||||
let range = { match_start = pre_location; match_end = post_location } in
|
||||
let environment = Environment.add ~range environment identifier (String.concat matched) in
|
||||
{ result with environment })
|
||||
>>= fun () -> f matched
|
||||
|
||||
let generate_hole_parser ?priority_left_delimiter:left_delimiter ?priority_right_delimiter:right_delimiter =
|
||||
let between_nested_delims p from =
|
||||
let until = until_of_from from in
|
||||
between (string from) (string until) p
|
||||
|>> fun result -> (String.concat @@ [from] @ result @ [until])
|
||||
in
|
||||
let between_nested_delims p =
|
||||
(match left_delimiter, right_delimiter with
|
||||
| Some left_delimiter, Some right_delimiter -> [ (left_delimiter, right_delimiter) ]
|
||||
| _ -> Syntax.user_defined_delimiters)
|
||||
|> List.map ~f:fst
|
||||
|> List.map ~f:(between_nested_delims p)
|
||||
|> choice
|
||||
in
|
||||
(* applies looser delimiter constraints for matching *)
|
||||
let reserved =
|
||||
(match left_delimiter, right_delimiter with
|
||||
| Some left_delimiter, Some right_delimiter -> [ (left_delimiter, right_delimiter) ]
|
||||
| _ -> Syntax.user_defined_delimiters)
|
||||
|> List.concat_map ~f:(fun (from, until) -> [from; until])
|
||||
|> List.map ~f:string
|
||||
|> choice
|
||||
in
|
||||
(* a parser that understands the hole matching cut off points happen at
|
||||
delimiters *)
|
||||
let rec nested_grammar s =
|
||||
(Syntax.comment_parser
|
||||
<|> escapable_string_literal_parser (fun ~contents ~left_delimiter:_ ~right_delimiter:_ -> contents)
|
||||
<|> raw_string_literal_parser (fun ~contents ~left_delimiter:_ ~right_delimiter:_ -> contents)
|
||||
<|> delimsx
|
||||
<|> (is_not reserved |>> String.of_char))
|
||||
s
|
||||
and delimsx s = (between_nested_delims (many nested_grammar)) s
|
||||
in
|
||||
nested_grammar
|
||||
|
||||
let turn_holes_into_matchers_for_this_level ?left_delimiter ?right_delimiter p_list =
|
||||
List.fold_right p_list ~init:[] ~f:(fun p acc ->
|
||||
let process_hole =
|
||||
match parse_string p "_signal_hole" (Match.create ()) with
|
||||
| Failed _ -> p
|
||||
| Success result ->
|
||||
match result with
|
||||
| Hole (Lazy (identifier, dimension)) ->
|
||||
let matcher =
|
||||
match dimension with
|
||||
| Code ->
|
||||
generate_hole_parser
|
||||
?priority_left_delimiter:left_delimiter
|
||||
?priority_right_delimiter:right_delimiter
|
||||
| Escapable_string_literal ->
|
||||
let right_delimiter = Option.value_exn right_delimiter in
|
||||
escapable_literal_grammar ~right_delimiter
|
||||
| Raw_string_literal ->
|
||||
let right_delimiter = Option.value_exn right_delimiter in
|
||||
raw_literal_grammar ~right_delimiter
|
||||
| Comment -> failwith "Unimplemented"
|
||||
in
|
||||
let rest =
|
||||
match acc with
|
||||
| [] -> eof >>= fun () -> f [""]
|
||||
| _ -> sequence_chain acc
|
||||
in
|
||||
(* continue until rest, but don't consume rest. *)
|
||||
let hole_semantics = many (not_followed_by rest "" >> matcher) in
|
||||
record_matches identifier hole_semantics
|
||||
|
||||
| Hole (Single (identifier, _)) ->
|
||||
let hole_semantics = many1 (generate_single_hole_parser ()) in
|
||||
record_matches identifier hole_semantics
|
||||
|
||||
| _ -> failwith "Hole expected"
|
||||
in
|
||||
process_hole::acc)
|
||||
|
||||
let hole_parser sort dimension =
|
||||
let skip_signal result =
|
||||
skip (string "_signal_hole") |>> fun () -> result
|
||||
in
|
||||
match sort with
|
||||
| `Single ->
|
||||
single_hole_parser () |>> fun id ->
|
||||
skip_signal (Hole (Single (id, dimension)))
|
||||
| `Lazy ->
|
||||
greedy_hole_parser () |>> fun id ->
|
||||
skip_signal (Hole (Lazy (id, dimension)))
|
||||
|
||||
let generate_hole_for_literal sort ~contents ~left_delimiter ~right_delimiter s =
|
||||
let p =
|
||||
many (hole_parser `Single sort
|
||||
<|> (hole_parser `Lazy sort)
|
||||
<|> ((many1 (is_not (string ":[" <|> string ":[["))
|
||||
|>> String.of_char_list) |>> generate_string_token_parser))
|
||||
in
|
||||
match parse_string p contents "" with
|
||||
| Success p ->
|
||||
begin
|
||||
match sort with
|
||||
| Escapable_string_literal
|
||||
| Raw_string_literal ->
|
||||
(turn_holes_into_matchers_for_this_level ~left_delimiter ~right_delimiter p
|
||||
|> sequence_chain) s
|
||||
| _ -> assert false
|
||||
end
|
||||
| Failed (_msg, _) ->
|
||||
failwith "literal parser did not succeed"
|
||||
|
||||
let rec generate_parsers s =
|
||||
many (common s)
|
||||
|
||||
and common _s =
|
||||
(hole_parser `Single Code)
|
||||
<|> (hole_parser `Lazy Code)
|
||||
(* string literals are handled specially because match semantics change inside string delimiters *)
|
||||
<|> (escapable_string_literal_parser (generate_hole_for_literal Escapable_string_literal))
|
||||
<|> (raw_string_literal_parser (generate_hole_for_literal Raw_string_literal))
|
||||
(* whitespace is handled specially because we may change whether they are significant for matching *)
|
||||
<|> (spaces1 |>> generate_spaces_parser)
|
||||
(* nested delimiters are handled specially for nestedness *)
|
||||
<|> (nested_delimiters_parser generate_outer_delimiter_parsers)
|
||||
(* everything else *)
|
||||
<|> ((many1 (is_not reserved) |>> String.of_char_list) |>> generate_string_token_parser)
|
||||
|
||||
and generate_outer_delimiter_parsers ~left_delimiter ~right_delimiter s =
|
||||
(generate_parsers s >>= fun p_list ->
|
||||
(turn_holes_into_matchers_for_this_level ~left_delimiter ~right_delimiter
|
||||
([ string left_delimiter
|
||||
>>= fun _ -> f [left_delimiter]]
|
||||
@ p_list
|
||||
@ [ string right_delimiter
|
||||
>>= fun _ -> f [right_delimiter]])
|
||||
|> sequence_chain)
|
||||
|> return
|
||||
) s
|
||||
|
||||
let general_parser_generator s =
|
||||
let outer_p =
|
||||
generate_parsers s >>= fun p_list ->
|
||||
(* eof of template is here *)
|
||||
eof >> (* result is unit so ignore *)
|
||||
(* customize the inner parser *)
|
||||
let inner_p =
|
||||
let matcher : ('a, Match.t) parser =
|
||||
turn_holes_into_matchers_for_this_level p_list
|
||||
|> sequence_chain
|
||||
in
|
||||
let matcher : ('a, Match.t) parser =
|
||||
let with_positions (matcher : ('a, Match.t) parser) : ('a, Match.t) parser =
|
||||
get_pos >>= fun (pre_offset, pre_line, pre_column) ->
|
||||
matcher >>= fun _last_production ->
|
||||
get_pos >>= fun (post_offset, post_line, post_column) ->
|
||||
let match_start =
|
||||
{ offset = pre_offset
|
||||
; line = pre_line
|
||||
; column = pre_column
|
||||
} in
|
||||
let match_end =
|
||||
{ offset = post_offset
|
||||
; line = post_line
|
||||
; column = post_column
|
||||
}
|
||||
in
|
||||
let range = { match_start; match_end } in
|
||||
update_user_state (fun result -> { result with range })
|
||||
>> return Unit
|
||||
in
|
||||
with_positions matcher
|
||||
in
|
||||
match !configuration_ref.match_kind with
|
||||
| Exact -> matcher << eof
|
||||
| Fuzzy ->
|
||||
many
|
||||
(not_followed_by matcher "" >>
|
||||
(
|
||||
(* respect grammar but ignore contents up to a match *)
|
||||
skip Syntax.comment_parser
|
||||
<|> skip (escapable_string_literal_parser (fun ~contents:_ ~left_delimiter:_ ~right_delimiter:_ -> ()))
|
||||
<|> skip (raw_string_literal_parser (fun ~contents:_ ~left_delimiter:_ ~right_delimiter:_ -> ()))
|
||||
<|> skip any_char)
|
||||
)
|
||||
>> matcher
|
||||
in
|
||||
return inner_p
|
||||
in
|
||||
outer_p s
|
||||
|
||||
let to_template template : ('a, Match.t) MParser.t Or_error.t =
|
||||
match parse_string general_parser_generator template 0 with
|
||||
| Success p -> Ok p
|
||||
| Failed (msg, _) -> Or_error.error_string msg
|
||||
|
||||
(** shift: start the scan in the source at an offset *)
|
||||
let first' shift p source : Match.t Or_error.t =
|
||||
let set_start_pos p = fun s -> p (advance_state s shift) in
|
||||
let p = set_start_pos p in
|
||||
match parse_string' p source (Match.create ()) with
|
||||
| Success (_, result) -> Ok result
|
||||
| Failed (msg, _) -> Or_error.error_string msg
|
||||
|
||||
let first ?configuration ?shift template source =
|
||||
let open Or_error in
|
||||
configuration_ref := Option.value configuration ~default:!configuration_ref;
|
||||
to_template template >>= fun p ->
|
||||
let shift =
|
||||
match shift with
|
||||
| Some s -> s
|
||||
| None -> 0
|
||||
in
|
||||
first' shift p source
|
||||
|
||||
let all ?configuration ~template ~source:original_source : Match.t list =
|
||||
let open Or_error in
|
||||
configuration_ref := Option.value configuration ~default:!configuration_ref;
|
||||
let make_result = function
|
||||
| Ok ok -> ok
|
||||
| Error _ -> []
|
||||
in
|
||||
make_result @@ begin
|
||||
to_template template >>= fun p ->
|
||||
if original_source = "" || template = "" then
|
||||
return []
|
||||
else
|
||||
let rec aux acc shift =
|
||||
match first' shift p original_source with
|
||||
| Ok ({range = { match_start; match_end; _ }; _} as result) ->
|
||||
let shift = match_end.offset in
|
||||
let matched = extract_matched_text original_source match_start match_end in
|
||||
let result = { result with matched } in
|
||||
if shift >= String.length original_source then
|
||||
result :: acc
|
||||
else
|
||||
aux (result :: acc) shift
|
||||
| Error _ -> acc
|
||||
in
|
||||
let matches = aux [] 0 |> List.rev in
|
||||
(* TODO(RVT): reintroduce nested matches *)
|
||||
let compute_nested_matches matches = matches in
|
||||
let matches = compute_nested_matches matches in
|
||||
return matches
|
||||
end
|
||||
end
|
3
lib/matchers/matcher.mli
Normal file
3
lib/matchers/matcher.mli
Normal file
@ -0,0 +1,3 @@
|
||||
open Types
|
||||
|
||||
module Make (Syntax: Syntax.S) : Matcher.S
|
10
lib/matchers/matchers.ml
Normal file
10
lib/matchers/matchers.ml
Normal file
@ -0,0 +1,10 @@
|
||||
module Generic = Generic
|
||||
module C = C
|
||||
module Go = Go
|
||||
module Python = Python
|
||||
module Bash = Bash
|
||||
module Html = Html
|
||||
|
||||
module Configuration = Configuration
|
||||
|
||||
module type Matcher = Types.Matcher.S
|
10
lib/matchers/matchers.mli
Normal file
10
lib/matchers/matchers.mli
Normal file
@ -0,0 +1,10 @@
|
||||
module Generic = Generic
|
||||
module C = C
|
||||
module Go = Go
|
||||
module Python = Python
|
||||
module Bash = Bash
|
||||
module Html = Html
|
||||
|
||||
module Configuration = Configuration
|
||||
|
||||
module type Matcher = Types.Matcher.S
|
20
lib/matchers/python.ml
Normal file
20
lib/matchers/python.ml
Normal file
@ -0,0 +1,20 @@
|
||||
module Python = struct
|
||||
include Generic.Syntax
|
||||
|
||||
let escapable_string_literals =
|
||||
[ {|"|}
|
||||
; {|'|}
|
||||
]
|
||||
|
||||
let escape_char =
|
||||
'\\'
|
||||
|
||||
let raw_string_literals =
|
||||
[ ({|"""|}, {|"""|})
|
||||
]
|
||||
|
||||
let comment_parser s =
|
||||
Parsers.Comments.python_newline s
|
||||
end
|
||||
|
||||
include Matcher.Make(Python)
|
44
lib/matchers/types.ml
Normal file
44
lib/matchers/types.ml
Normal file
@ -0,0 +1,44 @@
|
||||
open Core
|
||||
|
||||
module Syntax = struct
|
||||
module type S = sig
|
||||
val user_defined_delimiters : (string * string) list
|
||||
val escapable_string_literals : string list
|
||||
val escape_char : char
|
||||
val raw_string_literals : (string * string) list
|
||||
val comment_parser : (string, _) MParser.t
|
||||
end
|
||||
end
|
||||
|
||||
type dimension =
|
||||
| Code
|
||||
| Escapable_string_literal
|
||||
| Raw_string_literal
|
||||
| Comment
|
||||
|
||||
type hole =
|
||||
| Lazy of (string * dimension)
|
||||
| Single of (string * dimension)
|
||||
|
||||
type production =
|
||||
| Unit
|
||||
| String of string
|
||||
| Hole of hole
|
||||
| Match of (int * string * string)
|
||||
|
||||
module Matcher = struct
|
||||
module type S = sig
|
||||
val first
|
||||
: ?configuration:Configuration.t
|
||||
-> ?shift:int
|
||||
-> string
|
||||
-> string
|
||||
-> Match.t Or_error.t
|
||||
|
||||
val all
|
||||
: ?configuration:Configuration.t
|
||||
-> template:string
|
||||
-> source:string
|
||||
-> Match.t list
|
||||
end
|
||||
end
|
69
lib/parsers/comments.ml
Normal file
69
lib/parsers/comments.ml
Normal file
@ -0,0 +1,69 @@
|
||||
open Core
|
||||
open MParser
|
||||
|
||||
let to_string from until between : string =
|
||||
from ^ (String.of_char_list between) ^ until
|
||||
|
||||
let anything_including_newlines ~until =
|
||||
(many
|
||||
(not_followed_by (string until) ""
|
||||
>>= fun () -> any_char_or_nl))
|
||||
|
||||
let anything_excluding_newlines ~until =
|
||||
(many
|
||||
(not_followed_by (string until) ""
|
||||
>>= fun () -> any_char))
|
||||
|
||||
(** a parser for comments with delimiters [from] and [until] that do not nest *)
|
||||
let non_nested_comment_delimiters from until s =
|
||||
(between
|
||||
(string from)
|
||||
(string until)
|
||||
(anything_including_newlines ~until)
|
||||
|>> to_string from until
|
||||
) s
|
||||
|
||||
(** a parser for /* ... */ style block comments. *)
|
||||
let c_multiline s =
|
||||
non_nested_comment_delimiters "/*" "*/" s
|
||||
|
||||
let c_newline s =
|
||||
(string "//" >> anything_excluding_newlines ~until:"\n"
|
||||
|>> fun l -> "//"^(String.of_char_list l)) s
|
||||
|
||||
let python_newline s =
|
||||
(string "#" >> anything_excluding_newlines ~until:"\n"
|
||||
|>> fun l -> ("#"^String.of_char_list l)) s
|
||||
|
||||
let any_newline comment_string s =
|
||||
(string comment_string >> anything_excluding_newlines ~until:"\n" |>> fun l -> (comment_string^String.of_char_list l)) s
|
||||
|
||||
let is_not p s =
|
||||
if is_ok (p s) then
|
||||
Empty_failed (unknown_error s)
|
||||
else
|
||||
match read_char s with
|
||||
| Some c ->
|
||||
Consumed_ok (c, advance_state s 1, No_error)
|
||||
| None ->
|
||||
Empty_failed (unknown_error s)
|
||||
|
||||
(** A nested comment parser *)
|
||||
let skip_nested_comments_inner from until s =
|
||||
let reserved = skip ((string from) <|> (string until)) in
|
||||
let rec grammar s =
|
||||
((comment_delimiters >>= fun string -> return string)
|
||||
<|>
|
||||
(is_not reserved >>= fun c -> return (Char.to_string c)))
|
||||
s
|
||||
|
||||
and comment_delimiters s =
|
||||
(between
|
||||
(string from)
|
||||
(string until)
|
||||
((many grammar) >>= fun result ->
|
||||
return (String.concat result)))
|
||||
s
|
||||
in
|
||||
(comment_delimiters >>= fun _ ->
|
||||
return ()) s
|
11
lib/parsers/comments.mli
Normal file
11
lib/parsers/comments.mli
Normal file
@ -0,0 +1,11 @@
|
||||
(** C-style /* */ block comment parser *)
|
||||
val c_multiline : (string, _) MParser.t
|
||||
|
||||
(** C++-style // line comment parser *)
|
||||
val c_newline : (string, _) MParser.t
|
||||
|
||||
(** Python-style # line comment parser *)
|
||||
val python_newline : (string, _) MParser.t
|
||||
|
||||
(** Anything until newline *)
|
||||
val any_newline : string -> (string, _) MParser.t
|
18
lib/parsers/delimiters.ml
Normal file
18
lib/parsers/delimiters.ml
Normal file
@ -0,0 +1,18 @@
|
||||
open MParser
|
||||
|
||||
(** Significant, potentially nested delimiters. *)
|
||||
|
||||
let parens' p =
|
||||
char '(' >> p << char ')'
|
||||
|
||||
let braces' p =
|
||||
char '{' >> p << char '}'
|
||||
|
||||
let brackets' p =
|
||||
char '<' >> p << char '>'
|
||||
|
||||
let squares' p =
|
||||
char '[' >> p << char ']'
|
||||
|
||||
let between p from until =
|
||||
string from >> p << string until
|
5
lib/parsers/dune
Normal file
5
lib/parsers/dune
Normal file
@ -0,0 +1,5 @@
|
||||
(library
|
||||
(name parsers)
|
||||
(public_name comby.parsers)
|
||||
(preprocess (pps ppx_deriving.show ppx_sexp_conv))
|
||||
(libraries ppxlib core mparser mparser.pcre))
|
57
lib/parsers/string_literals.ml
Normal file
57
lib/parsers/string_literals.ml
Normal file
@ -0,0 +1,57 @@
|
||||
open Core
|
||||
open MParser
|
||||
|
||||
(** Assumes the left and right delimiter are the same, and that these can be
|
||||
escaped. Does not parse a string body containing newlines (as usual when
|
||||
escaping with \n) *)
|
||||
module Escapable = struct
|
||||
module type S = sig
|
||||
val delimiter : string
|
||||
val escape : char
|
||||
end
|
||||
|
||||
module Make (M : S) = struct
|
||||
(* delimiters can be escaped and parsing continues within the string body *)
|
||||
let escaped_char_s s =
|
||||
any_char s
|
||||
|
||||
let char_token_s s =
|
||||
((char M.escape >> escaped_char_s >>= fun c -> return (Format.sprintf {|%c%c|} M.escape c))
|
||||
<|> (any_char |>> String.of_char)
|
||||
)
|
||||
s
|
||||
|
||||
let base_string_literal s =
|
||||
((string M.delimiter >> (many_until char_token_s (string M.delimiter))
|
||||
|>> String.concat)
|
||||
>>= fun result ->
|
||||
return (Format.sprintf {|%s%s%s|} M.delimiter result M.delimiter)
|
||||
)
|
||||
s
|
||||
end
|
||||
end
|
||||
|
||||
(** Quoted or raw strings. Allows different left and right delimiters, and
|
||||
disallows any sort of escaping. Does not support raw strings with identifiers
|
||||
yet, e.g., {blah|<string body>|blah} (OCaml) or delim`<string body>`delim
|
||||
syntax (Go) *)
|
||||
module Raw = struct
|
||||
module type S = sig
|
||||
val left_delimiter : string
|
||||
val right_delimiter : string
|
||||
end
|
||||
|
||||
module Make (M : S) = struct
|
||||
let char_token_s s =
|
||||
(any_char_or_nl |>> String.of_char) s
|
||||
|
||||
let base_string_literal s =
|
||||
((
|
||||
string M.left_delimiter >> (many_until char_token_s (string M.right_delimiter))
|
||||
|>> String.concat <?> "raw string literal body")
|
||||
>>= fun result ->
|
||||
return (Format.sprintf {|%s%s%s|} M.left_delimiter result M.right_delimiter)
|
||||
)
|
||||
s
|
||||
end
|
||||
end
|
5
lib/rewriter/dune
Normal file
5
lib/rewriter/dune
Normal file
@ -0,0 +1,5 @@
|
||||
(library
|
||||
(name rewriter)
|
||||
(public_name comby.rewriter)
|
||||
(preprocess (pps ppx_deriving.show ppx_sexp_conv ppx_deriving_yojson))
|
||||
(libraries comby.matchers ppxlib core))
|
112
lib/rewriter/rewrite.ml
Normal file
112
lib/rewriter/rewrite.ml
Normal file
@ -0,0 +1,112 @@
|
||||
open Core
|
||||
|
||||
open Match
|
||||
|
||||
type match_context_replacement =
|
||||
{ range : range
|
||||
; replacement_content : string
|
||||
; environment : environment
|
||||
}
|
||||
[@@deriving yojson]
|
||||
|
||||
type result =
|
||||
{ rewritten_source : string
|
||||
; contextual_substitutions : match_context_replacement list
|
||||
}
|
||||
[@@deriving yojson]
|
||||
|
||||
let empty_result =
|
||||
{ rewritten_source = ""
|
||||
; contextual_substitutions = []
|
||||
}
|
||||
[@@deriving yojson]
|
||||
|
||||
let substitute_match_contexts (matches: Match.t list) source replacements =
|
||||
let rewrite_template, environment =
|
||||
List.fold2_exn
|
||||
matches replacements
|
||||
~init:(source, Environment.create ())
|
||||
~f:(fun
|
||||
(rewrite_template, accumulator_environment)
|
||||
({ environment = _match_environment; _ } as match_)
|
||||
{ replacement_content; _ } ->
|
||||
(* create a hole in the rewrite template based on this match context *)
|
||||
let hole_id, rewrite_template = Rewrite_template.of_match_context match_ ~source:rewrite_template in
|
||||
(* add this match context replacement to the environment *)
|
||||
let accumulator_environment = Environment.add accumulator_environment hole_id replacement_content in
|
||||
(* update match context replacements offset *)
|
||||
rewrite_template, accumulator_environment)
|
||||
in
|
||||
let rewritten_source = Rewrite_template.substitute rewrite_template environment |> fst in
|
||||
let offsets = Rewrite_template.get_offsets_for_holes rewrite_template (Environment.vars environment) in
|
||||
let offsets = Rewrite_template.get_offsets_after_substitution offsets environment in
|
||||
let contextual_substitutions =
|
||||
List.map2_exn replacements offsets ~f:(fun replacement (_uid, offset) ->
|
||||
let match_start = { Location.default with offset } in
|
||||
let offset = offset + String.length replacement.replacement_content in
|
||||
let match_end = { Location.default with offset } in
|
||||
let range = Range.{ match_start; match_end } in
|
||||
{ replacement with range })
|
||||
in
|
||||
{ rewritten_source
|
||||
; contextual_substitutions
|
||||
}
|
||||
|
||||
(*
|
||||
store range information for this match_context replacement:
|
||||
(a) its offset in the original source
|
||||
(b) its replacement context (to calculate the range)
|
||||
(c) an environment of values that are updated to reflect their relative offset in the rewrite template
|
||||
*)
|
||||
let substitute_in_rewrite_template rewrite_template ({ environment; _ } : Match.t) =
|
||||
let replacement_content, vars_substituted_for = Rewrite_template.substitute rewrite_template environment in
|
||||
let offsets = Rewrite_template.get_offsets_for_holes rewrite_template (Environment.vars environment) in
|
||||
let offsets = Rewrite_template.get_offsets_after_substitution offsets environment in
|
||||
let environment =
|
||||
List.fold offsets ~init:(Environment.create ()) ~f:(fun acc (var, relative_offset) ->
|
||||
if List.mem vars_substituted_for var ~equal:String.equal then
|
||||
let value = Option.value_exn (Environment.lookup environment var) in
|
||||
(* FIXME(RVT): Location does not update row/column here *)
|
||||
let start_location =
|
||||
Location.{ default with offset = relative_offset }
|
||||
in
|
||||
let end_location =
|
||||
let offset = relative_offset + String.length value in
|
||||
Location.{ default with offset }
|
||||
in
|
||||
let range =
|
||||
Range.
|
||||
{ match_start = start_location
|
||||
; match_end = end_location
|
||||
}
|
||||
in
|
||||
Environment.add ~range acc var value
|
||||
else
|
||||
acc)
|
||||
in
|
||||
{ replacement_content
|
||||
; environment
|
||||
; range =
|
||||
{ match_start = { Location.default with offset = 0 }
|
||||
; match_end = Location.default
|
||||
}
|
||||
}
|
||||
|
||||
let all ?source ~rewrite_template matches : result option =
|
||||
if matches = [] then None else
|
||||
match source with
|
||||
(* in-place substitution *)
|
||||
| Some source ->
|
||||
let matches : Match.t list = List.rev matches in
|
||||
matches
|
||||
|> List.map ~f:(substitute_in_rewrite_template rewrite_template)
|
||||
|> substitute_match_contexts matches source
|
||||
|> Option.some
|
||||
(* no-inplace substitution, emit result separated by newlines *)
|
||||
| None ->
|
||||
matches
|
||||
|> List.map ~f:(substitute_in_rewrite_template rewrite_template)
|
||||
|> List.map ~f:(fun { replacement_content; _ } -> replacement_content)
|
||||
|> String.concat ~sep:"\n"
|
||||
|> (fun rewritten_source -> { rewritten_source; contextual_substitutions = [] })
|
||||
|> Option.some
|
22
lib/rewriter/rewrite.mli
Normal file
22
lib/rewriter/rewrite.mli
Normal file
@ -0,0 +1,22 @@
|
||||
open Match
|
||||
|
||||
type match_context_replacement =
|
||||
{ range : range
|
||||
; replacement_content : string
|
||||
; environment : environment
|
||||
}
|
||||
[@@deriving yojson]
|
||||
|
||||
type result =
|
||||
{ rewritten_source : string
|
||||
; contextual_substitutions : match_context_replacement list
|
||||
}
|
||||
[@@deriving yojson]
|
||||
|
||||
(** if [source] is given, substitute in-place. If not,
|
||||
emit result separated by newlines *)
|
||||
val all
|
||||
: ?source:string
|
||||
-> rewrite_template:string
|
||||
-> Match.t list
|
||||
-> result option
|
63
lib/rewriter/rewrite_template.ml
Normal file
63
lib/rewriter/rewrite_template.ml
Normal file
@ -0,0 +1,63 @@
|
||||
open Core
|
||||
|
||||
open Match
|
||||
|
||||
let substitute template env =
|
||||
Environment.vars env
|
||||
|> List.fold ~init:(template, []) ~f:(fun (acc, vars) variable ->
|
||||
match Environment.lookup env variable with
|
||||
| Some value ->
|
||||
if Option.is_some (String.substr_index template ~pattern:(":["^variable^"]")) then
|
||||
(String.substr_replace_all acc ~pattern:(":["^variable^"]") ~with_:value, variable::vars)
|
||||
else
|
||||
acc, vars
|
||||
| None -> acc, vars)
|
||||
|
||||
let of_match_context
|
||||
{ range =
|
||||
{ match_start = { offset = start_index; _ }
|
||||
; match_end = { offset = end_index; _ } }
|
||||
; _
|
||||
}
|
||||
~source =
|
||||
let before_part =
|
||||
if start_index = 0 then
|
||||
""
|
||||
else
|
||||
String.slice source 0 start_index
|
||||
in
|
||||
let after_part = String.slice source end_index (String.length source) in
|
||||
let hole_id = Uuid.(Fn.compose to_string create ()) in
|
||||
let rewrite_template = String.concat [before_part; ":["; hole_id; "]"; after_part] in
|
||||
hole_id, rewrite_template
|
||||
|
||||
(* return the offset for holes (specified by variables) in a given match template *)
|
||||
let get_offsets_for_holes rewrite_template variables =
|
||||
let sorted_variables =
|
||||
List.fold variables ~init:[] ~f:(fun acc variable ->
|
||||
match String.substr_index rewrite_template ~pattern:(":["^variable^"]") with
|
||||
| Some index ->
|
||||
(variable, index)::acc
|
||||
| None -> acc)
|
||||
|> List.sort ~compare:(fun (_, i1) (_, i2) -> i1 - i2)
|
||||
|> List.map ~f:fst
|
||||
in
|
||||
List.fold sorted_variables ~init:(rewrite_template, []) ~f:(fun (rewrite_template, acc) variable ->
|
||||
match String.substr_index rewrite_template ~pattern:(":["^variable^"]") with
|
||||
| Some index ->
|
||||
let rewrite_template =
|
||||
String.substr_replace_all rewrite_template ~pattern:(":["^variable^"]") ~with_:"" in
|
||||
rewrite_template, (variable, index)::acc
|
||||
| None -> rewrite_template, acc)
|
||||
|> snd
|
||||
|
||||
(* pretend we substituted vars in offsets with environment. return what the offsets are after *)
|
||||
let get_offsets_after_substitution offsets environment =
|
||||
List.fold_right offsets ~init:([],0) ~f:(fun (var, offset) (acc, shift) ->
|
||||
match Environment.lookup environment var with
|
||||
| None -> failwith "Expected var"
|
||||
| Some s ->
|
||||
let offset' = offset + shift in
|
||||
let shift = shift + String.length s in
|
||||
((var, offset')::acc), shift)
|
||||
|> fst
|
10
lib/rewriter/rewrite_template.mli
Normal file
10
lib/rewriter/rewrite_template.mli
Normal file
@ -0,0 +1,10 @@
|
||||
open Match
|
||||
|
||||
(** substitute returns the result and variables substituted for *)
|
||||
val substitute : string -> Environment.t -> (string * string list)
|
||||
|
||||
val of_match_context : Match.t -> source:string -> (string * string)
|
||||
|
||||
val get_offsets_for_holes : string -> string list -> (string * int) list
|
||||
|
||||
val get_offsets_after_substitution : (string * int) list -> Environment.t -> (string * int) list
|
5
lib/statistics/dune
Normal file
5
lib/statistics/dune
Normal file
@ -0,0 +1,5 @@
|
||||
(library
|
||||
(name statistics)
|
||||
(public_name comby.statistics)
|
||||
(preprocess (pps ppx_deriving.show ppx_sexp_conv ppx_sexp_message ppx_deriving_yojson))
|
||||
(libraries ppxlib core yojson ppx_deriving_yojson ppx_deriving_yojson.runtime))
|
34
lib/statistics/statistics.ml
Normal file
34
lib/statistics/statistics.ml
Normal file
@ -0,0 +1,34 @@
|
||||
module Time = Time
|
||||
module Timer = Timer
|
||||
|
||||
type t =
|
||||
{ number_of_files : int
|
||||
; lines_of_code : int
|
||||
; number_of_matches : int
|
||||
; total_time : float
|
||||
}
|
||||
[@@deriving yojson]
|
||||
|
||||
let empty =
|
||||
{ number_of_files = 0
|
||||
; lines_of_code = 0
|
||||
; number_of_matches = 0
|
||||
; total_time = 0.0
|
||||
}
|
||||
|
||||
let merge
|
||||
{ number_of_files
|
||||
; lines_of_code
|
||||
; number_of_matches
|
||||
; total_time
|
||||
}
|
||||
{ number_of_files = number_of_files'
|
||||
; lines_of_code = lines_of_code'
|
||||
; number_of_matches = number_of_matches'
|
||||
; total_time = total_time'
|
||||
} =
|
||||
{ number_of_files = number_of_files + number_of_files'
|
||||
; lines_of_code = lines_of_code + lines_of_code'
|
||||
; number_of_matches = number_of_matches + number_of_matches'
|
||||
; total_time = total_time +. total_time'
|
||||
}
|
14
lib/statistics/statistics.mli
Normal file
14
lib/statistics/statistics.mli
Normal file
@ -0,0 +1,14 @@
|
||||
module Time = Time
|
||||
module Timer = Timer
|
||||
|
||||
type t =
|
||||
{ number_of_files : int
|
||||
; lines_of_code : int
|
||||
; number_of_matches : int
|
||||
; total_time : float
|
||||
}
|
||||
[@@deriving yojson]
|
||||
|
||||
val empty : t
|
||||
|
||||
val merge : t -> t -> t
|
25
lib/statistics/time.ml
Normal file
25
lib/statistics/time.ml
Normal file
@ -0,0 +1,25 @@
|
||||
open Core
|
||||
|
||||
let start () = Unix.gettimeofday ()
|
||||
|
||||
let stop start =
|
||||
(Unix.gettimeofday () -. start) *. 1000.0
|
||||
|
||||
exception Time_out
|
||||
|
||||
let time_out ~after f args =
|
||||
let behavior =
|
||||
Signal.(Expert.signal alrm (`Handle (fun _ -> raise Time_out)))
|
||||
in
|
||||
let cancel_alarm () =
|
||||
Unix.alarm 0 |> ignore;
|
||||
Signal.(Expert.set alrm behavior)
|
||||
in
|
||||
Unix.alarm after |> ignore;
|
||||
match f args with
|
||||
| result ->
|
||||
cancel_alarm ();
|
||||
result
|
||||
| exception exc ->
|
||||
cancel_alarm ();
|
||||
raise exc
|
12
src/dune
Normal file
12
src/dune
Normal file
@ -0,0 +1,12 @@
|
||||
(executables
|
||||
(libraries comby core ppx_deriving_yojson ppx_deriving_yojson.runtime hack_parallel)
|
||||
(preprocess (pps ppx_deriving_yojson ppx_let ppx_deriving.show))
|
||||
(names main))
|
||||
|
||||
(alias
|
||||
(name DEFAULT)
|
||||
(deps main.exe))
|
||||
|
||||
(install
|
||||
(section bin)
|
||||
(files (main.exe as comby)))
|
385
src/main.ml
Normal file
385
src/main.ml
Normal file
@ -0,0 +1,385 @@
|
||||
open Core
|
||||
open Command.Let_syntax
|
||||
|
||||
open Hack_parallel
|
||||
|
||||
open Matchers
|
||||
open Match
|
||||
open Language
|
||||
open Rewriter
|
||||
open Statistics
|
||||
|
||||
type json_result =
|
||||
{ matches : Match.t list
|
||||
; source : string
|
||||
}
|
||||
[@@deriving yojson]
|
||||
|
||||
type input_kind =
|
||||
| Paths of string list
|
||||
| Path of string
|
||||
| String of string
|
||||
[@@deriving show]
|
||||
|
||||
type processed_source_result =
|
||||
| Matches of (Match.t list * int)
|
||||
| Rewritten of (Rewrite.match_context_replacement list * string * int)
|
||||
| Nothing
|
||||
|
||||
let read = Fn.compose String.rstrip In_channel.read_all
|
||||
|
||||
let read_template =
|
||||
Fn.compose
|
||||
String.chop_suffix_exn ~suffix:"\n"
|
||||
In_channel.read_all
|
||||
|
||||
let verbose_out_file = "/tmp/comby.out"
|
||||
|
||||
let get_matches (module Matcher : Matchers.Matcher) configuration match_template match_rule source =
|
||||
let rule = Rule.create match_rule |> Or_error.ok_exn in
|
||||
Matcher.all ~configuration ~template:match_template ~source
|
||||
|> List.filter ~f:(fun { environment; _ } -> Rule.(sat @@ apply rule ~matcher:(module Matcher) environment))
|
||||
|
||||
let apply_rewrite_rule matcher rewrite_rule matches =
|
||||
let open Option in
|
||||
match rewrite_rule with
|
||||
| "" -> matches
|
||||
| rewrite_rule ->
|
||||
begin
|
||||
match Rule.create rewrite_rule with
|
||||
| Ok rule ->
|
||||
List.filter_map matches ~f:(fun ({ environment; _ } as match_) ->
|
||||
let sat, env = Rule.apply rule ~matcher environment in
|
||||
(if sat then env else None)
|
||||
>>| fun environment -> { match_ with environment })
|
||||
| Error _ -> []
|
||||
end
|
||||
|
||||
let rewrite rewrite_template _rewrite_rule source matches =
|
||||
Rewrite.all ~source ~rewrite_template matches
|
||||
|
||||
let process_single_source matcher verbose configuration source specification match_timeout =
|
||||
let open Specification in
|
||||
try
|
||||
let input_text =
|
||||
match source with
|
||||
| String input_text -> input_text
|
||||
| Path path ->
|
||||
if verbose then
|
||||
Out_channel.with_file ~append:true verbose_out_file ~f:(fun out_channel ->
|
||||
Out_channel.output_lines out_channel [Format.sprintf "Processing %s%!" path]);
|
||||
In_channel.read_all path
|
||||
| _ -> failwith "Don't send multiple paths to process_single_source"
|
||||
in
|
||||
match specification with
|
||||
| { match_specification = { match_template; match_rule }
|
||||
; rewrite_specification = None
|
||||
} ->
|
||||
let matches =
|
||||
try
|
||||
let f () = get_matches matcher configuration match_template match_rule input_text in
|
||||
Statistics.Time.time_out ~after:match_timeout f ();
|
||||
with Statistics.Time.Time_out ->
|
||||
Out_channel.with_file ~append:true verbose_out_file ~f:(fun out_channel ->
|
||||
Out_channel.output_lines out_channel [Format.sprintf "TIMEOUT: %s@." (show_input_kind source) ]);
|
||||
[]
|
||||
in
|
||||
Matches (matches, List.length matches)
|
||||
| { match_specification = { match_template; match_rule }
|
||||
; rewrite_specification = Some { rewrite_template; rewrite_rule }
|
||||
} ->
|
||||
let result =
|
||||
try
|
||||
let f () =
|
||||
get_matches matcher configuration match_template match_rule input_text
|
||||
|> fun matches ->
|
||||
(* TODO(RVT): merge match and rewrite rule application. *)
|
||||
apply_rewrite_rule matcher rewrite_rule matches
|
||||
|> fun matches ->
|
||||
if matches = [] then
|
||||
(* If there are no matches, return the original source (for editor support). *)
|
||||
Some (Some (Rewrite.{ rewritten_source = input_text; contextual_substitutions = [] }), [])
|
||||
else
|
||||
Some (rewrite rewrite_template rewrite_rule input_text matches, matches)
|
||||
in
|
||||
Statistics.Time.time_out ~after:match_timeout f ();
|
||||
with Statistics.Time.Time_out ->
|
||||
Out_channel.with_file ~append:true verbose_out_file ~f:(fun out_channel ->
|
||||
Out_channel.output_lines out_channel [Format.sprintf "TIMEOUT: FOR %s@." (show_input_kind source) ]);
|
||||
None
|
||||
in
|
||||
result
|
||||
|> function
|
||||
| Some (Some { rewritten_source; contextual_substitutions }, matches) ->
|
||||
Rewritten (contextual_substitutions, rewritten_source, List.length matches)
|
||||
| Some (None, _)
|
||||
| None -> Nothing
|
||||
with
|
||||
| _ -> Nothing
|
||||
|
||||
let output_result stdin spec_number json source_path result =
|
||||
match result with
|
||||
| Nothing -> ()
|
||||
| Matches (matches, _) ->
|
||||
if json then
|
||||
let json_matches = `List (List.map ~f:Match.to_yojson matches) in
|
||||
Format.printf "%s%!" @@ Yojson.Safe.pretty_to_string json_matches
|
||||
else
|
||||
let with_file =
|
||||
match source_path with
|
||||
| Some path -> Format.sprintf " in %s " path
|
||||
| None -> " "
|
||||
in
|
||||
Format.printf
|
||||
"%d matches%sfor spec %d (add -json for json format)@."
|
||||
(List.length matches)
|
||||
with_file
|
||||
(spec_number + 1)
|
||||
| Rewritten (replacements, result, _) ->
|
||||
match source_path, json, stdin with
|
||||
(* default: rewrite in place *)
|
||||
| Some path, false, false -> Out_channel.write_all path ~data:result
|
||||
(* stdin, not JSON *)
|
||||
| _, false, true -> Format.printf "%s%!" result
|
||||
(* stdin, JSON with path *)
|
||||
| Some path, true, _ ->
|
||||
let json_rewrites =
|
||||
let value = `List (List.map ~f:Rewrite.match_context_replacement_to_yojson replacements) in
|
||||
`Assoc [(path, value)]
|
||||
in
|
||||
Format.printf "%s%!" @@ Yojson.Safe.pretty_to_string json_rewrites
|
||||
(* JSON, no path *)
|
||||
| None, true, _ ->
|
||||
let json_rewrites =
|
||||
`List (List.map ~f:Rewrite.match_context_replacement_to_yojson replacements) in
|
||||
Format.printf "%s%!" @@ Yojson.Safe.pretty_to_string json_rewrites
|
||||
| _ -> Format.printf "%s%!" result
|
||||
|
||||
let write_statistics number_of_matches paths total_time =
|
||||
let total_time = Statistics.Time.stop total_time in
|
||||
let lines_of_code =
|
||||
List.fold paths ~init:0 ~f:(fun acc paths ->
|
||||
In_channel.read_lines paths
|
||||
|> List.length
|
||||
|> (+) acc)
|
||||
in
|
||||
let statistics =
|
||||
{ number_of_files = List.length paths
|
||||
; lines_of_code
|
||||
; number_of_matches
|
||||
; total_time = total_time
|
||||
}
|
||||
in
|
||||
Format.eprintf "%s%!"
|
||||
@@ Yojson.Safe.pretty_to_string
|
||||
@@ Statistics.to_yojson statistics
|
||||
|
||||
let paths_with_file_size paths =
|
||||
List.map paths ~f:(fun path ->
|
||||
let length =
|
||||
In_channel.create path
|
||||
|> fun channel ->
|
||||
In_channel.length channel
|
||||
|> Int64.to_int
|
||||
|> (fun value -> Option.value_exn value)
|
||||
|> (fun value -> In_channel.close channel; value)
|
||||
in
|
||||
(path, length))
|
||||
|
||||
let run
|
||||
matcher
|
||||
(sources : input_kind)
|
||||
(specifications : Specification.t list)
|
||||
sequential
|
||||
number_of_workers
|
||||
stdin
|
||||
json
|
||||
verbose
|
||||
match_timeout =
|
||||
let number_of_workers = if sequential then 0 else number_of_workers in
|
||||
let scheduler = Scheduler.create ~number_of_workers () in
|
||||
let configuration = Configuration.create ~match_kind:Fuzzy () in
|
||||
let total_time = Statistics.Time.start () in
|
||||
|
||||
let run_on_specifications input output_file =
|
||||
let result, count =
|
||||
List.fold specifications ~init:(Nothing,0) ~f:(fun (result, count) specification ->
|
||||
let input =
|
||||
match result with
|
||||
| Nothing | Matches _ -> input
|
||||
| Rewritten (_, content, _) -> String content
|
||||
in
|
||||
process_single_source matcher verbose configuration input specification match_timeout
|
||||
|> function
|
||||
| Nothing -> Nothing, count
|
||||
| Matches (x, number_of_matches) ->
|
||||
Matches (x, number_of_matches), count + number_of_matches
|
||||
| Rewritten (x, content, number_of_matches) ->
|
||||
Rewritten (x, content, number_of_matches),
|
||||
count + number_of_matches)
|
||||
in
|
||||
output_result stdin 0 json output_file result;
|
||||
count
|
||||
in
|
||||
|
||||
match sources with
|
||||
| String source ->
|
||||
let number_of_matches = run_on_specifications (String source) None in
|
||||
(* FIXME(RVT): statistics for single source text doesn't output LOC *)
|
||||
write_statistics number_of_matches [] total_time
|
||||
| Paths paths ->
|
||||
if sequential then
|
||||
let number_of_matches =
|
||||
List.fold ~init:0 paths ~f:(fun acc path ->
|
||||
let matches = run_on_specifications (Path path) (Some path) in
|
||||
acc + matches)
|
||||
in
|
||||
write_statistics number_of_matches paths total_time
|
||||
else
|
||||
let map init paths =
|
||||
List.fold
|
||||
paths
|
||||
~init
|
||||
~f:(fun count path -> count + run_on_specifications (Path path) (Some path))
|
||||
in
|
||||
let number_of_matches =
|
||||
try Scheduler.map_reduce scheduler ~init:0 ~map ~reduce:(+) paths
|
||||
with End_of_file -> 0
|
||||
in
|
||||
begin
|
||||
try Scheduler.destroy scheduler
|
||||
with Unix.Unix_error (_,"kill",_) ->
|
||||
(* No kill command on Mac OS X *)
|
||||
()
|
||||
end;
|
||||
write_statistics number_of_matches paths total_time
|
||||
| _ -> failwith "No single path handled here"
|
||||
|
||||
let parse_source_directories ?(file_extensions = []) target_directory =
|
||||
let rec ls_rec path =
|
||||
if Sys.is_file path = `Yes then
|
||||
match file_extensions with
|
||||
| [] -> [path]
|
||||
| suffixes when List.exists suffixes ~f:(fun suffix -> String.is_suffix ~suffix path) ->
|
||||
[path]
|
||||
| _ -> []
|
||||
else
|
||||
try
|
||||
Sys.ls_dir path
|
||||
|> List.map ~f:(fun sub -> ls_rec (Filename.concat path sub))
|
||||
|> List.concat
|
||||
with
|
||||
| _ -> []
|
||||
in
|
||||
ls_rec target_directory
|
||||
|
||||
let parse_specification_directories match_only specification_directory_paths =
|
||||
let parse_directory path =
|
||||
let match_template =
|
||||
let filename = path ^/ "match" in
|
||||
try read_template filename
|
||||
with _ -> failwith (Format.sprintf "Could not read required match file %s" filename)
|
||||
in
|
||||
let match_rule =
|
||||
let filename = path ^/ "match_rule" in
|
||||
try Some (read filename)
|
||||
with _ -> None
|
||||
in
|
||||
let rewrite_template =
|
||||
let filename = path ^/ "rewrite" in
|
||||
if match_only then
|
||||
None
|
||||
else
|
||||
try Some (read_template filename)
|
||||
with _ -> None
|
||||
in
|
||||
let rewrite_rule =
|
||||
let filename = path ^/ "rewrite_rule" in
|
||||
if match_only then
|
||||
None
|
||||
else
|
||||
try Some (read filename)
|
||||
with _ -> None
|
||||
in
|
||||
Specification.create ~match_template ?match_rule ?rewrite_template ?rewrite_rule ()
|
||||
in
|
||||
List.map specification_directory_paths ~f:parse_directory
|
||||
|
||||
let base_command_parameters : (unit -> 'result) Command.Param.t =
|
||||
[%map_open
|
||||
(* flags. *)
|
||||
let sequential = flag "sequential" no_arg ~doc:"Run sequentially"
|
||||
and match_only = flag "match-only" no_arg ~doc:"Only perform matching (ignore rewrite templates)"
|
||||
and verbose = flag "verbose" no_arg ~doc:(Format.sprintf "Log to %s" verbose_out_file)
|
||||
and rule = flag "rule" (optional_with_default "where true" string) ~doc:"rule Apply rules to matches. Respects -f"
|
||||
and match_timeout = flag "timeout" (optional_with_default 3 int) ~doc:"seconds Set match timeout on a source. Default: 3"
|
||||
and target_directory = flag "directory" (optional_with_default "." string) ~doc:(Format.sprintf "path Run on files in a directory. Default is current directory: %s" @@ Sys.getcwd ())
|
||||
and specification_directories = flag "templates" (optional (Arg_type.comma_separated string)) ~doc:"path CSV of directories containing templates"
|
||||
and file_extensions = flag "filter" (optional (Arg_type.comma_separated string)) ~doc:"extensions CSV of extensions to include"
|
||||
and json = flag "json" no_arg ~doc:"Output JSON format for matches or rewrite text to stdout"
|
||||
and number_of_workers = flag "jobs" (optional_with_default 4 int) ~doc:"n Number of worker processes. Default: 4"
|
||||
and stdin = flag "stdin" no_arg ~doc:"Read source from stdin"
|
||||
and anonymous_arguments =
|
||||
anon (maybe (t2
|
||||
("MATCH_TEMPLATE" %: string)
|
||||
("REWRITE_TEMPLATE" %: string)))
|
||||
in
|
||||
fun () ->
|
||||
let () =
|
||||
match Rule.create rule with
|
||||
| Ok _ -> ()
|
||||
| Error error ->
|
||||
let message = Error.to_string_hum error in
|
||||
Format.printf "Match rule parse error: %s@." message;
|
||||
exit 1
|
||||
in
|
||||
let specifications =
|
||||
match specification_directories, anonymous_arguments with
|
||||
| None, None
|
||||
| Some [], None ->
|
||||
Format.eprintf
|
||||
"Please either specify templates on the command line or using \
|
||||
-templates [dir] for templates in directory [dir].@.";
|
||||
exit 1
|
||||
| None, Some (match_template, rewrite_template) ->
|
||||
if match_only then
|
||||
[Specification.create ~match_template ~match_rule:rule ()]
|
||||
else
|
||||
[Specification.create ~match_template ~rewrite_template ~match_rule:rule ~rewrite_rule:rule ()]
|
||||
| Some specification_directories, None ->
|
||||
parse_specification_directories match_only specification_directories
|
||||
| Some specification_directories, Some _ ->
|
||||
Format.eprintf
|
||||
"Warning: ignoring match and rewrite templates and rules on \
|
||||
commandline and using those in directories instead@.";
|
||||
parse_specification_directories match_only specification_directories
|
||||
in
|
||||
let sources =
|
||||
match stdin with
|
||||
| false -> Paths (parse_source_directories ?file_extensions target_directory)
|
||||
| true -> String (In_channel.input_all In_channel.stdin)
|
||||
in
|
||||
|
||||
let matcher =
|
||||
let default = (module Matchers.C : Matchers.Matcher) in
|
||||
match file_extensions with
|
||||
| None | Some [] -> default
|
||||
| Some (hd::_) ->
|
||||
match hd with
|
||||
| ".c" | ".h" | ".cc" | ".cpp" | ".hpp" -> (module Matchers.C : Matchers.Matcher)
|
||||
| ".py" -> (module Matchers.Python : Matchers.Matcher)
|
||||
| ".go" -> (module Matchers.Go : Matchers.Matcher)
|
||||
| ".sh" -> (module Matchers.Bash : Matchers.Matcher)
|
||||
| ".html" -> (module Matchers.Html : Matchers.Matcher)
|
||||
| _ -> default
|
||||
in
|
||||
run matcher sources specifications sequential number_of_workers stdin json verbose match_timeout
|
||||
]
|
||||
|
||||
let default_command =
|
||||
Command.basic ~summary:"Run a rewrite pass." base_command_parameters
|
||||
|
||||
let () =
|
||||
Scheduler.Daemon.check_entry_point ();
|
||||
default_command
|
||||
|> Command.run
|
32
src/specification.ml
Normal file
32
src/specification.ml
Normal file
@ -0,0 +1,32 @@
|
||||
open Core
|
||||
|
||||
type match_specification =
|
||||
{ match_template : string
|
||||
; match_rule : string
|
||||
}
|
||||
[@@deriving show]
|
||||
|
||||
type rewrite_specification =
|
||||
{ rewrite_template : string
|
||||
; rewrite_rule : string
|
||||
}
|
||||
[@@deriving show]
|
||||
|
||||
type t =
|
||||
{ match_specification : match_specification
|
||||
; rewrite_specification : rewrite_specification option
|
||||
}
|
||||
[@@deriving show]
|
||||
|
||||
let create
|
||||
?rewrite_template
|
||||
?(match_rule = "where true")
|
||||
?(rewrite_rule = "")
|
||||
~match_template
|
||||
() =
|
||||
let match_specification = { match_template; match_rule } in
|
||||
let rewrite_specification =
|
||||
Option.map rewrite_template ~f:(fun rewrite_template ->
|
||||
{ rewrite_template; rewrite_rule})
|
||||
in
|
||||
{ match_specification; rewrite_specification }
|
21
test/dune
Normal file
21
test/dune
Normal file
@ -0,0 +1,21 @@
|
||||
(library
|
||||
(name test_integration)
|
||||
(modules
|
||||
test_match_rule
|
||||
test_integration
|
||||
test_statistics
|
||||
test_c
|
||||
test_cli
|
||||
test_bash
|
||||
test_go
|
||||
test_c_style_comments
|
||||
test_c_separators
|
||||
test_string_literals
|
||||
test_generic
|
||||
test_rewrite_parts
|
||||
test_rewrite_rule)
|
||||
(inline_tests)
|
||||
(preprocess (pps ppx_expect ppx_sexp_message))
|
||||
(libraries
|
||||
comby
|
||||
core))
|
77
test/test_bash.ml
Normal file
77
test/test_bash.ml
Normal file
@ -0,0 +1,77 @@
|
||||
open Core
|
||||
|
||||
open Matchers
|
||||
open Rewriter
|
||||
|
||||
let configuration = Configuration.create ~match_kind:Fuzzy ()
|
||||
|
||||
let run_bash source match_template rewrite_template =
|
||||
Bash.first ~configuration match_template source
|
||||
|> function
|
||||
| Ok result ->
|
||||
Rewrite.all ~source ~rewrite_template [result]
|
||||
|> (fun x -> Option.value_exn x)
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string
|
||||
| Error _ ->
|
||||
print_string rewrite_template
|
||||
|
||||
let run_go source match_template rewrite_template =
|
||||
Go.first ~configuration match_template source
|
||||
|> function
|
||||
| Ok result ->
|
||||
Rewrite.all ~source ~rewrite_template [result]
|
||||
|> (fun x -> Option.value_exn x)
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string
|
||||
| Error _ ->
|
||||
print_string rewrite_template
|
||||
|
||||
let%expect_test "custom_long_delimiters" =
|
||||
let source =
|
||||
{|
|
||||
case
|
||||
case
|
||||
block 1
|
||||
esac
|
||||
|
||||
case
|
||||
block 2
|
||||
esac
|
||||
esac
|
||||
|}
|
||||
in
|
||||
let match_template = {|case :[1] esac|} in
|
||||
let rewrite_template = {|case nuked blocks esac|} in
|
||||
|
||||
run_bash source match_template rewrite_template;
|
||||
[%expect_exact {|
|
||||
case nuked blocks esac
|
||||
|}]
|
||||
|
||||
let%expect_test "custom_long_delimiters_doesn't_work_in_go" =
|
||||
let source =
|
||||
{|
|
||||
case
|
||||
case
|
||||
block 1
|
||||
esac
|
||||
|
||||
case
|
||||
block 2
|
||||
esac
|
||||
esac
|
||||
|}
|
||||
in
|
||||
let match_template = {|case :[1] esac|} in
|
||||
let rewrite_template = {|case nuked blocks esac|} in
|
||||
|
||||
run_go source match_template rewrite_template;
|
||||
[%expect_exact {|
|
||||
case nuked blocks esac
|
||||
|
||||
case
|
||||
block 2
|
||||
esac
|
||||
esac
|
||||
|}]
|
105
test/test_c.ml
Normal file
105
test/test_c.ml
Normal file
@ -0,0 +1,105 @@
|
||||
open Core
|
||||
|
||||
open Matchers
|
||||
open Rewriter
|
||||
|
||||
let configuration = Configuration.create ~match_kind:Fuzzy ()
|
||||
|
||||
let run source match_template rewrite_template =
|
||||
C.first ~configuration match_template source
|
||||
|> function
|
||||
| Ok result ->
|
||||
Rewrite.all ~source ~rewrite_template [result]
|
||||
|> (fun x -> Option.value_exn x)
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string
|
||||
| Error _ ->
|
||||
print_string rewrite_template
|
||||
|
||||
let%expect_test "comments_1" =
|
||||
let source = {|match this /**/ expect end|} in
|
||||
let match_template = {|match this :[1] end|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|expect|}]
|
||||
|
||||
let%expect_test "comments_2" =
|
||||
let source = {|match this /* */ expect end|} in
|
||||
let match_template = {|match this :[1] end|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|expect|}]
|
||||
|
||||
let%expect_test "comments_3" =
|
||||
let source = {|match this /* blah blah */ expect /**/ end|} in
|
||||
let match_template = {|match this :[1] end|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|expect|}]
|
||||
|
||||
let%expect_test "comments_4" =
|
||||
let source = {|match this expect/**/end|} in
|
||||
let match_template = {|match this :[1]end|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|expect|}]
|
||||
|
||||
let%expect_test "comments_5" =
|
||||
let source = {|match this expect /**/end|} in
|
||||
let match_template = {|match this :[1] end|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|expect|}]
|
||||
|
||||
let%expect_test "comments_6" =
|
||||
let source = {|/* don't match this (a) end */|} in
|
||||
let match_template = {|match this :[1] end|} in
|
||||
let rewrite_template = {|nothing matches|} in
|
||||
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|nothing matches|}]
|
||||
|
||||
let%expect_test "comments_7" =
|
||||
let source = {|/* don't match /**/ this (a) end */|} in
|
||||
let match_template = {|match this :[1] end|} in
|
||||
let rewrite_template = {|nothing matches|} in
|
||||
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|nothing matches|}]
|
||||
|
||||
let%expect_test "comments_8" =
|
||||
let source = {|(/* don't match this (a) end */)|} in
|
||||
let match_template = {|match this :[1] end|} in
|
||||
let rewrite_template = {|nothing matches|} in
|
||||
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|nothing matches|}]
|
||||
|
||||
let%expect_test "comments_9" =
|
||||
let source = {|/* don't match this (a) end */ do match this (b) end|} in
|
||||
let match_template = {|match this :[1] end|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|/* don't match this (a) end */ do (b)|}]
|
||||
|
||||
let%expect_test "comments_10" =
|
||||
let source = {|/* don't match this (a) end */ do match this () end|} in
|
||||
let match_template = {|match this :[1] end|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|/* don't match this (a) end */ do ()|}]
|
||||
|
||||
let%expect_test "comments_11" =
|
||||
let source = {|do match this (b) end /* don't match this (a) end */|} in
|
||||
let match_template = {|match this :[1] end|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|do (b) /* don't match this (a) end */|}]
|
36
test/test_c_separators.ml
Normal file
36
test/test_c_separators.ml
Normal file
@ -0,0 +1,36 @@
|
||||
open Core
|
||||
|
||||
open Matchers
|
||||
open Rewriter
|
||||
|
||||
let configuration = Configuration.create ~match_kind:Fuzzy ()
|
||||
|
||||
let run source match_template rewrite_template =
|
||||
C.first ~configuration match_template source
|
||||
|> function
|
||||
| Ok result ->
|
||||
Rewrite.all ~source ~rewrite_template [result]
|
||||
|> (fun x -> Option.value_exn x)
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string
|
||||
| Error _ ->
|
||||
print_string rewrite_template
|
||||
|
||||
let%expect_test "whitespace_should_not_matter_between_separators" =
|
||||
let source = {|*p|} in
|
||||
let match_template = {|*:[1]|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|p|}];
|
||||
|
||||
let source = {|* p|} in
|
||||
let match_template = {|*:[1]|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {| p|}];
|
||||
|
||||
let source = {|* p|} in
|
||||
let match_template = {|* :[1]|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|p|}]
|
225
test/test_c_style_comments.ml
Normal file
225
test/test_c_style_comments.ml
Normal file
@ -0,0 +1,225 @@
|
||||
open Core
|
||||
|
||||
open Matchers
|
||||
open Rewriter
|
||||
|
||||
let configuration = Configuration.create ~match_kind:Fuzzy ()
|
||||
|
||||
let all ?(configuration = configuration) template source =
|
||||
C.all ~configuration ~template ~source
|
||||
|
||||
let print_matches matches =
|
||||
List.map matches ~f:Match.to_yojson
|
||||
|> (fun matches -> `List matches)
|
||||
|> Yojson.Safe.pretty_to_string
|
||||
|> print_string
|
||||
|
||||
let%expect_test "rewrite_comments_1" =
|
||||
let template = "replace this :[1] end" in
|
||||
let source = "/* don't replace this () end */ do replace this () end" in
|
||||
let rewrite_template = "X" in
|
||||
|
||||
all template source
|
||||
|> (fun matches ->
|
||||
Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact "/* don't replace this () end */ do X"]
|
||||
|
||||
let%expect_test "rewrite_comments_2" =
|
||||
let template =
|
||||
{|
|
||||
if (:[1]) { :[2] }
|
||||
|}
|
||||
in
|
||||
|
||||
let source =
|
||||
{|
|
||||
/* if (fake_condition_body_must_be_non_empty) { fake_body; } */
|
||||
// if (fake_condition_body_must_be_non_empty) { fake_body; }
|
||||
if (real_condition_body_must_be_empty) {
|
||||
int i;
|
||||
int j;
|
||||
}
|
||||
|}
|
||||
in
|
||||
|
||||
let rewrite_template =
|
||||
{|
|
||||
if (:[1]) {}
|
||||
|}
|
||||
in
|
||||
|
||||
all template source
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact
|
||||
{|
|
||||
/* if (fake_condition_body_must_be_non_empty) { fake_body; } */
|
||||
if (real_condition_body_must_be_empty) {}
|
||||
|}]
|
||||
|
||||
let%expect_test "capture_comments" =
|
||||
let template = {|if (:[1]) { :[2] }|} in
|
||||
let source = {|if (true) { /* some comment */ console.log(z); }|} in
|
||||
let matches = all template source in
|
||||
print_matches matches;
|
||||
[%expect_exact {|[
|
||||
{
|
||||
"range": {
|
||||
"start": { "offset": 0, "line": 1, "column": 1 },
|
||||
"end": { "offset": 48, "line": 1, "column": 49 }
|
||||
},
|
||||
"environment": [
|
||||
[
|
||||
"1",
|
||||
{
|
||||
"value": "true",
|
||||
"range": {
|
||||
"start": { "offset": 4, "line": 1, "column": 5 },
|
||||
"end": { "offset": 8, "line": 1, "column": 9 }
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"2",
|
||||
{
|
||||
"value": "console.log(z);",
|
||||
"range": {
|
||||
"start": { "offset": 31, "line": 1, "column": 32 },
|
||||
"end": { "offset": 46, "line": 1, "column": 47 }
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"matched": "if (true) { /* some comment */ console.log(z); }"
|
||||
}
|
||||
]|}]
|
||||
|
||||
let%expect_test "single_quote_in_comment" =
|
||||
let template =
|
||||
{| {:[1]} |}
|
||||
in
|
||||
|
||||
let source =
|
||||
{|
|
||||
/*'*/
|
||||
{test}
|
||||
|}
|
||||
in
|
||||
|
||||
let rewrite_template =
|
||||
{|
|
||||
{:[1]}
|
||||
|}
|
||||
in
|
||||
|
||||
all template source
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact
|
||||
{|
|
||||
{test}
|
||||
|}]
|
||||
|
||||
let%expect_test "single_quote_in_comment" =
|
||||
let template =
|
||||
{| {:[1]} |}
|
||||
in
|
||||
|
||||
let source =
|
||||
{|
|
||||
{
|
||||
a = 1;
|
||||
/* Events with mask == AE_NONE are not set. So let's initiaize the
|
||||
* vector with it. */
|
||||
for (i = 0; i < setsize; i++)
|
||||
}
|
||||
|}
|
||||
in
|
||||
|
||||
let rewrite_template =
|
||||
{|
|
||||
{:[1]}
|
||||
|}
|
||||
in
|
||||
|
||||
all template source
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact
|
||||
{|
|
||||
{
|
||||
a = 1;
|
||||
/* Events with mask == AE_NONE are not set. So let's initiaize the
|
||||
* vector with it. */
|
||||
for (i = 0; i < setsize; i++)
|
||||
}
|
||||
|}]
|
||||
|
||||
let%expect_test "single_quote_in_comment" =
|
||||
let template =
|
||||
{| {:[1]} |}
|
||||
in
|
||||
|
||||
let source =
|
||||
{|
|
||||
{
|
||||
a = 1;
|
||||
/* ' */
|
||||
for (i = 0; i < setsize; i++)
|
||||
}
|
||||
|}
|
||||
in
|
||||
|
||||
let rewrite_template =
|
||||
{|
|
||||
{:[1]}
|
||||
|}
|
||||
in
|
||||
|
||||
all template source
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact
|
||||
{|
|
||||
{
|
||||
a = 1;
|
||||
/* ' */
|
||||
for (i = 0; i < setsize; i++)
|
||||
}
|
||||
|}]
|
||||
|
||||
let%expect_test "give_back_the_comment_characters_for_newline_comments_too" =
|
||||
let template =
|
||||
{| {:[1]} |}
|
||||
in
|
||||
|
||||
let source =
|
||||
{|
|
||||
{
|
||||
// a comment
|
||||
}
|
||||
|}
|
||||
in
|
||||
|
||||
let rewrite_template =
|
||||
{|
|
||||
{:[1]}
|
||||
|}
|
||||
in
|
||||
|
||||
all template source
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact
|
||||
{|
|
||||
{
|
||||
// a comment
|
||||
}
|
||||
|}]
|
178
test/test_cli.ml
Normal file
178
test/test_cli.ml
Normal file
@ -0,0 +1,178 @@
|
||||
open Core
|
||||
|
||||
module Time = Core_kernel.Time_ns.Span
|
||||
|
||||
let binary_path = "../../../comby"
|
||||
|
||||
let read_with_timeout read_from_channel =
|
||||
let read_from_fd = Unix.descr_of_in_channel read_from_channel in
|
||||
let read_from_channel =
|
||||
Unix.select ~read:[read_from_fd] ~write:[] ~except:[] ~timeout:(`After (Time.of_int_sec 5)) ()
|
||||
|> (fun { Unix.Select_fds.read; _ } -> List.hd_exn read)
|
||||
|> Unix.in_channel_of_descr
|
||||
in
|
||||
In_channel.input_all read_from_channel
|
||||
|
||||
let read_source_from_stdin command source =
|
||||
let open Unix.Process_channels in
|
||||
let { stdin; stdout ; stderr = _ } = Unix.open_process_full ~env:[||] command in
|
||||
Out_channel.output_string stdin source;
|
||||
Out_channel.flush stdin;
|
||||
Out_channel.close stdin;
|
||||
read_with_timeout stdout
|
||||
|
||||
let%expect_test "stdin_command" =
|
||||
let source = "hello world" in
|
||||
let match_template = "hello :[1]" in
|
||||
let rewrite_template = ":[1]" in
|
||||
let command_args =
|
||||
Format.sprintf "-stdin '%s' '%s' -f .c" match_template rewrite_template
|
||||
in
|
||||
let command = Format.sprintf "%s %s" binary_path command_args in
|
||||
read_source_from_stdin command source
|
||||
|> print_string;
|
||||
[%expect_exact {|world|}]
|
||||
|
||||
let%expect_test "with_match_rule" =
|
||||
let source = "hello world" in
|
||||
let match_template = "hello :[1]" in
|
||||
let rewrite_template = ":[1]" in
|
||||
let rule = {|where :[1] == "world"|} in
|
||||
let command_args =
|
||||
Format.sprintf "-stdin '%s' '%s' -rule '%s' -f .c "
|
||||
match_template rewrite_template rule
|
||||
in
|
||||
let command = Format.sprintf "%s %s" binary_path command_args in
|
||||
read_source_from_stdin command source
|
||||
|> print_string;
|
||||
[%expect_exact {|world|}];
|
||||
|
||||
let source = "hello world" in
|
||||
let match_template = "hello :[1]" in
|
||||
let rewrite_template = ":[1]" in
|
||||
let rule = {|where :[1] != "world"|} in
|
||||
let command_args =
|
||||
Format.sprintf "-stdin '%s' '%s' -rule '%s' -f .c "
|
||||
match_template rewrite_template rule
|
||||
in
|
||||
let command = Format.sprintf "%s %s" binary_path command_args in
|
||||
read_source_from_stdin command source
|
||||
|> print_string;
|
||||
[%expect_exact {|hello world|}]
|
||||
|
||||
let%expect_test "with_rewrite_rule" =
|
||||
let source = "hello world" in
|
||||
let match_template = ":[2] :[1]" in
|
||||
let rewrite_template = ":[1]" in
|
||||
let rule = {|where rewrite :[1] { | ":[_]" -> ":[2]" }|} in
|
||||
let command_args =
|
||||
Format.sprintf "-stdin '%s' '%s' -rule '%s' -f .c "
|
||||
match_template rewrite_template rule
|
||||
in
|
||||
let command = Format.sprintf "%s %s" binary_path command_args in
|
||||
read_source_from_stdin command source
|
||||
|> print_string;
|
||||
[%expect_exact {|hello|}]
|
||||
|
||||
let%expect_test "json_output_option" =
|
||||
let source = "a X c a Y c" in
|
||||
let match_template = "a :[1] c" in
|
||||
let rewrite_template = "c :[1] a" in
|
||||
let command_args =
|
||||
Format.sprintf "-stdin -json '%s' '%s' -f .c "
|
||||
match_template rewrite_template
|
||||
in
|
||||
let command = Format.sprintf "%s %s" binary_path command_args in
|
||||
read_source_from_stdin command source
|
||||
|> print_string;
|
||||
[%expect_exact {|[
|
||||
{
|
||||
"range": {
|
||||
"start": { "offset": 6, "line": -1, "column": -1 },
|
||||
"end": { "offset": 11, "line": -1, "column": -1 }
|
||||
},
|
||||
"replacement_content": "c Y a",
|
||||
"environment": [
|
||||
[
|
||||
"1",
|
||||
{
|
||||
"value": "Y",
|
||||
"range": {
|
||||
"start": { "offset": 2, "line": -1, "column": -1 },
|
||||
"end": { "offset": 3, "line": -1, "column": -1 }
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"range": {
|
||||
"start": { "offset": 0, "line": -1, "column": -1 },
|
||||
"end": { "offset": 5, "line": -1, "column": -1 }
|
||||
},
|
||||
"replacement_content": "c X a",
|
||||
"environment": [
|
||||
[
|
||||
"1",
|
||||
{
|
||||
"value": "X",
|
||||
"range": {
|
||||
"start": { "offset": 2, "line": -1, "column": -1 },
|
||||
"end": { "offset": 3, "line": -1, "column": -1 }
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
]|}];
|
||||
|
||||
let source = "a X c a Y c" in
|
||||
let match_template = "a :[1] c" in
|
||||
let rewrite_template = "c :[1] a" in
|
||||
let command_args =
|
||||
Format.sprintf "-stdin -json -match-only '%s' '%s' -f .c "
|
||||
match_template rewrite_template
|
||||
in
|
||||
let command = Format.sprintf "%s %s" binary_path command_args in
|
||||
read_source_from_stdin command source
|
||||
|> print_string;
|
||||
[%expect_exact {|[
|
||||
{
|
||||
"range": {
|
||||
"start": { "offset": 0, "line": 1, "column": 1 },
|
||||
"end": { "offset": 5, "line": 1, "column": 6 }
|
||||
},
|
||||
"environment": [
|
||||
[
|
||||
"1",
|
||||
{
|
||||
"value": "X",
|
||||
"range": {
|
||||
"start": { "offset": 2, "line": 1, "column": 3 },
|
||||
"end": { "offset": 3, "line": 1, "column": 4 }
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"matched": "a X c"
|
||||
},
|
||||
{
|
||||
"range": {
|
||||
"start": { "offset": 6, "line": 1, "column": 7 },
|
||||
"end": { "offset": 11, "line": 1, "column": 12 }
|
||||
},
|
||||
"environment": [
|
||||
[
|
||||
"1",
|
||||
{
|
||||
"value": "Y",
|
||||
"range": {
|
||||
"start": { "offset": 8, "line": 1, "column": 9 },
|
||||
"end": { "offset": 9, "line": 1, "column": 10 }
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"matched": "a Y c"
|
||||
}
|
||||
]|}]
|
408
test/test_generic.ml
Normal file
408
test/test_generic.ml
Normal file
@ -0,0 +1,408 @@
|
||||
open Core
|
||||
|
||||
open Matchers
|
||||
open Rewriter
|
||||
|
||||
let configuration = Configuration.create ~match_kind:Fuzzy ()
|
||||
|
||||
let format s =
|
||||
let s = String.chop_prefix_exn ~prefix:"\n" s in
|
||||
let leading_indentation = Option.value_exn (String.lfindi s ~f:(fun _ c -> c <> ' ')) in
|
||||
s
|
||||
|> String.split ~on:'\n'
|
||||
|> List.map ~f:(Fn.flip String.drop_prefix leading_indentation)
|
||||
|> String.concat ~sep:"\n"
|
||||
|> String.chop_suffix_exn ~suffix:"\n"
|
||||
|
||||
let run ?(configuration = configuration) source match_template rewrite_template =
|
||||
Generic.first ~configuration match_template source
|
||||
|> function
|
||||
| Ok result ->
|
||||
Rewrite.all ~source ~rewrite_template [result]
|
||||
|> (fun x -> Option.value_exn x)
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string
|
||||
| Error _ ->
|
||||
(* this is too annoying to fix everytime the grammar changes. *)
|
||||
print_string ""
|
||||
|
||||
let run_all ?(configuration = configuration) source match_template rewrite_template =
|
||||
Generic.all ~configuration ~template:match_template ~source
|
||||
|> (fun results -> Option.value_exn (Rewrite.all ~source ~rewrite_template results))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string
|
||||
|
||||
|
||||
let%expect_test "basic" =
|
||||
let source = {|a b c d|} in
|
||||
let match_template = {|:[1]|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|a b c d|}];
|
||||
|
||||
let source = {|a b c d|} in
|
||||
let match_template = {|a :[1] c d|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|b|}];
|
||||
|
||||
let source = {|a b c d|} in
|
||||
let match_template = {|a :[1] d|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|b c|}];
|
||||
|
||||
let source = {|a b c d|} in
|
||||
let match_template = {|a :[1]|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|b c d|}];
|
||||
|
||||
let source = {|a b c d|} in
|
||||
let match_template = {|:[1] c d|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|a b|}];
|
||||
|
||||
let source = {|a b c d|} in
|
||||
let match_template = {|:[1] :[2]|} in
|
||||
let rewrite_template = {|(:[1]) (:[2])|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|(a) (b c d)|}];
|
||||
|
||||
let source = {|a b c d|} in
|
||||
let match_template = {|:[2] :[1]|} in
|
||||
let rewrite_template = {|(:[2]) (:[1])|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|(a) (b c d)|}];
|
||||
|
||||
let source = {|a b c d|} in
|
||||
let match_template = {|a :[2] :[1] d|} in
|
||||
let rewrite_template = {|(:[2]) (:[1])|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|(b) (c)|}];
|
||||
|
||||
let source = {|a b c d|} in
|
||||
let match_template = {|a :[2] :[1]|} in
|
||||
let rewrite_template = {|(:[2]) (:[1])|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|(b) (c d)|}];
|
||||
|
||||
let source = {|a b c d|} in
|
||||
let match_template = {|a :[2] c :[1]|} in
|
||||
let rewrite_template = {|(:[2]) (:[1])|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|(b) (d)|}];
|
||||
|
||||
let source = {|x:|} in
|
||||
let match_template = {|:[1]:|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|x|}]
|
||||
|
||||
let%expect_test "basic_failures" =
|
||||
let source = {|a x b bbq|} in
|
||||
let match_template = {|a :[1] b c|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {||}];
|
||||
|
||||
let source = {|a b c d|} in
|
||||
let match_template = {|a :[2] d :[1]|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact
|
||||
{||}];
|
||||
|
||||
let source = {|a b c d|} in
|
||||
let match_template = {|a :[2] b :[1]|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {||}]
|
||||
|
||||
let%expect_test "delimiter_matching" =
|
||||
let source = {|(a b c) d|} in
|
||||
let match_template = {|(:[1]) d|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|a b c|}];
|
||||
|
||||
let source = {|(a b c) d|} in
|
||||
let match_template = {|(:[1] b :[2]) d|} in
|
||||
let rewrite_template = {|(:[1]) (:[2])|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|(a) (c)|}];
|
||||
|
||||
let source = {|q(a b c) d|} in
|
||||
let match_template = {|q(:[1] b :[2]) d|} in
|
||||
let rewrite_template = {|(:[1]) (:[2])|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|(a) (c)|}];
|
||||
|
||||
let source = {|((a) b)|} in
|
||||
let match_template = {|(:[1] b)|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|(a)|}];
|
||||
|
||||
let source = {|((a b c)) d|} in
|
||||
let match_template = {|(:[1]) d|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|(a b c)|}];
|
||||
|
||||
let source = {|((a b c)) d|} in
|
||||
let match_template = {|(:[1]) d|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|(a b c)|}];
|
||||
|
||||
let source = {|((a b c) q) d|} in
|
||||
let match_template = {|((:[1]) q) d|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|a b c|}];
|
||||
|
||||
let source = {|((a b c) q) d|} in
|
||||
let match_template = {|((:[1] c) q) d|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|a b|}];
|
||||
|
||||
let source = {|((a b () c) q) d|} in
|
||||
let match_template = {|((:[1] () c) q) d|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|a b|}];
|
||||
|
||||
let source = {|((a ((x) d) b c)) d|} in
|
||||
let match_template = {|((a :[1] :[2] c)) d|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|((x) d)|}];
|
||||
|
||||
let source = {|((a ((x) d) b c)) d|} in
|
||||
let match_template = {|((a (:[1]) :[2] c)) d|} in
|
||||
let rewrite_template = {|:[1] :[2]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|(x) d b|}];
|
||||
|
||||
let source = {|(b (c) d)|} in
|
||||
let match_template = {|(:[1])|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|b (c) d|}];
|
||||
|
||||
let source = {|(b (c) d.)|} in
|
||||
let match_template = {|(:[1].)|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|b (c) d|}];
|
||||
|
||||
let source = {|(b (c.) d.)|} in
|
||||
let match_template = {|(:[1].)|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|b (c.) d|}];
|
||||
|
||||
let source = {|(b. (c) d.)|} in
|
||||
let match_template = {|(:[1].)|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|b. (c) d|}];
|
||||
|
||||
let source = {|(b (c) d.)|} in
|
||||
let match_template = {|(b :[1] d.)|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|(c)|}];
|
||||
|
||||
let source = {|outer(inner(dst,src),src)|} in
|
||||
let match_template = {|outer(:[1],src)|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|inner(dst,src)|}];
|
||||
|
||||
let source = {|(b ((c)) d.)|} in
|
||||
let match_template = {|(b :[1] d.)|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|((c))|}];
|
||||
|
||||
let source = {|a b c|} in
|
||||
let match_template = {|a :[1] c|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|b|}];
|
||||
|
||||
let source = {|x = foo;|} in
|
||||
let match_template = {|x = :[1];|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|foo|}];
|
||||
|
||||
let source = {|((a {{x} d} b c)) d|} in
|
||||
let match_template = {|((a {:[1] d} :[2] c)) d|} in
|
||||
let rewrite_template = {|:[1] :[2]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|{x} b|}];
|
||||
|
||||
let source = {|((a {([{x}]) d} b c)) d|} in
|
||||
let match_template = {|((a {:[1] d} :[2] c)) d|} in
|
||||
let rewrite_template = {|:[1] :[2]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|([{x}]) b|}];
|
||||
|
||||
let source = {|(((((x)))))|} in
|
||||
let match_template = {|(((:[1])))|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|((x))|}];
|
||||
|
||||
let source = {|((((y(x)z))))|} in
|
||||
let match_template = {|(((:[1])))|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|(y(x)z)|}];
|
||||
|
||||
let source = {|((((y(x)z))))|} in
|
||||
let match_template = {|(((:[1]):[2]))|} in
|
||||
let rewrite_template = {|:[1] :[2]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|(y(x)z) |}];
|
||||
|
||||
let source = {|(((x)z))|} in
|
||||
let match_template = {|(((:[1]):[2]))|} in
|
||||
let rewrite_template = {|:[1] :[2]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|x z|}];
|
||||
|
||||
let source = {|((((x))z))|} in
|
||||
let match_template = {|(((:[1]):[2]))|} in
|
||||
let rewrite_template = {|:[1] :[2]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|(x) z|}];
|
||||
|
||||
let source = {|lolwtfbbq|} in
|
||||
let match_template = {|lol:[1]bbq|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|wtf|}];
|
||||
|
||||
let source = {|x = foo; x = bar;|} in
|
||||
let match_template = {|x = :[1];|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|foo x = bar;|}];
|
||||
|
||||
let source = {|[ no match prefix ] x = foo; [ no match suffix ]|} in
|
||||
let match_template = {|x = :[1];|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|[ no match prefix ] foo [ no match suffix ]|}];
|
||||
|
||||
let source = {|x = a; x = b; x = c|} in
|
||||
let match_template = {|x = :[1];|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|a x = b; x = c|}];
|
||||
|
||||
let source = {|x = ( x = x; );|} in
|
||||
let match_template = {|x = :[1];|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|( x = x; )|}];
|
||||
|
||||
let source = {|( x = x = x; )|} in
|
||||
let match_template = {|x = :[1];|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|( x = x )|}];
|
||||
|
||||
let source = {|xxx a b d c 1 2 3 b d d blah|} in
|
||||
let match_template = {|a :[1] c :[2] d|} in
|
||||
let rewrite_template = {|:[1] :[2]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|xxx b d 1 2 3 b d blah|}];
|
||||
|
||||
let source = {|howevenlolwtfbbqispossible|} in
|
||||
let match_template = {|lol:[1]bbq|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|howevenwtfispossible|}];
|
||||
|
||||
let source = {|lolhowevenlolwtfbbqispossiblebbq|} in
|
||||
let match_template = {|lol:[1]bbq|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|howevenlolwtfispossiblebbq|}];
|
||||
|
||||
let source = {|hello my name is bob the builder|} in
|
||||
let match_template = {|:[alongidentifiername] :[2] :[3] :[xyz] :[5] :[6]|} in
|
||||
let rewrite_template = {|:[alongidentifiername] :[2] :[3] :[xyz] :[5] :[6]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|hello my name is bob the builder|}];
|
||||
|
||||
let source = {|www.testdofooname.com/picsinsideit/stunningpictureofkays1381737242g8k4n-280x428.jpg|} in
|
||||
let match_template = {|www.:[1]-:[2].jpg|} in
|
||||
let rewrite_template = {|:[1] :[2]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|testdofooname.com/picsinsideit/stunningpictureofkays1381737242g8k4n 280x428|}];
|
||||
|
||||
let source = {|https://api.github.com/repos/dmjacobsen/slurm/commits/716c1499695c68afcab848a1b49653574b4fc167|} in
|
||||
let match_template = {|:[1]api.:[2]/repos/:[3]s/:[4]|} in
|
||||
let rewrite_template = {|:[1] :[2] :[3] :[4]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|https:// github.com dmjacobsen/slurm/commit 716c1499695c68afcab848a1b49653574b4fc167|}];
|
||||
|
||||
let source =
|
||||
{|
|
||||
assert(stream->md_len + md_len -
|
||||
si.foo_data_begin <= MAD_BUFFER_MDLEN);
|
||||
memcpy(*stream->foo_data + stream->md_len,
|
||||
mad_bit_nextbyte(&stream->ptr),
|
||||
frame_used = md_len - si.foo_data_begin);
|
||||
stream->md_len += frame_used;
|
||||
|} |> format
|
||||
in
|
||||
let match_template = {|memcpy(:[1], :[2], :[3]);|} in
|
||||
let rewrite_template = {|:[1], :[2], :[3]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|assert(stream->md_len + md_len -
|
||||
si.foo_data_begin <= MAD_BUFFER_MDLEN);
|
||||
*stream->foo_data + stream->md_len, mad_bit_nextbyte(&stream->ptr), frame_used = md_len - si.foo_data_begin
|
||||
stream->md_len += frame_used;|}]
|
||||
|
||||
let%expect_test "significant_whitespace" =
|
||||
let configuration = Configuration.create ~match_kind:Fuzzy ~significant_whitespace:true () in
|
||||
let run = run ~configuration in
|
||||
|
||||
let source = {|two spaces|} in
|
||||
let match_template = {|:[1] :[2]|} in
|
||||
let rewrite_template = {|:[1] :[2]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|two spaces|}];
|
||||
|
||||
(* FIXME: this should fail. also test case where separators do or do not need
|
||||
whitespace. e.g., strict about strcpy(src,dst) matching a template
|
||||
strcpy(:[1],:[2]) versus strcpy(:[1], :[2]) *)
|
||||
let source = {|two spaces|} in
|
||||
let match_template = {|:[1] :[2]|} in
|
||||
let rewrite_template = {|:[1] :[2]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|two spaces|}]
|
||||
|
||||
let%expect_test "contextual_matching" =
|
||||
let run = run_all in
|
||||
|
||||
let source = {|memcpy(dst1, src1, 1); memcpy(dst2, src2, 2);|} in
|
||||
let match_template = {|memcpy(:[1], :[2], :[3])|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|dst1; dst2;|}];
|
||||
|
||||
let source = {|memcpy(dst1, src1, 1); memcpy(dst2, src2, 2);|} in
|
||||
let match_template = {|memcpy(:[1], :[2], :[3])|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|dst1; dst2;|}];
|
101
test/test_go.ml
Normal file
101
test/test_go.ml
Normal file
@ -0,0 +1,101 @@
|
||||
open Core
|
||||
|
||||
open Language
|
||||
open Matchers
|
||||
open Match
|
||||
open Rewriter
|
||||
|
||||
let configuration = Configuration.create ~match_kind:Fuzzy ()
|
||||
|
||||
let run ?(rule = "where true") source match_template rewrite_template =
|
||||
let rule = Rule.create rule |> Or_error.ok_exn in
|
||||
Go.first ~configuration match_template source
|
||||
|> function
|
||||
| Ok ({environment; _ } as result) ->
|
||||
if Rule.(sat @@ apply rule environment) then
|
||||
Rewrite.all ~source ~rewrite_template [result]
|
||||
|> (fun x -> Option.value_exn x)
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source) |> print_string
|
||||
else
|
||||
assert false
|
||||
| Error _ ->
|
||||
print_string rewrite_template
|
||||
|
||||
let%expect_test "gosimple_s1000" =
|
||||
let source =
|
||||
{|
|
||||
select {
|
||||
case x := <-ch:
|
||||
fmt.Println(x)
|
||||
}
|
||||
|}
|
||||
in
|
||||
|
||||
let match_template =
|
||||
{|
|
||||
select {
|
||||
case :[1] := :[2]:
|
||||
:[3]
|
||||
}
|
||||
|}
|
||||
in
|
||||
|
||||
let rewrite_template =
|
||||
{|
|
||||
:[1] := :[2]
|
||||
:[3]
|
||||
|}
|
||||
in
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|
|
||||
x := <-ch
|
||||
fmt.Println(x)
|
||||
|}]
|
||||
|
||||
let%expect_test "gosimple_s1001" =
|
||||
let source =
|
||||
{|
|
||||
for i, x := range src {
|
||||
dst[i] = x
|
||||
}
|
||||
|}
|
||||
in
|
||||
|
||||
let match_template =
|
||||
{|
|
||||
for :[index_define], :[src_element_define] := range :[src_array] {
|
||||
:[dst_array][:[index_use]] = :[src_element_use]
|
||||
}
|
||||
|}
|
||||
in
|
||||
|
||||
let rewrite_template =
|
||||
{|
|
||||
copy(:[dst_array], :[src_array])
|
||||
|}
|
||||
in
|
||||
|
||||
let rule = {|where :[index_define] == :[index_use], :[src_element_define] == :[src_element_use]|} in
|
||||
|
||||
run ~rule source match_template rewrite_template;
|
||||
[%expect_exact {|
|
||||
copy(dst, src)
|
||||
|}]
|
||||
|
||||
let%expect_test "gosimple_s1003" =
|
||||
let source =
|
||||
{|
|
||||
if strings.Index(x, y) != -1 { ignore }
|
||||
|}
|
||||
in
|
||||
|
||||
let match_template =
|
||||
{|
|
||||
if strings.:[1](x, y) != -1 { :[_] }
|
||||
|}
|
||||
in
|
||||
|
||||
let rewrite_template = {|:[1]|} in
|
||||
|
||||
run source match_template rewrite_template;
|
||||
[%expect_exact {|Index|}]
|
295
test/test_integration.ml
Normal file
295
test/test_integration.ml
Normal file
@ -0,0 +1,295 @@
|
||||
open Core
|
||||
|
||||
open Matchers
|
||||
open Rewriter
|
||||
|
||||
let configuration = Configuration.create ~match_kind:Fuzzy ()
|
||||
|
||||
let all ?(configuration = configuration) template source =
|
||||
Generic.all ~configuration ~template ~source
|
||||
|
||||
let print_matches matches =
|
||||
List.map matches ~f:Match.to_yojson
|
||||
|> (fun matches -> `List matches)
|
||||
|> Yojson.Safe.pretty_to_string
|
||||
|> print_string
|
||||
|
||||
let%expect_test "dont_get_stuck" =
|
||||
let template = "" in
|
||||
let source = "a" in
|
||||
let matches = all ~configuration template source in
|
||||
print_matches matches;
|
||||
[%expect_exact {|[]|}]
|
||||
|
||||
let%expect_test "dont_get_stuck" =
|
||||
let template = "a" in
|
||||
let source = "a" in
|
||||
let matches = all template source in
|
||||
print_matches matches;
|
||||
[%expect_exact {|[
|
||||
{
|
||||
"range": {
|
||||
"start": { "offset": 0, "line": 1, "column": 1 },
|
||||
"end": { "offset": 1, "line": 1, "column": 2 }
|
||||
},
|
||||
"environment": [],
|
||||
"matched": "a"
|
||||
}
|
||||
]|}]
|
||||
|
||||
let%expect_test "dont_get_stuck" =
|
||||
let template = "a" in
|
||||
let source = "aaa" in
|
||||
let matches = all template source in
|
||||
print_matches matches;
|
||||
[%expect_exact {|[
|
||||
{
|
||||
"range": {
|
||||
"start": { "offset": 0, "line": 1, "column": 1 },
|
||||
"end": { "offset": 1, "line": 1, "column": 2 }
|
||||
},
|
||||
"environment": [],
|
||||
"matched": "a"
|
||||
},
|
||||
{
|
||||
"range": {
|
||||
"start": { "offset": 1, "line": 1, "column": 2 },
|
||||
"end": { "offset": 2, "line": 1, "column": 3 }
|
||||
},
|
||||
"environment": [],
|
||||
"matched": "a"
|
||||
},
|
||||
{
|
||||
"range": {
|
||||
"start": { "offset": 2, "line": 1, "column": 3 },
|
||||
"end": { "offset": 3, "line": 1, "column": 4 }
|
||||
},
|
||||
"environment": [],
|
||||
"matched": "a"
|
||||
}
|
||||
]|}]
|
||||
|
||||
let%expect_test "rewrite_awesome_1" =
|
||||
let template = "replace this :[1] end" in
|
||||
let source = "xreplace this () end" in
|
||||
let rewrite_template = "X" in
|
||||
|
||||
all template source
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact "xX"]
|
||||
|
||||
let%expect_test "rewrite_whole_template_matches" =
|
||||
let template = {|rewrite :[1] <- this string|} in
|
||||
let source = {|rewrite hello world <- this string|} in
|
||||
let rewrite_template = "?" in
|
||||
|
||||
all template source
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact "?"]
|
||||
|
||||
let%expect_test "single_token" =
|
||||
let template = {|:[[1]] this|} in
|
||||
let source = {|the problem is this|} in
|
||||
let matches = all template source in
|
||||
print_matches matches;
|
||||
[%expect_exact {|[
|
||||
{
|
||||
"range": {
|
||||
"start": { "offset": 12, "line": 1, "column": 13 },
|
||||
"end": { "offset": 19, "line": 1, "column": 20 }
|
||||
},
|
||||
"environment": [
|
||||
[
|
||||
"1",
|
||||
{
|
||||
"value": "is",
|
||||
"range": {
|
||||
"start": { "offset": 12, "line": 1, "column": 13 },
|
||||
"end": { "offset": 14, "line": 1, "column": 15 }
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"matched": "is this"
|
||||
}
|
||||
]|}]
|
||||
|
||||
|
||||
let%expect_test "single_token_with_preceding_whitespace" =
|
||||
let template = {| :[[1]] this|} in
|
||||
let source = {|the problem is this|} in
|
||||
let matches = all template source in
|
||||
print_matches matches;
|
||||
[%expect_exact {|[
|
||||
{
|
||||
"range": {
|
||||
"start": { "offset": 11, "line": 1, "column": 12 },
|
||||
"end": { "offset": 19, "line": 1, "column": 20 }
|
||||
},
|
||||
"environment": [
|
||||
[
|
||||
"1",
|
||||
{
|
||||
"value": "is",
|
||||
"range": {
|
||||
"start": { "offset": 12, "line": 1, "column": 13 },
|
||||
"end": { "offset": 14, "line": 1, "column": 15 }
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"matched": " is this"
|
||||
}
|
||||
]|}]
|
||||
|
||||
let%expect_test "single_token_rewrite" =
|
||||
let template = {| :[[1]] this|} in
|
||||
let source = {|the problem is this|} in
|
||||
let rewrite_template = ":[1]" in
|
||||
all template source
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact "the problemis"]
|
||||
|
||||
let%expect_test "single_token_match_inside_paren_no_succeeding_whitespace" =
|
||||
let template = {|:[[1]](:[[2]])|} in
|
||||
let source = {|foo(bar)|} in
|
||||
let rewrite_template = ":[1] : :[2]" in
|
||||
all template source
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact "foo : bar"]
|
||||
|
||||
|
||||
let%expect_test "shift_or_at_least_dont_get_stuck" =
|
||||
let template = ":[1]" in
|
||||
let source = "a" in
|
||||
let matches = all template source in
|
||||
print_matches matches;
|
||||
[%expect_exact {|[
|
||||
{
|
||||
"range": {
|
||||
"start": { "offset": 0, "line": 1, "column": 1 },
|
||||
"end": { "offset": 1, "line": 1, "column": 2 }
|
||||
},
|
||||
"environment": [
|
||||
[
|
||||
"1",
|
||||
{
|
||||
"value": "a",
|
||||
"range": {
|
||||
"start": { "offset": 0, "line": 1, "column": 1 },
|
||||
"end": { "offset": 1, "line": 1, "column": 2 }
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"matched": "a"
|
||||
}
|
||||
]|}]
|
||||
|
||||
let%expect_test "shift_or_at_least_dont_get_stuck" =
|
||||
let template = ":[1]" in
|
||||
let source = "aa" in
|
||||
let matches = all template source in
|
||||
print_matches matches;
|
||||
[%expect_exact {|[
|
||||
{
|
||||
"range": {
|
||||
"start": { "offset": 0, "line": 1, "column": 1 },
|
||||
"end": { "offset": 2, "line": 1, "column": 3 }
|
||||
},
|
||||
"environment": [
|
||||
[
|
||||
"1",
|
||||
{
|
||||
"value": "aa",
|
||||
"range": {
|
||||
"start": { "offset": 0, "line": 1, "column": 1 },
|
||||
"end": { "offset": 2, "line": 1, "column": 3 }
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"matched": "aa"
|
||||
}
|
||||
]|}]
|
||||
|
||||
let%expect_test "nested_rewrite1" =
|
||||
let source =
|
||||
{|
|
||||
x x y strcpy(strcpy(dst1, src1), src2); blah blah XXX
|
||||
|}
|
||||
in
|
||||
|
||||
let template =
|
||||
{|
|
||||
strcpy(:[1], :[2])
|
||||
|}
|
||||
in
|
||||
|
||||
let matches = all template source in
|
||||
print_matches matches;
|
||||
[%expect_exact "[]"]
|
||||
|
||||
(* FIXME(RVT) nested rewrites *)
|
||||
let%expect_test "nested_rewrite2" =
|
||||
let template =
|
||||
{|
|
||||
if :[var_check] != nil {
|
||||
for :[defines] := range :[var_use] {:[inner_body]}
|
||||
}
|
||||
|}
|
||||
in
|
||||
let source =
|
||||
{|
|
||||
if fields.List != nil {
|
||||
for _, field := range fields.List {
|
||||
if field.Names != nil {
|
||||
for _, fieldName := range field.Names {
|
||||
stuff with fields and things
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|}
|
||||
in
|
||||
let rewrite_template = "for :[defines] := range :[var_use] {:[inner_body]}" in
|
||||
all template source
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact {|for _, field := range fields.List {
|
||||
if field.Names != nil {
|
||||
for _, fieldName := range field.Names {
|
||||
stuff with fields and things
|
||||
}
|
||||
}
|
||||
}|}]
|
||||
|
||||
let%expect_test "match_:[[1]]" =
|
||||
let template =
|
||||
{|
|
||||
:[[1]].next()
|
||||
|}
|
||||
in
|
||||
let source =
|
||||
{|
|
||||
col_names = reader.next()
|
||||
}
|
||||
|}
|
||||
in
|
||||
let rewrite_template = "next(:[1])" in
|
||||
all template source
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact {|
|
||||
col_names =next(reader)}
|
||||
|}]
|
719
test/test_match_rule.ml
Normal file
719
test/test_match_rule.ml
Normal file
@ -0,0 +1,719 @@
|
||||
open Core
|
||||
|
||||
open Language
|
||||
open Matchers
|
||||
open Match
|
||||
|
||||
let rule_parses rule =
|
||||
match Rule.create rule with
|
||||
| Ok _ -> "true"
|
||||
| Error _ -> "false"
|
||||
|
||||
let print_matches matches =
|
||||
List.map matches ~f:Match.to_yojson
|
||||
|> (fun matches -> `List matches)
|
||||
|> Yojson.Safe.pretty_to_string
|
||||
|> print_string
|
||||
|
||||
let%expect_test "parse_rule" =
|
||||
let rule = {| where :[1] == :[2], :[3] == "y" |} in
|
||||
rule_parses rule |> print_string;
|
||||
[%expect_exact {|true|}];
|
||||
|
||||
let rule = {| where :[1] == :[2], :[3] != "x" |} in
|
||||
rule_parses rule |> print_string;
|
||||
[%expect_exact {|true|}];
|
||||
|
||||
let rule = {| where :[1] != :[3] |} in
|
||||
rule_parses rule |> print_string;
|
||||
[%expect_exact {|true|}]
|
||||
|
||||
let%expect_test "parse_basic" =
|
||||
Rule.create {|where "a" == "a"|}
|
||||
|> Or_error.ok_exn
|
||||
|> fun rule -> print_s [%message (rule : Ast.expression list)];
|
||||
[%expect_exact {|(rule ((Equal (String a) (String a))))
|
||||
|}]
|
||||
|
||||
let%expect_test "parse_match_one_case" =
|
||||
Rule.create {|where match "match_me" { | "case_one" -> true }|}
|
||||
|> Or_error.ok_exn
|
||||
|> fun rule -> print_s [%message (rule : Ast.expression list)];
|
||||
[%expect_exact "(rule ((Match (String match_me) (((String case_one) (True))))))
|
||||
"]
|
||||
|
||||
let%expect_test "parse_match_multi_case" =
|
||||
Rule.create
|
||||
{| where
|
||||
match "match_me" {
|
||||
| "case_one" -> true
|
||||
| "case_two" -> false
|
||||
}
|
||||
|}
|
||||
|> Or_error.ok_exn
|
||||
|> fun rule -> print_s [%message (rule : Ast.expression list)];
|
||||
[%expect_exact "(rule
|
||||
((Match (String match_me)
|
||||
(((String case_one) (True)) ((String case_two) (False))))))
|
||||
"]
|
||||
|
||||
let sat ?(env = Environment.create ()) rule =
|
||||
let rule = Rule.create rule |> Or_error.ok_exn in
|
||||
Format.sprintf "%b" (Rule.(sat @@ apply rule env))
|
||||
|
||||
let make_env bindings =
|
||||
List.fold bindings
|
||||
~init:(Environment.create ())
|
||||
~f:(fun env (var, value) -> Environment.add env var value)
|
||||
|
||||
let%expect_test "rule_sat" =
|
||||
let rule = {| where "x" != "y" |} in
|
||||
sat rule |> print_string;
|
||||
[%expect_exact {|true|}];
|
||||
|
||||
let rule = {| where "x" != "x" |} in
|
||||
sat rule |> print_string;
|
||||
[%expect_exact {|false|}];
|
||||
|
||||
let rule = {| where "x" == "x" |} in
|
||||
sat rule |> print_string;
|
||||
[%expect_exact {|true|}];
|
||||
|
||||
let rule = {| where "x" == "y" |} in
|
||||
sat rule |> print_string;
|
||||
[%expect_exact {|false|}];
|
||||
|
||||
let rule = {| where :[x] == "y" |} in
|
||||
sat rule |> print_string;
|
||||
[%expect_exact {|false|}];
|
||||
|
||||
let rule = {| where :[x] == :[x] |} in
|
||||
sat rule |> print_string;
|
||||
[%expect_exact {|false|}]
|
||||
|
||||
let%expect_test "rule_sat_with_env" =
|
||||
let env = make_env ["1", "x"; "2", "y"; "3", "x"] in
|
||||
|
||||
let rule = {| where :[1] == :[3], :[1] != :[2] |} in
|
||||
sat ~env rule |> print_string;
|
||||
[%expect_exact {|true|}];
|
||||
|
||||
let rule = {| where :[1] == :[3], :[1] != "y" |} in
|
||||
sat ~env rule |> print_string;
|
||||
[%expect_exact {|true|}];
|
||||
|
||||
let rule = {| where :[1] == :[3], :[1] == "x" |} in
|
||||
sat ~env rule |> print_string;
|
||||
[%expect_exact {|true|}];
|
||||
|
||||
let rule = {| where :[1] == :[2], :[1] != :[2] |} in
|
||||
sat ~env rule |> print_string;
|
||||
[%expect_exact {|false|}]
|
||||
|
||||
let configuration = Configuration.create ~match_kind:Fuzzy ()
|
||||
|
||||
let format s =
|
||||
let s = s |> String.chop_prefix_exn ~prefix:"\n" in
|
||||
let leading_indentation =
|
||||
Option.value_exn (String.lfindi s ~f:(fun _ c -> c <> ' ')) in
|
||||
s
|
||||
|> String.split ~on:'\n'
|
||||
|> List.map ~f:(Fn.flip String.drop_prefix leading_indentation)
|
||||
|> String.concat ~sep:"\n"
|
||||
|> String.chop_suffix_exn ~suffix:"\n"
|
||||
|
||||
|
||||
let%expect_test "where_true" =
|
||||
let template =
|
||||
{|
|
||||
(:[1]) => {}
|
||||
|}
|
||||
|> format
|
||||
in
|
||||
|
||||
let source =
|
||||
{|
|
||||
(b,c) => {}
|
||||
|}
|
||||
|> format
|
||||
in
|
||||
|
||||
let rule =
|
||||
{| where true
|
||||
|}
|
||||
|> Rule.create
|
||||
|> Or_error.ok_exn
|
||||
in
|
||||
|
||||
Generic.all ~configuration ~template ~source
|
||||
|> List.filter ~f:(fun { environment; _ } -> Rule.(sat @@ apply rule environment))
|
||||
|> print_matches;
|
||||
[%expect {|
|
||||
[
|
||||
{
|
||||
"range": {
|
||||
"start": { "offset": 0, "line": 1, "column": 1 },
|
||||
"end": { "offset": 11, "line": 1, "column": 12 }
|
||||
},
|
||||
"environment": [
|
||||
[
|
||||
"1",
|
||||
{
|
||||
"value": "b,c",
|
||||
"range": {
|
||||
"start": { "offset": 1, "line": 1, "column": 2 },
|
||||
"end": { "offset": 4, "line": 1, "column": 5 }
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"matched": "(b,c) => {}"
|
||||
}
|
||||
] |}]
|
||||
|
||||
let%expect_test "match_sat" =
|
||||
let template =
|
||||
{|
|
||||
(:[1]) => {}
|
||||
|}
|
||||
|> format
|
||||
in
|
||||
|
||||
let source =
|
||||
{|
|
||||
(b,c) => {}
|
||||
|}
|
||||
|> format
|
||||
in
|
||||
|
||||
let rule =
|
||||
{| where
|
||||
match :[1] {
|
||||
| ":[_],:[_]" -> false
|
||||
}
|
||||
|}
|
||||
|> Rule.create
|
||||
|> Or_error.ok_exn
|
||||
in
|
||||
|
||||
Generic.all ~configuration ~template ~source
|
||||
|> List.filter ~f:(fun { environment; _ } -> Rule.(sat @@ apply rule environment))
|
||||
|> print_matches;
|
||||
[%expect {|
|
||||
[] |}];
|
||||
|
||||
let rule =
|
||||
{| where
|
||||
match :[1] {
|
||||
| ":[_],:[_]" -> true
|
||||
}
|
||||
|}
|
||||
|> Rule.create
|
||||
|> Or_error.ok_exn
|
||||
in
|
||||
|
||||
Generic.all ~configuration ~template ~source
|
||||
|> List.filter ~f:(fun { environment; _ } -> Rule.(sat @@ apply rule environment))
|
||||
|> print_matches;
|
||||
[%expect {|
|
||||
[
|
||||
{
|
||||
"range": {
|
||||
"start": { "offset": 0, "line": 1, "column": 1 },
|
||||
"end": { "offset": 11, "line": 1, "column": 12 }
|
||||
},
|
||||
"environment": [
|
||||
[
|
||||
"1",
|
||||
{
|
||||
"value": "b,c",
|
||||
"range": {
|
||||
"start": { "offset": 1, "line": 1, "column": 2 },
|
||||
"end": { "offset": 4, "line": 1, "column": 5 }
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"matched": "(b,c) => {}"
|
||||
}
|
||||
] |}];
|
||||
|
||||
let source =
|
||||
{|
|
||||
(a) => {}
|
||||
(b,c) => {}
|
||||
|}
|
||||
|> format
|
||||
in
|
||||
|
||||
let rule =
|
||||
{| where
|
||||
match :[1] {
|
||||
| ":[_],:[_]" -> false
|
||||
| ":[_]" -> true
|
||||
}
|
||||
|}
|
||||
|> Rule.create
|
||||
|> Or_error.ok_exn
|
||||
in
|
||||
|
||||
Generic.all ~configuration ~template ~source
|
||||
|> List.filter ~f:(fun { environment; _ } -> Rule.(sat @@ apply rule environment))
|
||||
|> print_matches;
|
||||
[%expect {|
|
||||
[
|
||||
{
|
||||
"range": {
|
||||
"start": { "offset": 0, "line": 1, "column": 1 },
|
||||
"end": { "offset": 9, "line": 1, "column": 10 }
|
||||
},
|
||||
"environment": [
|
||||
[
|
||||
"1",
|
||||
{
|
||||
"value": "a",
|
||||
"range": {
|
||||
"start": { "offset": 1, "line": 1, "column": 2 },
|
||||
"end": { "offset": 2, "line": 1, "column": 3 }
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"matched": "(a) => {}"
|
||||
}
|
||||
] |}];
|
||||
|
||||
let rule =
|
||||
{|
|
||||
where
|
||||
match :[1] {
|
||||
| ":[_],:[_]" -> false
|
||||
| ":[_]" -> :[1] == "a"
|
||||
}
|
||||
|}
|
||||
|> Rule.create
|
||||
|> Or_error.ok_exn
|
||||
in
|
||||
|
||||
Generic.all ~configuration ~template ~source
|
||||
|> List.filter ~f:(fun { environment; _ } -> Rule.(sat @@ apply rule environment))
|
||||
|> print_matches;
|
||||
[%expect {|
|
||||
[
|
||||
{
|
||||
"range": {
|
||||
"start": { "offset": 0, "line": 1, "column": 1 },
|
||||
"end": { "offset": 9, "line": 1, "column": 10 }
|
||||
},
|
||||
"environment": [
|
||||
[
|
||||
"1",
|
||||
{
|
||||
"value": "a",
|
||||
"range": {
|
||||
"start": { "offset": 1, "line": 1, "column": 2 },
|
||||
"end": { "offset": 2, "line": 1, "column": 3 }
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"matched": "(a) => {}"
|
||||
}
|
||||
] |}];
|
||||
|
||||
let rule =
|
||||
{| where
|
||||
match :[1] {
|
||||
| ":[_],:[_]" -> false
|
||||
| ":[_]" -> :[1] == "b"
|
||||
}
|
||||
|}
|
||||
|> Rule.create
|
||||
|> Or_error.ok_exn
|
||||
in
|
||||
|
||||
Generic.all ~configuration ~template ~source
|
||||
|> List.filter ~f:(fun { environment; _ } -> Rule.(sat @@ apply rule environment))
|
||||
|> print_matches;
|
||||
[%expect {|
|
||||
[] |}]
|
||||
|
||||
|
||||
let%expect_test "match_s_suffix" =
|
||||
let template = ":[1]s" in
|
||||
|
||||
let source = "names" in
|
||||
|
||||
let rule =
|
||||
{| where true
|
||||
|}
|
||||
|> Rule.create
|
||||
|> Or_error.ok_exn
|
||||
in
|
||||
|
||||
Generic.all ~configuration ~template ~source
|
||||
|> List.filter ~f:(fun { environment; _ } -> Rule.(sat @@ apply rule environment))
|
||||
|> print_matches;
|
||||
[%expect {|
|
||||
[
|
||||
{
|
||||
"range": {
|
||||
"start": { "offset": 0, "line": 1, "column": 1 },
|
||||
"end": { "offset": 5, "line": 1, "column": 6 }
|
||||
},
|
||||
"environment": [
|
||||
[
|
||||
"1",
|
||||
{
|
||||
"value": "name",
|
||||
"range": {
|
||||
"start": { "offset": 0, "line": 1, "column": 1 },
|
||||
"end": { "offset": 4, "line": 1, "column": 5 }
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"matched": "names"
|
||||
}
|
||||
] |}]
|
||||
|
||||
let%expect_test "match_s_suffix" =
|
||||
let template = ":[1]" in
|
||||
|
||||
let source = "names" in
|
||||
|
||||
let rule =
|
||||
{| where true
|
||||
|}
|
||||
|> Rule.create
|
||||
|> Or_error.ok_exn
|
||||
in
|
||||
|
||||
Generic.all ~configuration ~template ~source
|
||||
|> List.filter ~f:(fun { environment; _ } -> Rule.(sat @@ apply rule environment))
|
||||
|> print_matches;
|
||||
[%expect {|
|
||||
[
|
||||
{
|
||||
"range": {
|
||||
"start": { "offset": 0, "line": 1, "column": 1 },
|
||||
"end": { "offset": 5, "line": 1, "column": 6 }
|
||||
},
|
||||
"environment": [
|
||||
[
|
||||
"1",
|
||||
{
|
||||
"value": "names",
|
||||
"range": {
|
||||
"start": { "offset": 0, "line": 1, "column": 1 },
|
||||
"end": { "offset": 5, "line": 1, "column": 6 }
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"matched": "names"
|
||||
}
|
||||
] |}]
|
||||
|
||||
let%expect_test "configuration_choice_based_on_case" =
|
||||
let template = ":[1]" in
|
||||
|
||||
let source = "names" in
|
||||
|
||||
let rule =
|
||||
{| where match :[1] {
|
||||
| "ame" -> true
|
||||
}
|
||||
|}
|
||||
|> Rule.create
|
||||
|> Or_error.ok_exn
|
||||
in
|
||||
|
||||
Generic.all ~configuration ~template ~source
|
||||
|> List.filter ~f:(fun { environment; _ } -> Rule.(sat @@ apply rule environment))
|
||||
|> print_matches;
|
||||
[%expect {|
|
||||
[] |}];
|
||||
|
||||
let template = ":[1]" in
|
||||
|
||||
let source = "names" in
|
||||
|
||||
let rule =
|
||||
{| where match :[1] {
|
||||
| "names" -> true
|
||||
}
|
||||
|}
|
||||
|> Rule.create
|
||||
|> Or_error.ok_exn
|
||||
in
|
||||
|
||||
Generic.all ~configuration ~template ~source
|
||||
|> List.filter ~f:(fun { environment; _ } -> Rule.(sat @@ apply rule environment))
|
||||
|> print_matches;
|
||||
[%expect {|
|
||||
[
|
||||
{
|
||||
"range": {
|
||||
"start": { "offset": 0, "line": 1, "column": 1 },
|
||||
"end": { "offset": 5, "line": 1, "column": 6 }
|
||||
},
|
||||
"environment": [
|
||||
[
|
||||
"1",
|
||||
{
|
||||
"value": "names",
|
||||
"range": {
|
||||
"start": { "offset": 0, "line": 1, "column": 1 },
|
||||
"end": { "offset": 5, "line": 1, "column": 6 }
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"matched": "names"
|
||||
}
|
||||
] |}];
|
||||
|
||||
let template = ":[1]" in
|
||||
|
||||
let source = "namesXXXXX" in
|
||||
|
||||
let rule =
|
||||
{| where match :[1] {
|
||||
| "names" -> true
|
||||
}
|
||||
|}
|
||||
|> Rule.create
|
||||
|> Or_error.ok_exn
|
||||
in
|
||||
|
||||
Generic.all ~configuration ~template ~source
|
||||
|> List.filter ~f:(fun { environment; _ } -> Rule.(sat @@ apply rule environment))
|
||||
|> print_matches;
|
||||
[%expect {|
|
||||
[] |}]
|
||||
|
||||
|
||||
|
||||
|
||||
let%expect_test "match_using_environment_merge" =
|
||||
let template = "{:[1]}" in
|
||||
|
||||
let source = "{{ a : a } { a : a }}" in
|
||||
|
||||
let rule =
|
||||
{| where match :[1] { | "{ :[x] : :[y] }" -> :[x] == :[y] }
|
||||
|}
|
||||
|> Rule.create
|
||||
|> Or_error.ok_exn
|
||||
in
|
||||
|
||||
Generic.all ~configuration ~template ~source
|
||||
|> List.filter ~f:(fun { environment; _ } -> Rule.(sat @@ apply rule environment))
|
||||
|> print_matches;
|
||||
[%expect {|
|
||||
[
|
||||
{
|
||||
"range": {
|
||||
"start": { "offset": 0, "line": 1, "column": 1 },
|
||||
"end": { "offset": 21, "line": 1, "column": 22 }
|
||||
},
|
||||
"environment": [
|
||||
[
|
||||
"1",
|
||||
{
|
||||
"value": "{ a : a } { a : a }",
|
||||
"range": {
|
||||
"start": { "offset": 1, "line": 1, "column": 2 },
|
||||
"end": { "offset": 20, "line": 1, "column": 21 }
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"matched": "{{ a : a } { a : a }}"
|
||||
}
|
||||
] |}];
|
||||
|
||||
let template = "{:[1]}" in
|
||||
|
||||
let source = "{{ a : a } { a : b }}" in
|
||||
|
||||
let rule =
|
||||
{| where match :[1] { | "{ :[x] : :[y] }" -> :[x] == :[y] }
|
||||
|}
|
||||
|> Rule.create
|
||||
|> Or_error.ok_exn
|
||||
in
|
||||
|
||||
Generic.all ~configuration ~template ~source
|
||||
|> List.filter ~f:(fun { environment; _ } -> Rule.(sat @@ apply rule environment))
|
||||
|> print_matches;
|
||||
[%expect {|
|
||||
[] |}]
|
||||
|
||||
|
||||
let%expect_test "nested_matches" =
|
||||
let template = ":[1]" in
|
||||
|
||||
let source = "{ { foo : { bar : { baz : qux } } } }" in
|
||||
|
||||
let rule =
|
||||
{| where match :[1] {
|
||||
| "{ :[foo] : :[tail1] }" -> match :[tail1] {
|
||||
| "{ :[bar] : :[tail2] }" -> match :[tail2] {
|
||||
| "{ baz : :[qux] }" -> :[qux] == "qux", :[bar] == "bar"
|
||||
}
|
||||
}
|
||||
}
|
||||
|}
|
||||
|> Rule.create
|
||||
|> Or_error.ok_exn
|
||||
in
|
||||
|
||||
Generic.all ~configuration ~template ~source
|
||||
|> List.filter ~f:(fun { environment; _ } -> Rule.(sat @@ apply rule environment))
|
||||
|> print_matches;
|
||||
[%expect {|
|
||||
[
|
||||
{
|
||||
"range": {
|
||||
"start": { "offset": 0, "line": 1, "column": 1 },
|
||||
"end": { "offset": 37, "line": 1, "column": 38 }
|
||||
},
|
||||
"environment": [
|
||||
[
|
||||
"1",
|
||||
{
|
||||
"value": "{ { foo : { bar : { baz : qux } } } }",
|
||||
"range": {
|
||||
"start": { "offset": 0, "line": 1, "column": 1 },
|
||||
"end": { "offset": 37, "line": 1, "column": 38 }
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"matched": "{ { foo : { bar : { baz : qux } } } }"
|
||||
}
|
||||
] |}];
|
||||
|
||||
let template = ":[1]" in
|
||||
|
||||
let source = "{ { foo : { bar : { baz : qux } } } }" in
|
||||
|
||||
let rule =
|
||||
{| where match :[1] {
|
||||
| "{ :[foo] : :[tail1] }" -> match :[tail1] {
|
||||
| "{ :[bar] : :[tail2] }" -> match :[tail2] {
|
||||
| "{ baz : :[qux] }" -> :[qux] == "fail"
|
||||
}
|
||||
}
|
||||
}
|
||||
|}
|
||||
|> Rule.create
|
||||
|> Or_error.ok_exn
|
||||
in
|
||||
|
||||
Generic.all ~configuration ~template ~source
|
||||
|> List.filter ~f:(fun { environment; _ } -> Rule.(sat @@ apply rule environment))
|
||||
|> print_matches;
|
||||
[%expect {|
|
||||
[] |}]
|
||||
|
||||
|
||||
let%expect_test "match_on_template" =
|
||||
let template = ":[1]" in
|
||||
|
||||
let source = "oodles" in
|
||||
|
||||
let rule =
|
||||
{| where match "p:[1]" {
|
||||
| "poodles" -> true
|
||||
}
|
||||
|}
|
||||
|> Rule.create
|
||||
|> Or_error.ok_exn
|
||||
in
|
||||
|
||||
Generic.all ~configuration ~template ~source
|
||||
|> List.filter ~f:(fun { environment; _ } -> Rule.(sat @@ apply rule environment))
|
||||
|> print_matches;
|
||||
[%expect {|
|
||||
[
|
||||
{
|
||||
"range": {
|
||||
"start": { "offset": 0, "line": 1, "column": 1 },
|
||||
"end": { "offset": 6, "line": 1, "column": 7 }
|
||||
},
|
||||
"environment": [
|
||||
[
|
||||
"1",
|
||||
{
|
||||
"value": "oodles",
|
||||
"range": {
|
||||
"start": { "offset": 0, "line": 1, "column": 1 },
|
||||
"end": { "offset": 6, "line": 1, "column": 7 }
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"matched": "oodles"
|
||||
}
|
||||
] |}];
|
||||
|
||||
let template = ":[1]" in
|
||||
|
||||
let source = "poodle" in
|
||||
|
||||
let rule =
|
||||
{| where match ":[1]s" {
|
||||
| "poodles" -> true
|
||||
}
|
||||
|}
|
||||
|> Rule.create
|
||||
|> Or_error.ok_exn
|
||||
in
|
||||
|
||||
Generic.all ~configuration ~template ~source
|
||||
|> List.filter ~f:(fun { environment; _ } -> Rule.(sat @@ apply rule environment))
|
||||
|> print_matches;
|
||||
[%expect {|
|
||||
[
|
||||
{
|
||||
"range": {
|
||||
"start": { "offset": 0, "line": 1, "column": 1 },
|
||||
"end": { "offset": 6, "line": 1, "column": 7 }
|
||||
},
|
||||
"environment": [
|
||||
[
|
||||
"1",
|
||||
{
|
||||
"value": "poodle",
|
||||
"range": {
|
||||
"start": { "offset": 0, "line": 1, "column": 1 },
|
||||
"end": { "offset": 6, "line": 1, "column": 7 }
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"matched": "poodle"
|
||||
}
|
||||
] |}];
|
||||
|
||||
let template = ":[1]" in
|
||||
|
||||
let source = "poodle" in
|
||||
|
||||
let rule =
|
||||
{| where match ":[1]," {
|
||||
| "poodle" -> true
|
||||
}
|
||||
|}
|
||||
|> Rule.create
|
||||
|> Or_error.ok_exn
|
||||
in
|
||||
|
||||
Generic.all ~configuration ~template ~source
|
||||
|> List.filter ~f:(fun { environment; _ } -> Rule.(sat @@ apply rule environment))
|
||||
|> print_matches;
|
||||
[%expect {|
|
||||
[] |}];
|
223
test/test_rewrite_parts.ml
Normal file
223
test/test_rewrite_parts.ml
Normal file
@ -0,0 +1,223 @@
|
||||
open Core
|
||||
|
||||
open Match
|
||||
open Matchers
|
||||
open Rewriter
|
||||
|
||||
let%expect_test "get_offsets_for_holes" =
|
||||
let rewrite_template = {|1234:[1]1234:[2]|} in
|
||||
let result = Rewrite_template.get_offsets_for_holes rewrite_template ["1"; "2"] in
|
||||
print_s [%message (result : (string * int) list)];
|
||||
[%expect_exact {|(result ((2 8) (1 4)))
|
||||
|}]
|
||||
|
||||
let%expect_test "get_offsets_for_holes_after_substitution_1" =
|
||||
let rewrite_template = {|1234:[1]1234:[2]|} in
|
||||
let offsets = Rewrite_template.get_offsets_for_holes rewrite_template ["1"; "2"] in
|
||||
let environment =
|
||||
Environment.create ()
|
||||
|> (fun environment -> Environment.add environment "1" "333")
|
||||
|> (fun environment -> Environment.add environment "2" "22")
|
||||
in
|
||||
let result = Rewrite_template.get_offsets_after_substitution offsets environment in
|
||||
print_s [%message (result : (string * int) list)];
|
||||
[%expect_exact {|(result ((2 11) (1 4)))
|
||||
|}]
|
||||
|
||||
let%expect_test "get_offsets_for_holes_after_substitution_1" =
|
||||
let rewrite_template = {|1234:[1]1234:[3]11:[2]|} in
|
||||
let offsets = Rewrite_template.get_offsets_for_holes rewrite_template ["1"; "3"; "2"] in
|
||||
let environment =
|
||||
Environment.create ()
|
||||
|> (fun environment -> Environment.add environment "1" "333")
|
||||
|> (fun environment -> Environment.add environment "3" "333")
|
||||
|> (fun environment -> Environment.add environment "2" "22")
|
||||
in
|
||||
let result = Rewrite_template.get_offsets_after_substitution offsets environment in
|
||||
print_s [%message (result : (string * int) list)];
|
||||
[%expect_exact {|(result ((2 16) (3 11) (1 4)))
|
||||
|}]
|
||||
|
||||
|
||||
let configuration = Configuration.create ~match_kind:Fuzzy ()
|
||||
|
||||
let all ?(configuration = configuration) template source =
|
||||
C.all ~configuration ~template ~source
|
||||
|
||||
let%expect_test "comments_in_string_literals_should_not_be_treated_as_comments_by_fuzzy" =
|
||||
let source = {|123433312343331122|} in
|
||||
let match_template = {|1234:[1]1234:[3]11:[2]|} in
|
||||
let rewrite_template = {|1234:[1]1234:[3]11:[2]|} in
|
||||
all match_template source
|
||||
|> Rewrite.all ~source ~rewrite_template
|
||||
|> (function
|
||||
| Some rewrite_result -> print_string (Yojson.Safe.pretty_to_string (Rewrite.result_to_yojson rewrite_result))
|
||||
| None -> print_string "BROKEN EXPECT");
|
||||
[%expect_exact {|{
|
||||
"rewritten_source": "123433312343331122",
|
||||
"contextual_substitutions": [
|
||||
{
|
||||
"range": {
|
||||
"start": { "offset": 0, "line": -1, "column": -1 },
|
||||
"end": { "offset": 18, "line": -1, "column": -1 }
|
||||
},
|
||||
"replacement_content": "123433312343331122",
|
||||
"environment": [
|
||||
[
|
||||
"1",
|
||||
{
|
||||
"value": "333",
|
||||
"range": {
|
||||
"start": { "offset": 4, "line": -1, "column": -1 },
|
||||
"end": { "offset": 7, "line": -1, "column": -1 }
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"2",
|
||||
{
|
||||
"value": "22",
|
||||
"range": {
|
||||
"start": { "offset": 16, "line": -1, "column": -1 },
|
||||
"end": { "offset": 18, "line": -1, "column": -1 }
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"3",
|
||||
{
|
||||
"value": "333",
|
||||
"range": {
|
||||
"start": { "offset": 11, "line": -1, "column": -1 },
|
||||
"end": { "offset": 14, "line": -1, "column": -1 }
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
}|}]
|
||||
|
||||
let%expect_test "comments_in_string_literals_should_not_be_treated_as_comments_by_fuzzy" =
|
||||
let source = {|123433312343331122;123433312343331122;|} in
|
||||
let match_template = {|1234:[1]1234:[3]11:[2];|} in
|
||||
let rewrite_template = {|1234:[1]1234:[3]11:[2];|} in
|
||||
all match_template source
|
||||
|> Rewrite.all ~source ~rewrite_template
|
||||
|> (function
|
||||
| Some rewrite_result -> print_string (Yojson.Safe.pretty_to_string (Rewrite.result_to_yojson rewrite_result))
|
||||
| None -> print_string "BROKEN EXPECT");
|
||||
[%expect_exact {|{
|
||||
"rewritten_source": "123433312343331122;123433312343331122;",
|
||||
"contextual_substitutions": [
|
||||
{
|
||||
"range": {
|
||||
"start": { "offset": 19, "line": -1, "column": -1 },
|
||||
"end": { "offset": 38, "line": -1, "column": -1 }
|
||||
},
|
||||
"replacement_content": "123433312343331122;",
|
||||
"environment": [
|
||||
[
|
||||
"1",
|
||||
{
|
||||
"value": "333",
|
||||
"range": {
|
||||
"start": { "offset": 4, "line": -1, "column": -1 },
|
||||
"end": { "offset": 7, "line": -1, "column": -1 }
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"2",
|
||||
{
|
||||
"value": "22",
|
||||
"range": {
|
||||
"start": { "offset": 16, "line": -1, "column": -1 },
|
||||
"end": { "offset": 18, "line": -1, "column": -1 }
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"3",
|
||||
{
|
||||
"value": "333",
|
||||
"range": {
|
||||
"start": { "offset": 11, "line": -1, "column": -1 },
|
||||
"end": { "offset": 14, "line": -1, "column": -1 }
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"range": {
|
||||
"start": { "offset": 0, "line": -1, "column": -1 },
|
||||
"end": { "offset": 19, "line": -1, "column": -1 }
|
||||
},
|
||||
"replacement_content": "123433312343331122;",
|
||||
"environment": [
|
||||
[
|
||||
"1",
|
||||
{
|
||||
"value": "333",
|
||||
"range": {
|
||||
"start": { "offset": 4, "line": -1, "column": -1 },
|
||||
"end": { "offset": 7, "line": -1, "column": -1 }
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"2",
|
||||
{
|
||||
"value": "22",
|
||||
"range": {
|
||||
"start": { "offset": 16, "line": -1, "column": -1 },
|
||||
"end": { "offset": 18, "line": -1, "column": -1 }
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"3",
|
||||
{
|
||||
"value": "333",
|
||||
"range": {
|
||||
"start": { "offset": 11, "line": -1, "column": -1 },
|
||||
"end": { "offset": 14, "line": -1, "column": -1 }
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
}|}]
|
||||
|
||||
let%expect_test "multiple_contextual_substitutions" =
|
||||
let source = {|foo bar foo|} in
|
||||
let match_template = {|foo|} in
|
||||
let rewrite_template = {|xxxx|} in
|
||||
all match_template source
|
||||
|> Rewrite.all ~source ~rewrite_template
|
||||
|> (function
|
||||
| Some rewrite_result -> print_string (Yojson.Safe.pretty_to_string (Rewrite.result_to_yojson rewrite_result))
|
||||
| None -> print_string "BROKEN EXPECT");
|
||||
[%expect_exact {|{
|
||||
"rewritten_source": "xxxx bar xxxx",
|
||||
"contextual_substitutions": [
|
||||
{
|
||||
"range": {
|
||||
"start": { "offset": 9, "line": -1, "column": -1 },
|
||||
"end": { "offset": 13, "line": -1, "column": -1 }
|
||||
},
|
||||
"replacement_content": "xxxx",
|
||||
"environment": []
|
||||
},
|
||||
{
|
||||
"range": {
|
||||
"start": { "offset": 0, "line": -1, "column": -1 },
|
||||
"end": { "offset": 4, "line": -1, "column": -1 }
|
||||
},
|
||||
"replacement_content": "xxxx",
|
||||
"environment": []
|
||||
}
|
||||
]
|
||||
}|}]
|
169
test/test_rewrite_rule.ml
Normal file
169
test/test_rewrite_rule.ml
Normal file
@ -0,0 +1,169 @@
|
||||
open Core
|
||||
|
||||
open Language
|
||||
open Matchers
|
||||
open Match
|
||||
open Rewriter
|
||||
|
||||
let configuration = Configuration.create ~match_kind:Fuzzy ()
|
||||
|
||||
let format s =
|
||||
let s = String.chop_prefix_exn ~prefix:"\n" s in
|
||||
let leading_indentation = Option.value_exn (String.lfindi s ~f:(fun _ c -> c <> ' ')) in
|
||||
s
|
||||
|> String.split ~on:'\n'
|
||||
|> List.map ~f:(Fn.flip String.drop_prefix leading_indentation)
|
||||
|> String.concat ~sep:"\n"
|
||||
|> String.chop_suffix_exn ~suffix:"\n"
|
||||
|
||||
let run_rule source match_template rewrite_template rule =
|
||||
Generic.first ~configuration match_template source
|
||||
|> function
|
||||
| Error _ -> print_string "bad"
|
||||
| Ok result ->
|
||||
match result with
|
||||
| ({ environment; _ } as m) ->
|
||||
let e = Rule.(result_env @@ apply rule environment) in
|
||||
match e with
|
||||
| None -> print_string "bad bad"
|
||||
| Some e ->
|
||||
{ m with environment = e }
|
||||
|> List.return
|
||||
|> Rewrite.all ~source ~rewrite_template
|
||||
|> (fun x -> Option.value_exn x)
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string
|
||||
|
||||
let%expect_test "rewrite_rule" =
|
||||
let source = {|int|} in
|
||||
let match_template = {|:[1]|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
|
||||
let rule =
|
||||
{|
|
||||
where rewrite :[1] {
|
||||
| "int" -> "expect"
|
||||
}
|
||||
|}
|
||||
|> Rule.create
|
||||
|> Or_error.ok_exn
|
||||
in
|
||||
|
||||
run_rule source match_template rewrite_template rule;
|
||||
[%expect_exact {|expect|}]
|
||||
|
||||
let%expect_test "rewrite_rule" =
|
||||
let source = {|string expect|} in
|
||||
let match_template = {|:[1] :[2]|} in
|
||||
let rewrite_template = {|:[2]|} in
|
||||
|
||||
let rule =
|
||||
{|
|
||||
where rewrite :[1] {
|
||||
| "int" -> "5"
|
||||
| "string" -> ":[2]"
|
||||
}
|
||||
|}
|
||||
|> Rule.create
|
||||
|> Or_error.ok_exn
|
||||
in
|
||||
|
||||
run_rule source match_template rewrite_template rule;
|
||||
[%expect_exact {|expect|}]
|
||||
|
||||
let%expect_test "conditional_rewrite_rule" =
|
||||
let source = {|{ { a : { b : { c : d } } } }|} in
|
||||
let match_template = {|:[1]|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
|
||||
let rule =
|
||||
{|
|
||||
where rewrite :[1] {
|
||||
| "{ :[a] : :[rest] }" -> "a" == :[a], "doot"
|
||||
}
|
||||
|}
|
||||
|> Rule.create
|
||||
|> Or_error.ok_exn
|
||||
in
|
||||
|
||||
run_rule source match_template rewrite_template rule;
|
||||
[%expect_exact {|doot|}]
|
||||
|
||||
let%expect_test "rewrite_rule_using_match_result" =
|
||||
let source = {|{ { a : { b : { c : d } } } }|} in
|
||||
let match_template = {|:[1]|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
|
||||
let rule =
|
||||
{|
|
||||
where rewrite :[1] {
|
||||
| "{ :[a] : :[rest] }" -> "a" == :[a], ":[rest]"
|
||||
}
|
||||
|}
|
||||
|> Rule.create
|
||||
|> Or_error.ok_exn
|
||||
in
|
||||
|
||||
run_rule source match_template rewrite_template rule;
|
||||
[%expect_exact {|{ b : { c : d } }|}];
|
||||
|
||||
let source = {|{ { a : { b : { c : d } } } }|} in
|
||||
let match_template = {|:[1]|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
let rule =
|
||||
{|
|
||||
where rewrite :[1] {
|
||||
| "{ :[a] : :[rest] }" -> "b" == :[a], ":[rest]"
|
||||
}
|
||||
|}
|
||||
|> Rule.create
|
||||
|> Or_error.ok_exn
|
||||
in
|
||||
|
||||
run_rule source match_template rewrite_template rule;
|
||||
[%expect_exact {|{ { a : { b : { c : d } } } }|}]
|
||||
|
||||
|
||||
let%expect_test "nested_rewrite_rule" =
|
||||
let source = {|{ { a : { b : { c : d } } } }|} in
|
||||
let match_template = {|:[1]|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
|
||||
let rule =
|
||||
{|
|
||||
where
|
||||
rewrite :[1] {
|
||||
| "{ :[a] : :[rest] }" ->
|
||||
rewrite :[a] {
|
||||
| "a" -> "b"
|
||||
}, "{ :[a] : :[rest] }"
|
||||
}
|
||||
|}
|
||||
|> Rule.create
|
||||
|> Or_error.ok_exn
|
||||
in
|
||||
|
||||
run_rule source match_template rewrite_template rule;
|
||||
[%expect_exact {|{ b : { b : { c : d } } }|}]
|
||||
|
||||
let%expect_test "sequenced_rewrite_rule" =
|
||||
let source = {|{ { a : { b : { c : d } } } }|} in
|
||||
let match_template = {|{ :[a] : :[rest] }|} in
|
||||
let rewrite_template = {|{ :[a] : :[rest] }|} in
|
||||
|
||||
let rule =
|
||||
{|
|
||||
where
|
||||
rewrite :[a] {
|
||||
| "a" -> "qqq"
|
||||
},
|
||||
rewrite :[rest] {
|
||||
| "{ b : { :[other] } }" -> "{ :[other] }"
|
||||
}
|
||||
|}
|
||||
|> Rule.create
|
||||
|> Or_error.ok_exn
|
||||
in
|
||||
|
||||
run_rule source match_template rewrite_template rule;
|
||||
[%expect_exact {|{ { qqq : { c : d } } }|}]
|
87
test/test_statistics.ml
Normal file
87
test/test_statistics.ml
Normal file
@ -0,0 +1,87 @@
|
||||
open Core
|
||||
|
||||
open Language
|
||||
open Matchers
|
||||
open Match
|
||||
|
||||
let configuration = Configuration.create ~match_kind:Fuzzy ()
|
||||
|
||||
let format s =
|
||||
let s = s |> String.chop_prefix_exn ~prefix:"\n" in
|
||||
let leading_indentation =
|
||||
Option.value_exn (String.lfindi s ~f:(fun _ c -> c <> ' ')) in
|
||||
s
|
||||
|> String.split ~on:'\n'
|
||||
|> List.map ~f:(Fn.flip String.drop_prefix leading_indentation)
|
||||
|> String.concat ~sep:"\n"
|
||||
|> String.chop_suffix_exn ~suffix:"\n"
|
||||
|
||||
let %expect_test "statistics" =
|
||||
let template =
|
||||
{|
|
||||
def :[fn_name](:[fn_params])
|
||||
|}
|
||||
|> format
|
||||
in
|
||||
|
||||
let source =
|
||||
{|
|
||||
def foo(bar):
|
||||
pass
|
||||
|
||||
def bar(bazz):
|
||||
pass
|
||||
|}
|
||||
|> format
|
||||
in
|
||||
|
||||
let rule =
|
||||
{| where true
|
||||
|}
|
||||
|> Rule.create
|
||||
|> Or_error.ok_exn
|
||||
in
|
||||
Go.all ~configuration ~template ~source
|
||||
|> List.filter ~f:(fun { environment; _ } ->
|
||||
Rule.(sat @@ apply rule environment))
|
||||
|> fun matches ->
|
||||
let statistics =
|
||||
Statistics.
|
||||
{ number_of_files = 1
|
||||
; lines_of_code = 5
|
||||
; number_of_matches = List.length matches
|
||||
; total_time = 0.0
|
||||
}
|
||||
in
|
||||
statistics
|
||||
|> Statistics.to_yojson
|
||||
|> Yojson.Safe.pretty_to_string
|
||||
|> print_string;
|
||||
[%expect {|
|
||||
{
|
||||
"number_of_files": 1,
|
||||
"lines_of_code": 5,
|
||||
"number_of_matches": 2,
|
||||
"total_time": 0.0
|
||||
} |}];
|
||||
|
||||
let statistics' =
|
||||
Statistics.merge
|
||||
{ number_of_files = 1
|
||||
; lines_of_code = 10
|
||||
; number_of_matches = 1
|
||||
; total_time = 1.5
|
||||
}
|
||||
statistics
|
||||
in
|
||||
statistics'
|
||||
|> Statistics.to_yojson
|
||||
|> Yojson.Safe.pretty_to_string
|
||||
|> print_string;
|
||||
[%expect {|
|
||||
{
|
||||
"number_of_files": 2,
|
||||
"lines_of_code": 15,
|
||||
"number_of_matches": 3,
|
||||
"total_time": 1.5
|
||||
} |}]
|
412
test/test_string_literals.ml
Normal file
412
test/test_string_literals.ml
Normal file
@ -0,0 +1,412 @@
|
||||
open Core
|
||||
|
||||
open Matchers
|
||||
open Rewriter
|
||||
|
||||
let format s =
|
||||
let s = String.chop_prefix_exn ~prefix:"\n" s in
|
||||
let leading_indentation = Option.value_exn (String.lfindi s ~f:(fun _ c -> c <> ' ')) in
|
||||
s
|
||||
|> String.split ~on:'\n'
|
||||
|> List.map ~f:(Fn.flip String.drop_prefix leading_indentation)
|
||||
|> String.concat ~sep:"\n"
|
||||
|> String.chop_suffix_exn ~suffix:"\n"
|
||||
|
||||
let configuration = Configuration.create ~match_kind:Fuzzy ()
|
||||
|
||||
let all ?(configuration = configuration) template source =
|
||||
C.all ~configuration ~template ~source
|
||||
|
||||
let print_matches matches =
|
||||
List.map matches ~f:Match.to_yojson
|
||||
|> (fun matches -> `List matches)
|
||||
|> Yojson.Safe.pretty_to_string
|
||||
|> print_string
|
||||
|
||||
let%expect_test "comments_in_string_literals_should_not_be_treated_as_comments_by_fuzzy" =
|
||||
let source = {|"/*"(x)|} in
|
||||
let template = {|(:[1])|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
all template source
|
||||
|> Rewrite.all ~source ~rewrite_template
|
||||
|> (function
|
||||
| Some { rewritten_source; _ } -> print_string rewritten_source
|
||||
| None -> print_string "BROKEN EXPECT");
|
||||
[%expect_exact {|"/*"x|}]
|
||||
|
||||
let%expect_test "comments_in_string_literals_should_not_be_treated_as_comments_by_fuzzy_go_raw" =
|
||||
let source = {|`//`(x)|} in
|
||||
let template = {|(:[1])|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
Go.all ~configuration ~template ~source
|
||||
|> Rewrite.all ~source ~rewrite_template
|
||||
|> (function
|
||||
| Some { rewritten_source; _ } -> print_string rewritten_source
|
||||
| None -> print_string "BROKEN EXPECT");
|
||||
[%expect_exact {|`//`x|}]
|
||||
|
||||
let%expect_test "tolerate_unbalanced_stuff_in_string_literals" =
|
||||
let template = {|"("|} in
|
||||
let source = {|"("|} in
|
||||
let matches = all ~configuration template source in
|
||||
print_matches matches;
|
||||
[%expect_exact {|[
|
||||
{
|
||||
"range": {
|
||||
"start": { "offset": 0, "line": 1, "column": 1 },
|
||||
"end": { "offset": 3, "line": 1, "column": 4 }
|
||||
},
|
||||
"environment": [],
|
||||
"matched": "\"(\""
|
||||
}
|
||||
]|}]
|
||||
|
||||
let%expect_test "base_literal_matching" =
|
||||
let source = {|"hello"|} in
|
||||
let match_template = {|":[1]"|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
all match_template source
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact {|hello|}]
|
||||
|
||||
let%expect_test "base_literal_matching" =
|
||||
let source = {|rewrite ("hello") this string|} in
|
||||
let match_template = {|rewrite (":[1]") this string|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
all match_template source
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact {|hello|}]
|
||||
|
||||
let%expect_test "match_string_literals" =
|
||||
let source = {|rewrite (".") this string|} in
|
||||
let match_template = {|rewrite (":[1]") this string|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
all match_template source
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact {|.|}]
|
||||
|
||||
let%expect_test "match_string_literals" =
|
||||
let source = {|rewrite ("") this string|} in
|
||||
let match_template = {|rewrite (":[1]") this string|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
all match_template source
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact {||}]
|
||||
|
||||
let%expect_test "match_string_literals" =
|
||||
let source = {|"(" match "a""a" this "(" |} in
|
||||
let match_template = {|match :[1] this|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
all match_template source
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact {|"(" "a""a" "(" |}]
|
||||
|
||||
(* this tests special functionality in non-literal hole parser
|
||||
but which must still ignore unbalanced delims within strings *)
|
||||
let%expect_test "match_string_literals" =
|
||||
let source = {|"(" match "(""(" this "(" |} in
|
||||
let match_template = {|match :[1] this|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
all match_template source
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact {|"(" "(""(" "(" |}]
|
||||
|
||||
let%expect_test "match_string_literals" =
|
||||
let source = {|rewrite ("") this string|} in
|
||||
let match_template = {|rewrite (:[1]) this string|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
all match_template source
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact {|""|}]
|
||||
|
||||
let%expect_test "base_literal_matching" =
|
||||
let source = {|"("|} in
|
||||
let match_template = {|":[1]"|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
all match_template source
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact {|(|}]
|
||||
|
||||
let%expect_test "base_literal_matching" =
|
||||
let source = {|"(""("|} in
|
||||
let match_template = {|":[1]"|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
all match_template source
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact {|((|}]
|
||||
|
||||
let%expect_test "base_literal_matching" =
|
||||
let source = {|"(""("|} in
|
||||
let match_template = {|":[1]"|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
all match_template source
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact {|((|}]
|
||||
|
||||
let%expect_test "base_literal_matching" =
|
||||
let source = {|"hello world"|} in
|
||||
let match_template = {|":[x] :[y]"|} in
|
||||
let rewrite_template = {|:[x] :[y]|} in
|
||||
all match_template source
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact {|hello world|}]
|
||||
|
||||
(* complex test: basically, we are checking that the inside of this literal is only matched by the val b part *)
|
||||
let%expect_test "base_literal_matching" =
|
||||
let source = {|val a = "class = ${String::class}" val b = "not $a"|} in
|
||||
let match_template = {|":[x]$:[[y]]"|} in
|
||||
let rewrite_template = {|(rewritten part: (:[x]) ([y]))|} in
|
||||
all match_template source
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact {|val a = "class = ${String::class}" val b = (rewritten part: (not ) ([y]))|}]
|
||||
|
||||
let%expect_test "base_literal_matching" =
|
||||
let source = {|get("type") rekt ("enabled", True)|} in
|
||||
let match_template = {|(":[1]", :[[3]])|} in
|
||||
let rewrite_template = {|(rewritten part: (:[1]) (:[3]))|} in
|
||||
all match_template source
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact {|get("type") rekt (rewritten part: (enabled) (True))|}]
|
||||
|
||||
let%expect_test "rewrite_string_literals_8" =
|
||||
let source = {|match "\"" this|} in
|
||||
let match_template = {|match "\"" this|} in
|
||||
let rewrite_template = "" in
|
||||
|
||||
all match_template source
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact {||}]
|
||||
|
||||
let%expect_test "rewrite_string_literals_8" =
|
||||
let source = {|match "\"" this|} in
|
||||
let match_template = {|match :[1] this|} in
|
||||
let rewrite_template = ":[1]" in
|
||||
|
||||
all match_template source
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact {|"\""|}]
|
||||
|
||||
let%expect_test "rewrite_string_literals_8" =
|
||||
let source = {|match "\"\"" this|} in
|
||||
let match_template = {|match :[1] this|} in
|
||||
let rewrite_template = ":[1]" in
|
||||
|
||||
all match_template source
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact {|"\"\""|}]
|
||||
|
||||
let%expect_test "rewrite_string_literals_8" =
|
||||
let source = {|match "\"(\"" "(\"" this|} in
|
||||
let match_template = {|match :[1] this|} in
|
||||
let rewrite_template = ":[1]" in
|
||||
|
||||
all match_template source
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact {|"\"(\"" "(\""|}]
|
||||
|
||||
let%expect_test "rewrite_string_literals_8" =
|
||||
let source = {|match "\"(\"" "(\"" this|} in
|
||||
let match_template = {|match ":[1]" ":[2]" this|} in
|
||||
let rewrite_template = {|:[1] :[2]|} in
|
||||
|
||||
all match_template source
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact {|\"(\" (\"|}]
|
||||
|
||||
let%expect_test "rewrite_string_literals_8" =
|
||||
let source = {|match 'sin(gle' 'quo(tes' this|} in
|
||||
let match_template = {|:[1]|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
|
||||
all match_template source
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact {|match 'sin(gle' 'quo(tes' this|}]
|
||||
|
||||
let%expect_test "rewrite_string_literals_8" =
|
||||
let source = {|match '\''|} in
|
||||
let match_template = {|:[1]|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
|
||||
all match_template source
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact {|match '\''|}]
|
||||
|
||||
let%expect_test "rewrite_string_literals_8" =
|
||||
let source = {|match 'asdf'|} in
|
||||
let match_template = {|':[1]'|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
|
||||
all match_template source
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact {|match asdf|}]
|
||||
|
||||
let%expect_test "rewrite_string_literals_8" =
|
||||
let source = {|match '\''|} in
|
||||
let match_template = {|':[1]'|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
|
||||
all match_template source
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact {|match \'|}]
|
||||
|
||||
|
||||
let%expect_test "go_raw_string_literals" =
|
||||
let source =
|
||||
{|
|
||||
x = x
|
||||
y = `multi-line
|
||||
raw str(ing literal`
|
||||
z = `other multi-line
|
||||
raw stri(ng literal`
|
||||
|}
|
||||
in
|
||||
let match_template = {|`:[1]`|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
|
||||
Go.all ~configuration ~source ~template:match_template
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact {|
|
||||
x = x
|
||||
y = multi-line
|
||||
raw str(ing literal
|
||||
z = other multi-line
|
||||
raw stri(ng literal
|
||||
|}]
|
||||
|
||||
let%expect_test "go_raw_string_literals" =
|
||||
let source = {|blah `(` quux|} in
|
||||
let match_template = {|:[1]|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
|
||||
Go.all ~configuration ~source ~template:match_template
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact {|blah `(` quux|}]
|
||||
|
||||
let%expect_test "match_string_literals" =
|
||||
let source = {|`(` match `(``(` this `(` |} in
|
||||
let match_template = {|match :[1] this|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
Go.all ~configuration ~template:match_template ~source
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact {|`(` `(``(` `(` |}]
|
||||
|
||||
|
||||
let%expect_test "go_raw_string_literals" =
|
||||
let source =
|
||||
{|
|
||||
x = x
|
||||
y = `multi-line
|
||||
raw "str"(ing literal`
|
||||
z = `other multi-line
|
||||
raw '"'\"\\s\\\\\tr\ni(ng literal`
|
||||
|}
|
||||
in
|
||||
let match_template = {|`:[1]`|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
|
||||
Go.all ~configuration ~source ~template:match_template
|
||||
|> (fun matches -> Option.value_exn (Rewrite.all ~source ~rewrite_template matches))
|
||||
|> (fun { rewritten_source; _ } -> rewritten_source)
|
||||
|> print_string;
|
||||
[%expect_exact {|
|
||||
x = x
|
||||
y = multi-line
|
||||
raw "str"(ing literal
|
||||
z = other multi-line
|
||||
raw '"'\"\\s\\\\\tr\ni(ng literal
|
||||
|}]
|
||||
|
||||
let%expect_test "regression_matching_kubernetes" =
|
||||
let source = {|"\n" y = 5|} in
|
||||
let template = {|y = :[1]|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
Go.all ~configuration ~template ~source
|
||||
|> Rewrite.all ~source ~rewrite_template
|
||||
|> (function
|
||||
| Some { rewritten_source; _ } -> print_string rewritten_source
|
||||
| None -> print_string "BROKEN EXPECT");
|
||||
[%expect_exact {|"\n" 5|}]
|
||||
|
||||
|
||||
let%expect_test "match_escaped_any_char" =
|
||||
let source = {|printf("hello world\n");|} in
|
||||
let template = {|printf(":[1]");|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
Go.all ~configuration ~template ~source
|
||||
|> Rewrite.all ~source ~rewrite_template
|
||||
|> (function
|
||||
| Some { rewritten_source; _ } -> print_string rewritten_source
|
||||
| None -> print_string "BROKEN EXPECT");
|
||||
[%expect_exact {|hello world\n|}]
|
||||
|
||||
let%expect_test "match_escaped_escaped" =
|
||||
let source = {|printf("hello world\n\\");|} in
|
||||
let template = {|printf(":[1]");|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
Go.all ~configuration ~template ~source
|
||||
|> Rewrite.all ~source ~rewrite_template
|
||||
|> (function
|
||||
| Some { rewritten_source; _ } -> print_string rewritten_source
|
||||
| None -> print_string "BROKEN EXPECT");
|
||||
[%expect_exact {|hello world\n\\|}]
|
||||
|
||||
let%expect_test "match_escaped_escaped" =
|
||||
let source = {|printf("hello world\n\");|} in
|
||||
let template = {|printf(":[1]");|} in
|
||||
let rewrite_template = {|:[1]|} in
|
||||
Go.all ~configuration ~template ~source
|
||||
|> Rewrite.all ~source ~rewrite_template
|
||||
|> (function
|
||||
| Some { rewritten_source; _ } -> print_string rewritten_source
|
||||
| None -> print_string "EXPECT SUCCESS");
|
||||
[%expect_exact {|EXPECT SUCCESS|}]
|
Loading…
Reference in New Issue
Block a user