diff --git a/infer/src/backend/CommandLineOption.ml b/infer/src/backend/CommandLineOption.ml index f01402181..03d31a647 100644 --- a/infer/src/backend/CommandLineOption.ml +++ b/infer/src/backend/CommandLineOption.ml @@ -12,6 +12,7 @@ open! Utils module F = Format +module YBU = Yojson.Basic.Util (* Each command line option may appear in the --help list of any executable, these tags are used to @@ -28,7 +29,11 @@ let current_exe = | _ -> T -type desc = { long: string; short: string; meta: string; doc: string; spec: Arg.spec } +type desc = { + long: string; short: string; meta: string; doc: string; spec: Arg.spec; + (** how to go from an option in the json config file to a list of command-line options *) + decode_json: Yojson.Basic.json -> string list ; +} let dashdash long = match long with @@ -125,7 +130,7 @@ let add exes desc = ) exe_desc_lists let mk ?(deprecated=[]) ?(exes=[]) - ~long ?(short="") ~default ~meta doc ~default_to_string ~mk_setter ~mk_spec = + ~long ?(short="") ~default ~meta doc ~default_to_string ~decode_json ~mk_setter ~mk_spec = let variable = ref default in let closure = mk_setter variable in let setter str = @@ -136,7 +141,7 @@ let mk ?(deprecated=[]) ?(exes=[]) let default_string = default_to_string default in if default_string = "" then doc else doc ^ " (default: " ^ default_string ^ ")" in - let desc = {long; short; meta; doc; spec} in + let desc = {long; short; meta; doc; spec; decode_json} in (* add desc for long option, with documentation (which includes any short option) for exes *) add exes desc ; (* add desc for short option only for parsing, without documentation *) @@ -153,11 +158,16 @@ type 'a t = ?exes:exe list -> ?meta:string -> Arg.doc -> 'a +let string_json_decoder ~long json = [dashdash long; YBU.to_string json] + +let list_json_decoder json_decoder json = IList.flatten (YBU.convert_each json_decoder json) + let mk_set var value ?(deprecated=[]) ~long ?short ?exes ?(meta="") doc = let setter () = var := value in ignore( mk ~deprecated ~long ?short ~default:() ?exes ~meta doc ~default_to_string:(fun () -> "") + ~decode_json:(string_json_decoder ~long) ~mk_setter:(fun _ _ -> setter ()) ~mk_spec:(fun _ -> Arg.Unit setter) ) @@ -165,6 +175,7 @@ let mk_option ?(default=None) ?(default_to_string=fun _ -> "") ~f ?(deprecated=[]) ~long ?short ?exes ?(meta="") doc = mk ~deprecated ~long ?short ~default ?exes ~meta doc ~default_to_string + ~decode_json:(string_json_decoder ~long) ~mk_setter:(fun var str -> var := f str) ~mk_spec:(fun set -> Arg.String set) @@ -199,10 +210,16 @@ let mk_bool ?(deprecated_no=[]) ?(default=false) ?(f=fun b -> b) let mk_spec set = Arg.Unit (fun () -> set "") in let var = mk ~long ?short ~deprecated ~default ?exes - ~meta doc ~default_to_string ~mk_setter:(fun var _ -> var := f true) ~mk_spec in + ~meta doc ~default_to_string ~mk_setter:(fun var _ -> var := f true) + ~decode_json:(fun json -> + [dashdash (if YBU.to_bool json then long else nolong)]) + ~mk_spec in ignore( mk ~long:nolong ?short:noshort ~deprecated:deprecated_no ~default:(not default) ?exes - ~meta nodoc ~default_to_string ~mk_setter:(fun _ _ -> var := f false) ~mk_spec ); + ~meta nodoc ~default_to_string ~mk_setter:(fun _ _ -> var := f false) + ~decode_json:(fun json -> + [dashdash (if YBU.to_bool json then nolong else long)]) + ~mk_spec ); var let mk_bool_group ?(deprecated_no=[]) ?(default=false) @@ -217,18 +234,21 @@ let mk_int ~default ?(deprecated=[]) ~long ?short ?exes ?(meta="") doc = mk ~deprecated ~long ?short ~default ?exes ~meta doc ~default_to_string:string_of_int ~mk_setter:(fun var str -> var := (int_of_string str)) + ~decode_json:(string_json_decoder ~long) ~mk_spec:(fun set -> Arg.String set) let mk_float ~default ?(deprecated=[]) ~long ?short ?exes ?(meta="") doc = mk ~deprecated ~long ?short ~default ?exes ~meta doc ~default_to_string:string_of_float ~mk_setter:(fun var str -> var := (float_of_string str)) + ~decode_json:(string_json_decoder ~long) ~mk_spec:(fun set -> Arg.String set) let mk_string ~default ?(f=fun s -> s) ?(deprecated=[]) ~long ?short ?exes ?(meta="") doc = mk ~deprecated ~long ?short ~default ?exes ~meta doc ~default_to_string:(fun s -> s) ~mk_setter:(fun var str -> var := f str) + ~decode_json:(string_json_decoder ~long) ~mk_spec:(fun set -> Arg.String set) let mk_string_opt ?default ?(f=fun s -> s) ?(deprecated=[]) ~long ?short ?exes ?(meta="") doc = @@ -241,6 +261,7 @@ let mk_string_list ?(default=[]) ?(f=fun s -> s) mk ~deprecated ~long ?short ~default ?exes ~meta doc ~default_to_string:(String.concat ", ") ~mk_setter:(fun var str -> var := (f str) :: !var) + ~decode_json:(list_json_decoder (string_json_decoder ~long)) ~mk_spec:(fun set -> Arg.String set) let mk_symbol ~default ~symbols ?(deprecated=[]) ~long ?short ?exes ?(meta="") doc = @@ -251,6 +272,7 @@ let mk_symbol ~default ~symbols ?(deprecated=[]) ~long ?short ?exes ?(meta="") d mk ~deprecated ~long ?short ~default ?exes ~meta doc ~default_to_string:(fun s -> to_string s) ~mk_setter:(fun var str -> var := of_string str) + ~decode_json:(string_json_decoder ~long) ~mk_spec:(fun set -> Arg.Symbol (strings, set)) let mk_symbol_opt ~symbols ?(deprecated=[]) ~long ?short ?exes ?(meta="") doc = @@ -259,6 +281,7 @@ let mk_symbol_opt ~symbols ?(deprecated=[]) ~long ?short ?exes ?(meta="") doc = mk ~deprecated ~long ?short ~default:None ?exes ~meta doc ~default_to_string:(fun _ -> "") ~mk_setter:(fun var str -> var := Some (of_string str)) + ~decode_json:(string_json_decoder ~long) ~mk_spec:(fun set -> Arg.Symbol (strings, set)) let mk_symbol_seq ?(default=[]) ~symbols ?(deprecated=[]) ~long ?short ?exes ?(meta="") doc = @@ -269,6 +292,9 @@ let mk_symbol_seq ?(default=[]) ~symbols ?(deprecated=[]) ~long ?short ?exes ?(m ~default_to_string:(fun syms -> String.concat " " (IList.map to_string syms)) ~mk_setter:(fun var str_seq -> var := IList.map of_string (Str.split (Str.regexp_string ",") str_seq)) + ~decode_json:(fun json -> + [dashdash long; + String.concat "," (YBU.convert_each YBU.to_string json)]) ~mk_spec:(fun set -> Arg.String set) let anon_fun = ref (fun arg -> raise (Arg.Bad ("unexpected anonymous argument: " ^ arg))) @@ -279,6 +305,38 @@ let mk_anon () = anon +(* begin temporarily export inferconfig_json while .inferconfig-specific options still exist *) +let inferconfig_json = ref (`Assoc []) +(* end temporarily export inferconfig_json *) + +let decode_inferconfig_to_argv path = + let json = match read_optional_json_file path with + | Ok json -> + (* begin temporarily export inferconfig_json while + .inferconfig-specific options still exist *) + inferconfig_json := json ; + (* end temporarily export inferconfig_json *) + json + | Error msg -> + F.eprintf "WARNING: Could not read or parse Infer config in %s:@\n%s@." path msg ; + `Assoc [] in + let desc_list = !(IList.assoc ( = ) current_exe exe_desc_lists) in + let json_config = YBU.to_assoc json in + let one_config_item result (key, json_val) = + try + let {decode_json} = IList.find (fun {long} -> string_equal key long) desc_list in + decode_json json_val @ result + with + | Not_found -> + (* TODO: have all json options be regular options as well. When this is done, we can show a + warning if a json key is not a valid option. *) + result + | YBU.Type_error (msg, json) -> + F.eprintf "WARNING: while reading config file %s:@\nIll-formed value %s for option %s: %s@." + path (Yojson.Basic.to_string json) key msg ; + result in + IList.fold_left one_config_item [] json_config + (** [sep_char] is used to separate elements of argv when encoded into environment variables *) let sep_char = '^' @@ -308,7 +366,7 @@ let prefix_before_rest args = prefix_before_rest_ [] args -let parse ?(incomplete=false) env_var exe_usage = +let parse ?(incomplete=false) ?config_file env_var exe_usage = let curr_speclist = ref [] and full_speclist = ref [] in @@ -322,12 +380,15 @@ let parse ?(incomplete=false) env_var exe_usage = exit status in let help_desc_list = - [ { long = "help"; short = ""; meta = ""; spec = Arg.Unit (fun () -> curr_usage 0); + [ { long = "help"; short = ""; meta = ""; decode_json = (fun _ -> []); + spec = Arg.Unit (fun () -> curr_usage 0); doc = "Display this list of options" } - ; { long = "help-full"; short = ""; meta = ""; spec = Arg.Unit (fun () -> full_usage 0); + ; { long = "help-full"; short = ""; meta = ""; decode_json = (fun _ -> []); + spec = Arg.Unit (fun () -> full_usage 0); doc = "Display the full list of options, including internal and experimental options" } ] in - let section heading speclist = + let normalize speclist = + let speclist = help_desc_list @ speclist in let norm k = let len = String.length k in if len > 3 && String.sub k 0 3 = "no-" then String.sub k 3 (len - 3) else k in @@ -338,27 +399,20 @@ let parse ?(incomplete=false) env_var exe_usage = | _, "--" -> -1 | _ -> String.compare (norm x) (norm y) in let sort speclist = IList.sort compare_specs speclist in - let add_heading speclist = - match heading with - | Some heading -> - let doc = "\n " ^ heading ^ "\n" in - { doc; long = ""; short = ""; meta = ""; spec = Arg.Unit (fun () -> ()) } :: speclist - | None -> - speclist in let suppress_help speclist = ("-help", Arg.Unit (fun () -> raise (Arg.Bad "unknown option '-help'")), "") :: speclist in - suppress_help (align ~limit:32 (add_heading (sort speclist))) + suppress_help (align ~limit:32 (sort speclist)) in let curr_desc_list = IList.assoc ( = ) current_exe exe_desc_lists in (* curr_speclist includes args for current exe with docs, and all other args without docs, so that all args can be parsed, but --help and parse failures only show external args for current exe *) - curr_speclist := (section None (help_desc_list @ !curr_desc_list)) + curr_speclist := normalize !curr_desc_list ; assert( check_no_duplicates !curr_speclist ) ; - full_speclist := (section None (help_desc_list @ !full_desc_list)) + full_speclist := normalize !full_desc_list ; let env_args = decode_env_to_argv (try Unix.getenv env_var with Not_found -> "") in (* begin transitional support for INFERCLANG_ARGS *) @@ -368,10 +422,16 @@ let parse ?(incomplete=false) env_var exe_usage = let env_args = c_args @ env_args in (* end transitional support for INFERCLANG_ARGS *) let exe_name, env_cl_args = prepend_to_argv env_args in + let all_args = match config_file with + | None -> env_cl_args + | Some path -> + let json_args = decode_inferconfig_to_argv path in + (* read .inferconfig first, as both env vars and command-line options overwrite it *) + json_args @ env_cl_args in let current = ref 0 in let rec parse_loop () = try - Arg.parse_argv_dynamic ~current (Array.of_list (exe_name :: env_cl_args)) + Arg.parse_argv_dynamic ~current (Array.of_list (exe_name :: all_args)) curr_speclist !anon_fun usage_msg with | Arg.Bad _ when incomplete -> parse_loop () @@ -380,5 +440,5 @@ let parse ?(incomplete=false) env_var exe_usage = in parse_loop () ; if not incomplete then - Unix.putenv env_var (encode_argv_to_env (prefix_before_rest env_cl_args)) ; + Unix.putenv env_var (encode_argv_to_env (prefix_before_rest all_args)) ; curr_usage diff --git a/infer/src/backend/CommandLineOption.mli b/infer/src/backend/CommandLineOption.mli index 7ec992200..574816cf0 100644 --- a/infer/src/backend/CommandLineOption.mli +++ b/infer/src/backend/CommandLineOption.mli @@ -11,6 +11,10 @@ open! Utils +(* begin temporarily export inferconfig_json while .inferconfig-specific options still exist *) +val inferconfig_json : Yojson.Basic.json ref +(* end temporarily export inferconfig_json *) + type exe = A | C | J | L | P | T val current_exe : exe @@ -89,8 +93,11 @@ val mk_anon : (** [parse env_var exe_usage] parses command line arguments as specified by preceding calls to the [mk_*] functions, and returns a function that prints the usage message and help text then exits. - The decoded value of environment variable [env_var] is prepended to [Sys.argv] before parsing. - Therefore arguments passed on the command line supercede those specified in the environment - variable. WARNING: If an argument appears both in the environment variable and on the command - line, it will be interpreted twice. *) -val parse : ?incomplete:bool -> string -> (exe -> Arg.usage_msg) -> (int -> 'a) + The decoded values of the inferconfig file [config_file], if provided, and of the environment + variable [env_var] are prepended to [Sys.argv] before parsing. Therefore arguments passed on + the command line supersede those specified in the environment variable, which themselves + supersede those passed via the config file. WARNING: An argument will be interpreted as many + times as it appears in all of the config file, the environment variable, and the command + line. *) +val parse : ?incomplete:bool -> ?config_file:string -> + string -> (exe -> Arg.usage_msg) -> (int -> 'a) diff --git a/infer/src/backend/config.ml b/infer/src/backend/config.ml index 18db9fc66..0e0093d10 100644 --- a/infer/src/backend/config.ml +++ b/infer/src/backend/config.ml @@ -952,7 +952,7 @@ let post_parsing_initialization () = let parse_args_and_return_usage_exit = - let usage_exit = CLOpt.parse "INFER_ARGS" exe_usage in + let usage_exit = CLOpt.parse ~config_file:inferconfig_path "INFER_ARGS" exe_usage in if !debug || (!developer_mode && not (CLOpt.current_exe = CLOpt.P)) then prerr_endline ((Filename.basename Sys.executable_name) ^ " got args " @@ -1055,22 +1055,15 @@ and write_html = !write_html and xml_specs = !xml_specs and zip_libraries = !zip_libraries -let inferconfig_json = - lazy ( - match read_optional_json_file inferconfig_path with - | Ok json -> json - | Error msg -> - F.fprintf F.err_formatter "Could not read or parse Infer config in %s:@\n%s@." - inferconfig_path msg; - exit 1) +let inferconfig_json = lazy !CLOpt.inferconfig_json and suppress_warnings_json = lazy ( let error msg = - F.fprintf F.err_formatter "There was an issue reading the option %s.@\n" + F.eprintf "There was an issue reading the option %s.@\n" suppress_warnings_annotations_long ; - F.fprintf F.err_formatter "If you did not call %s directly, this is likely a bug in Infer.@\n" + F.eprintf "If you did not call %s directly, this is likely a bug in Infer.@\n" (Filename.basename Sys.executable_name) ; - F.fprintf F.err_formatter "%s@." msg ; + F.eprintf "%s@." msg ; exit 1 in match !suppress_warnings_out with | Some path -> ( diff --git a/infer/src/backend/inferprint.ml b/infer/src/backend/inferprint.ml index b4d68309c..6599b88ca 100644 --- a/infer/src/backend/inferprint.ml +++ b/infer/src/backend/inferprint.ml @@ -823,7 +823,7 @@ module AnalysisResults = struct (* find spec files specified by command-line arguments *) IList.iter (fun arg -> if not (Filename.check_suffix arg Config.specs_files_suffix) && arg <> "." - then print_usage_exit "arguments must be .specs files" + then print_usage_exit ("file "^ arg ^ ": arguments must be .specs files") ) Config.anon_args ; (match Config.source_file_copy with | Some s -> source_file_copy := Some (DB.abs_source_file_from_path s)