[buck] Unify buck command line arguments recognition, buck query invocation, and support target patterns and aliases

Summary:
:
Make both buck capture and compilation database handle buck command line arguments and invoke buck query the same way.
Plus allow:
- target patterns `//some/dir:` and `//some/dir/...`. However since `//some/dir:#flavor` and `//some/dir/...#flavor` are not supported, they need to be expanded before adding the infer flavor.
- target aliases (defined in `.buckconfig`)
- shortcuts `//some/dir` rewritten to `//some/dir:dir`
- relative path `some/dir:name` rewritten to `//some/dir:name`

Reviewed By: jvillard

Differential Revision: D5321087

fbshipit-source-id: 48876d4
master
Mehdi Bouaziz 7 years ago committed by Facebook Github Bot
parent 6c9cee700b
commit a2f69050ac

@ -129,3 +129,5 @@ let escape_filename s =
in in
escape_map map s escape_map map s
let escape_double_quotes s = escape_map (function '"' -> Some "\\\"" | _ -> None) s

@ -31,3 +31,6 @@ val escape_url : string -> string
val escape_filename : string -> string val escape_filename : string -> string
(** escape a string to be used as a file name *) (** escape a string to be used as a file name *)
val escape_double_quotes : string -> string
(** replaces double-quote with backslash double-quote *)

@ -221,13 +221,46 @@ let with_process_in command read =
do_finally_swallow_timeout ~f ~finally do_finally_swallow_timeout ~f ~finally
let shell_escape_command cmd = let shell_escape_command =
let escape arg = let no_quote_needed = Str.regexp "^[A-Za-z0-9-_%/:,.]+$" in
(* ends on-going single quote, output single quote inside double quotes, then open a new single let easy_single_quotable = Str.regexp "^[^']+$" in
let easy_double_quotable = Str.regexp "^[^$`\\!]+$" in
let escape = function
| "" ->
"''"
| arg ->
if Str.string_match no_quote_needed arg 0 then arg
else if Str.string_match easy_single_quotable arg 0 then F.sprintf "'%s'" arg
else if Str.string_match easy_double_quotable arg 0 then arg |> Escape.escape_double_quotes
|> F.sprintf "\"%s\""
else
(* ends on-going single quote, output single quote inside double quotes, then open a new single
quote *) quote *)
Escape.escape_map (function '\'' -> Some "'\"'\"'" | _ -> None) arg |> Printf.sprintf "'%s'" arg |> Escape.escape_map (function '\'' -> Some "'\"'\"'" | _ -> None)
|> F.sprintf "'%s'"
in in
List.map ~f:escape cmd |> String.concat ~sep:" " fun cmd -> List.map ~f:escape cmd |> String.concat ~sep:" "
let with_process_lines ~(debug: ('a, F.formatter, unit) format -> 'a) ~cmd ~tmp_prefix ~f =
let shell_cmd = shell_escape_command cmd in
let verbose_err_file = Filename.temp_file tmp_prefix ".err" in
let shell_cmd_redirected = Printf.sprintf "%s 2>'%s'" shell_cmd verbose_err_file in
debug "Trying to execute: %s@\n%!" shell_cmd_redirected ;
let input_lines chan = In_channel.input_lines ~fix_win_eol:true chan in
let res = with_process_in shell_cmd_redirected input_lines in
let verbose_errlog = with_file_in verbose_err_file ~f:In_channel.input_all in
if not (String.equal verbose_errlog "") then
debug "@\nlog:@\n<<<<<<@\n%s@\n>>>>>>@\n%!" verbose_errlog ;
match res with
| lines, Ok () ->
f lines
| lines, (Error _ as err) ->
let output = String.concat ~sep:"\n" lines in
L.(die ExternalError)
"*** Failed to execute: %s@\n*** Command: %s@\n*** Output:@\n%s@."
(Unix.Exit_or_signal.to_string_hum err)
shell_cmd output
(** Create a directory if it does not exist already. *) (** Create a directory if it does not exist already. *)

@ -67,6 +67,12 @@ val with_process_in : string -> (In_channel.t -> 'a) -> 'a * Unix.Exit_or_signal
val shell_escape_command : string list -> string val shell_escape_command : string list -> string
val with_process_lines :
debug:((string -> unit, Format.formatter, unit) format -> string -> unit) -> cmd:string list
-> tmp_prefix:string -> f:(string list -> 'res) -> 'res
(** Runs the command [cmd] and calls [f] on the output lines. Uses [debug] to print debug
information, and [tmp_prefix] as a prefix for temporary files. *)
val create_dir : string -> unit val create_dir : string -> unit
(** create a directory if it does not exist already *) (** create a directory if it does not exist already *)

@ -8,96 +8,174 @@
*) *)
open! IStd open! IStd
module F = Format
module L = Logging module L = Logging
type target = {name: string; flavors: string list} module Target = struct
type t = {name: string; flavors: string list}
let target_of_string target =
match String.split target ~on:'#' with
| [name; flavors_string] ->
let flavors = String.split flavors_string ~on:',' in
{name; flavors}
| [name] ->
{name; flavors= []}
| _ ->
L.(die ExternalError) "cannot parse target %s" target
let string_of_target {name; flavors} =
let pp_string fmt s = Format.fprintf fmt "%s" s in
Format.asprintf "%s#%a" name (Pp.comma_seq pp_string) flavors
let of_string target =
match String.split target ~on:'#' with
| [name; flavors_string] ->
let flavors = String.split flavors_string ~on:',' in
{name; flavors}
| [name] ->
{name; flavors= []}
| _ ->
L.(die ExternalError) "cannot parse target %s" target
let is_target_string =
let target_regexp = Str.regexp "[^/]*//[^/]+.*:.*" in
fun s -> Str.string_match target_regexp s 0
let to_string {name; flavors} = F.asprintf "%s#%a" name (Pp.comma_seq Pp.string) flavors
let no_targets_found_error_and_exit buck_cmd = let add_flavor_internal target flavor =
Process.print_error_and_exit
"No targets found in Buck command %s.@\nOnly fully qualified Buck targets are supported. In particular, aliases are not allowed.@."
(String.concat ~sep:" " buck_cmd)
let add_flavor_to_target target =
let add flavor =
if List.mem ~equal:String.equal target.flavors flavor then if List.mem ~equal:String.equal target.flavors flavor then
(* there's already an infer flavor associated to the target, do nothing *) (* there's already an infer flavor associated to the target, do nothing *)
target target
else {target with flavors= flavor :: target.flavors} else {target with flavors= flavor :: target.flavors}
in
match (Config.buck_compilation_database, Config.analyzer) with
| Some _, _ ->
add "compilation-database"
| None, CompileOnly ->
target
| None, (BiAbduction | CaptureOnly | Checkers | Linters) ->
add "infer-capture-all"
| None, Crashcontext ->
L.(die UserError)
"Analyzer %s is Java-only; not supported with Buck flavors"
(Config.string_of_analyzer Config.analyzer)
let add_flavors_to_buck_command build_cmd = let add_flavor ~extra_flavors target =
let add_infer_if_target s (cmd, found_one_target) = let target = List.fold_left ~f:add_flavor_internal ~init:target extra_flavors in
if not (is_target_string s) then (s :: cmd, found_one_target) match (Config.buck_compilation_database, Config.analyzer) with
else (string_of_target (add_flavor_to_target (target_of_string s)) :: cmd, true) | Some _, _ ->
in add_flavor_internal target "compilation-database"
let cmd', found_one_target = | None, CompileOnly ->
List.fold_right build_cmd ~f:add_infer_if_target ~init:([], false) target
in | None, (BiAbduction | CaptureOnly | Checkers | Linters) ->
if not found_one_target then no_targets_found_error_and_exit build_cmd ; add_flavor_internal target "infer-capture-all"
cmd' | None, Crashcontext ->
L.(die UserError)
"Analyzer %s is Java-only; not supported with Buck flavors"
let get_dependency_targets_and_add_flavors targets ~depth = (Config.string_of_analyzer Config.analyzer)
let build_deps_string targets =
List.map targets ~f:(fun target -> end
match depth with
| None (* full depth *) -> let parse_target_string =
Printf.sprintf "deps('%s')" target let alias_target_regexp = Str.regexp "^[^/:]+\\(#.*\\)?$" in
| Some n -> let pattern_target_regexp = Str.regexp "^[^/]*//\\(\\.\\.\\.\\|.*\\(:\\|/\\.\\.\\.\\)\\)$" in
Printf.sprintf "deps('%s', %d)" target n ) let normal_target_regexp = Str.regexp "^[^/]*//[^/].*:.+$" in
|> String.concat ~sep:" union " let noname_target_regexp = Str.regexp "^[^/]*//.*$" in
let parse_with_retry s ~retry =
(* do not consider --buck-options as targets *)
if String.equal s "" || Char.equal s.[0] '-' || Char.equal s.[0] '@' then `NotATarget s
else if Str.string_match alias_target_regexp s 0 then `AliasTarget s
else if Str.string_match pattern_target_regexp s 0 then `PatternTarget s
else if Str.string_match normal_target_regexp s 0 then `NormalTarget s
else if Str.string_match noname_target_regexp s 0 then
let name = String.split s ~on:'/' |> List.last_exn in
`NormalTarget (F.sprintf "%s:%s" s name)
else retry s
in in
let buck_query = fun s ->
[ "buck" parse_with_retry s ~retry:(fun s ->
; "query" parse_with_retry ("//" ^ s) ~retry:(fun s ->
; "\"kind('(apple_binary|apple_library|apple_test|cxx_binary|cxx_library|cxx_test)', " L.(die InternalError) "Do not know how to parse buck command line argument '%s'" s ) )
^ build_deps_string targets ^ ")\"" ]
module Query = struct
type expr =
| Deps of {depth: int option; expr: expr}
| Kind of {pattern: string; expr: expr}
| Set of string list
| Target of string
| Union of expr list
exception NotATarget
let quote_if_needed =
let no_quote_needed_regexp = Str.regexp "^[a-zA-Z0-9/:_*][a-zA-Z0-9/:.-_*]*$" in
fun s ->
if Str.string_match no_quote_needed_regexp s 0 then s
else s |> Escape.escape_double_quotes |> F.sprintf "\"%s\""
let target string = Target (quote_if_needed string)
let kind ~pattern expr = Kind {pattern= quote_if_needed pattern; expr}
let deps ?depth expr = Deps {depth; expr}
let set exprs =
match List.rev_map exprs ~f:(function Target t -> t | _ -> raise NotATarget) with
| targets ->
Set targets
| exception NotATarget ->
Union exprs
let rec pp fmt = function
| Target s ->
Pp.string fmt s
| Kind {pattern; expr} ->
F.fprintf fmt "kind(%s, %a)" pattern pp expr
| Deps {depth= None; expr} ->
F.fprintf fmt "deps(%a)" pp expr (* full depth *)
| Deps {depth= Some depth; expr} ->
F.fprintf fmt "deps(%a, %d)" pp expr depth
| Set sl ->
F.fprintf fmt "set(%a)" (Pp.seq Pp.string) sl
| Union exprs ->
Pp.seq ~sep:" + " pp fmt exprs
let exec expr =
let query = F.asprintf "%a" pp expr in
let cmd = ["buck"; "query"; query] in
let tmp_prefix = "buck_query_" in
let debug = L.(debug Capture Medium) in
Utils.with_process_lines ~debug ~cmd ~tmp_prefix ~f:Fn.id
end
let accepted_buck_commands = ["build"]
let parameters_with_argument =
["--build-report"; "--config"; "-j"; "--num-threads"; "--out"; "-v"; "--verbose"]
let accepted_buck_kinds_pattern = "(apple|cxx)_(binary|library|test)"
let max_command_line_length = 50
let die_if_empty f = function [] -> f L.(die UserError) | l -> l
let resolve_pattern_targets ~filter_kind ~dep_depth targets =
targets |> List.rev_map ~f:Query.target |> Query.set
|> (match dep_depth with None -> Fn.id | Some depth -> Query.deps ?depth)
|> (if filter_kind then Query.kind ~pattern:accepted_buck_kinds_pattern else Fn.id) |> Query.exec
|> die_if_empty (fun die -> die "*** buck query returned no targets.")
let resolve_alias_targets aliases =
let debug = L.(debug Capture Medium) in
(* we could use buck query to resolve aliases but buck targets --resolve-alias is faster *)
let cmd = "buck" :: "targets" :: "--resolve-alias" :: aliases in
let tmp_prefix = "buck_targets_" in
let on_result_lines =
die_if_empty (fun die ->
die "*** No alias found for: '%a'." (Pp.seq ~sep:"', '" Pp.string) aliases )
in in
let buck_query_cmd = String.concat buck_query ~sep:" " in Utils.with_process_lines ~debug ~cmd ~tmp_prefix ~f:on_result_lines
Logging.(debug Linters Quiet) "*** Executing command:@\n*** %s@." buck_query_cmd ;
let output, exit_or_signal = Utils.with_process_in buck_query_cmd In_channel.input_lines in
match exit_or_signal with type parsed_args =
| Error _ as status -> { rev_not_targets': string list
Logging.(die ExternalError) ; normal_targets: string list
"*** command failed:@\n*** %s@\n*** %s@." buck_query_cmd ; alias_targets: string list
(Unix.Exit_or_signal.to_string_hum status) ; pattern_targets: string list }
| Ok () ->
List.map output ~f:(fun name -> let empty_parsed_args =
string_of_target (add_flavor_to_target {name; flavors= Config.append_buck_flavors}) ) {rev_not_targets'= []; normal_targets= []; alias_targets= []; pattern_targets= []}
let split_buck_command buck_cmd =
match buck_cmd with
| command :: args when List.mem ~equal:String.equal accepted_buck_commands command ->
(command, args)
| _ ->
L.(die UserError)
"ERROR: cannot parse buck command `%a`. Expected %a." (Pp.seq Pp.string) buck_cmd
(Pp.seq ~sep:" or " Pp.string) accepted_buck_commands
(** Given a list of arguments return the extended list of arguments where (** Given a list of arguments return the extended list of arguments where
@ -120,12 +198,72 @@ let inline_argument_files buck_args =
List.concat_map ~f:expand_buck_arg buck_args List.concat_map ~f:expand_buck_arg buck_args
let store_targets_in_file buck_targets = type flavored_arguments = {command: string; rev_not_targets: string list; targets: string list}
let file = Filename.temp_file "buck_targets_" ".txt" in
let write_args outc = Out_channel.output_string outc (String.concat ~sep:"\n" buck_targets) in let add_flavors_to_buck_arguments ~filter_kind ~dep_depth ~extra_flavors original_buck_args =
Utils.with_file_out file ~f:write_args |> ignore ; let expanded_buck_args = inline_argument_files original_buck_args in
L.(debug Capture Quiet) "Buck targets options stored in file '%s'@\n" file ; let command, args = split_buck_command expanded_buck_args in
Printf.sprintf "@%s" file let rec parse_cmd_args parsed_args = function
| [] ->
parsed_args
| param :: arg :: args when List.mem ~equal:String.equal parameters_with_argument param ->
parse_cmd_args
{parsed_args with rev_not_targets'= arg :: param :: parsed_args.rev_not_targets'} args
| target :: args ->
let parsed_args =
match parse_target_string target with
| `NotATarget s ->
{parsed_args with rev_not_targets'= s :: parsed_args.rev_not_targets'}
| `NormalTarget t ->
{parsed_args with normal_targets= t :: parsed_args.normal_targets}
| `AliasTarget a ->
{parsed_args with alias_targets= a :: parsed_args.alias_targets}
| `PatternTarget p ->
{parsed_args with pattern_targets= p :: parsed_args.pattern_targets}
in
parse_cmd_args parsed_args args
in
let parsed_args = parse_cmd_args empty_parsed_args args in
let targets =
match (filter_kind, dep_depth, parsed_args) with
| false, None, {pattern_targets= []; alias_targets= []; normal_targets} ->
normal_targets
| false, None, {pattern_targets= []; alias_targets; normal_targets} ->
alias_targets |> resolve_alias_targets |> List.rev_append normal_targets
| _, _, {pattern_targets; alias_targets; normal_targets} ->
pattern_targets |> List.rev_append alias_targets |> List.rev_append normal_targets
|> resolve_pattern_targets ~filter_kind ~dep_depth
in
match targets with
| [] ->
L.(die UserError)
"ERROR: no targets found in Buck command `%a`." (Pp.seq Pp.string) original_buck_args
| _ ->
let rev_not_targets = parsed_args.rev_not_targets' in
let targets =
List.rev_map targets ~f:(fun t ->
Target.(t |> of_string |> add_flavor ~extra_flavors |> to_string) )
in
{command; rev_not_targets; targets}
let rec exceed_length ~max = function
| _ when max < 0 ->
true
| [] ->
false
| h :: t ->
exceed_length ~max:(max - String.length h) t
let store_args_in_file args =
if exceed_length ~max:max_command_line_length args then
let file = Filename.temp_file "buck_targets_" ".txt" in
let write_args outc = Out_channel.output_string outc (String.concat ~sep:"\n" args) in
let () = Utils.with_file_out file ~f:write_args in
L.(debug Capture Quiet) "Buck targets options stored in file '%s'@\n" file ;
[Printf.sprintf "@%s" file]
else args
let filter_compatible subcommand args = let filter_compatible subcommand args =

@ -9,30 +9,20 @@
open! IStd open! IStd
val is_target_string : string -> bool type flavored_arguments = {command: string; rev_not_targets: string list; targets: string list}
(** is this a Buck target string, eg //foo/bar:baz or boo//foo/bar:baz *)
val no_targets_found_error_and_exit : string list -> unit val add_flavors_to_buck_arguments :
(** prints an error that no Buck targets were identified in the given list, and exits *) filter_kind:bool -> dep_depth:int option option -> extra_flavors:string list -> string list
-> flavored_arguments
val add_flavors_to_buck_command : string list -> string list (** Add infer flavors to the targets in the given buck arguments, depending on the infer analyzer. For
(** Add infer flavors to the targets in the given buck command, depending on the infer analyzer. For
instance, in capture mode, the buck command: instance, in capture mode, the buck command:
buck build //foo/bar:baz#some,flavor build //foo/bar:baz#some,flavor
becomes: becomes:
buck build //foo/bar:baz#infer-capture-all,some,flavor build //foo/bar:baz#infer-capture-all,some,flavor
*) *)
val get_dependency_targets_and_add_flavors : string list -> depth:int option -> string list val store_args_in_file : string list -> string list
(** Runs buck query to get the dependency targets of the given targets (** Given a list of arguments, stores them in a file if needed and returns the new command line *)
[get_dependency_targets args] = targets with dependent targets, other args *)
val inline_argument_files : string list -> string list
(** Given a list of arguments to buck, return the extended list of arguments where
the args in a file have been extracted *)
val store_targets_in_file : string list -> string
(** Given a list of buck targets, stores them in a file and returns the file name *)
val filter_compatible : [> `Targets] -> string list -> string list val filter_compatible : [> `Targets] -> string list -> string list
(** keep only the options compatible with the given Buck subcommand *) (** keep only the options compatible with the given Buck subcommand *)

@ -90,45 +90,36 @@ let run_compilation_database compilation_database should_capture_file =
(** Computes the compilation database files. *) (** Computes the compilation database files. *)
let get_compilation_database_files_buck ~prog ~args = let get_compilation_database_files_buck ~prog ~args =
let all_buck_args = Buck.inline_argument_files args in let dep_depth =
let targets, no_targets = List.partition_tf ~f:Buck.is_target_string all_buck_args in match Config.buck_compilation_database with Some Deps depth -> Some depth | _ -> None
let targets =
match Config.buck_compilation_database with
| Some Deps depth ->
Buck.get_dependency_targets_and_add_flavors targets ~depth
| _ ->
Buck.add_flavors_to_buck_command targets
in in
match no_targets with match
| "build" :: no_targets_no_build Buck.add_flavors_to_buck_arguments ~filter_kind:true ~dep_depth
-> ( ~extra_flavors:Config.append_buck_flavors args
let targets_in_file = Buck.store_targets_in_file targets in with
let build_args = no_targets @ ["--config"; "*//cxx.pch_enabled=false"; targets_in_file] in | {command= "build" as command; rev_not_targets; targets} ->
let targets_args = Buck.store_args_in_file targets in
let build_args =
command
:: List.rev_append rev_not_targets
("--config" :: "*//cxx.pch_enabled=false" :: targets_args)
in
Logging.(debug Linters Quiet) Logging.(debug Linters Quiet)
"Processed buck command is : 'buck %s'@\n" "Processed buck command is: 'buck %a'@\n" (Pp.seq Pp.string) build_args ;
(String.concat ~sep:" " build_args) ;
Process.create_process_and_wait ~prog ~args:build_args ; Process.create_process_and_wait ~prog ~args:build_args ;
let buck_targets_shell = let buck_targets_shell =
Buck.filter_compatible `Targets no_targets_no_build prog
|> List.append [prog; "targets"; "--show-output"; targets_in_file] :: "targets"
|> Utils.shell_escape_command :: List.rev_append
in (Buck.filter_compatible `Targets rev_not_targets)
let output, exit_or_signal = ("--show-output" :: targets_args)
Utils.with_process_in buck_targets_shell In_channel.input_lines
in in
match exit_or_signal with let on_target_lines = function
| Error _ as status ->
L.(die ExternalError)
"*** command failed:@\n*** %s@\n*** %s@." buck_targets_shell
(Unix.Exit_or_signal.to_string_hum status)
| Ok () ->
match output with
| [] -> | [] ->
L.external_error "There are no files to process, exiting@." ; L.(die ExternalError) "There are no files to process, exiting"
L.exit 0
| lines -> | lines ->
L.(debug Capture Quiet) L.(debug Capture Quiet)
"Reading compilation database from:@\n%s@\n" (String.concat ~sep:"\n" lines) ; "Reading compilation database from:@\n%a@\n" (Pp.seq ~sep:"\n" Pp.string) lines ;
(* this assumes that flavors do not contain spaces *) (* this assumes that flavors do not contain spaces *)
let split_regex = Str.regexp "#[^ ]* " in let split_regex = Str.regexp "#[^ ]* " in
let scan_output compilation_database_files line = let scan_output compilation_database_files line =
@ -139,11 +130,14 @@ let get_compilation_database_files_buck ~prog ~args =
L.(die ExternalError) L.(die ExternalError)
"Failed to parse `buck targets --show-output ...` line of output:@\n%s" line "Failed to parse `buck targets --show-output ...` line of output:@\n%s" line
in in
List.fold ~f:scan_output ~init:[] lines ) List.fold ~f:scan_output ~init:[] lines
in
Utils.with_process_lines
~debug:L.(debug Capture Quiet)
~cmd:buck_targets_shell ~tmp_prefix:"buck_targets_" ~f:on_target_lines
| _ -> | _ ->
let cmd = String.concat ~sep:" " (prog :: args) in Process.print_error_and_exit "Incorrect buck command: %s %a. Please use buck build <targets>"
Process.print_error_and_exit "Incorrect buck command: %s. Please use buck build <targets>" prog (Pp.seq Pp.string) args
cmd
(** Compute the compilation database files. *) (** Compute the compilation database files. *)

@ -287,16 +287,15 @@ let capture ~changed_files mode =
(Option.to_list (Sys.getenv CLOpt.args_env_var) @ ["--buck"]) (Option.to_list (Sys.getenv CLOpt.args_env_var) @ ["--buck"])
in in
Unix.putenv ~key:CLOpt.args_env_var ~data:infer_args_with_buck ; Unix.putenv ~key:CLOpt.args_env_var ~data:infer_args_with_buck ;
let all_buck_args = Buck.inline_argument_files build_cmd in let prog, buck_args = IList.uncons_exn build_cmd in
let targets, no_targets = let {Buck.command; rev_not_targets; targets} =
List.partition_tf ~f:Buck.is_target_string all_buck_args Buck.add_flavors_to_buck_arguments ~filter_kind:false ~dep_depth:None
~extra_flavors:[] buck_args
in in
let targets_with_flavor = Buck.add_flavors_to_buck_command targets in let all_args = List.rev_append rev_not_targets targets in
let targets_in_file = Buck.store_targets_in_file targets_with_flavor in let updated_buck_cmd = prog :: command :: Buck.store_args_in_file all_args in
let updated_buck_cmd = no_targets @ [targets_in_file] in
Logging.(debug Capture Quiet) Logging.(debug Capture Quiet)
"Processed buck command '%s'@\n" "Processed buck command '%a'@\n" (Pp.seq Pp.string) updated_buck_cmd ;
(String.concat ~sep:" " updated_buck_cmd) ;
updated_buck_cmd updated_buck_cmd
else build_cmd ) ) else build_cmd ) )
in in

@ -100,3 +100,5 @@ let to_string f l =
let rec aux l = match l with [] -> "" | [s] -> f s | s :: rest -> f s ^ ", " ^ aux rest in let rec aux l = match l with [] -> "" | [s] -> f s | s :: rest -> f s ^ ", " ^ aux rest in
"[" ^ aux l ^ "]" "[" ^ aux l ^ "]"
let uncons_exn = function [] -> failwith "uncons_exn" | hd :: tl -> (hd, tl)

@ -27,3 +27,6 @@ val inter : ('a -> 'a -> int) -> 'a list -> 'a list -> 'a list
(** [inter cmp xs ys] are the elements in both [xs] and [ys], sorted according to [cmp]. *) (** [inter cmp xs ys] are the elements in both [xs] and [ys], sorted according to [cmp]. *)
val to_string : ('a -> string) -> 'a list -> string val to_string : ('a -> string) -> 'a list -> string
val uncons_exn : 'a list -> 'a * 'a list
(** deconstruct a list, like hd_exn and tl_exn *)

@ -24,4 +24,4 @@ infer-out/report.json: $(CLANG_DEPS) $(SOURCES)
NO_BUCKD=1 \ NO_BUCKD=1 \
$(INFER_BIN) -a $(ANALYZER) --stats $(INFER_OPTIONS) -o $(CURDIR)/$(@D) \ $(INFER_BIN) -a $(ANALYZER) --stats $(INFER_OPTIONS) -o $(CURDIR)/$(@D) \
--buck-compilation-database no-deps \ --buck-compilation-database no-deps \
-- $(BUCK) build --no-cache '//clang_compilation_database:Hel lo#x86_64' @clang_compilation_database/buck_target_hello_test.txt) -- $(BUCK) build --no-cache '//clang_compilation_database:Hel lo#default' @clang_compilation_database/buck_target_hello_test.txt)

Loading…
Cancel
Save