From a9a2ab28121a15264a3a8b026ff5eda965cf8e57 Mon Sep 17 00:00:00 2001 From: Jeff Morrison Date: Thu, 21 Jul 2016 11:12:50 -0400 Subject: [PATCH] flow shadow-file --- Makefile | 1 + hack/utils/tty.ml | 15 +- hack/utils/tty.mli | 4 +- newtests/shadow_file_command/_flowconfig | 7 + .../shadow_file_command/function_exports.js | 4 + .../named_class_exports.js | 21 ++ .../named_variable_exports.js | 13 + newtests/shadow_file_command/non_flow_file.js | 1 + newtests/shadow_file_command/t.js | 14 + newtests/shadow_file_command/test.js | 69 ++++ newtests/shadow_file_command/type_error.js | 3 + src/commands/commandUtils.ml | 3 +- src/commands/shadowFileCommand.ml | 66 ++++ src/common/errors.ml | 29 +- src/common/errors.mli | 2 + src/flow.ml | 1 + src/server/server.ml | 30 ++ src/server/serverProt.ml | 6 + .../autocomplete/autocompleteService_js.ml | 1 + .../interfaceGenerator/InterfaceGenerator.ml | 240 ++++++++++++++ .../interfaceGenerator/InterfaceGenerator.mli | 1 + src/typing/codegen.ml | 295 ++++++++++++++++++ src/typing/flow_js.ml | 15 +- src/typing/flow_js.mli | 3 + tsrc/test/TestStep.js | 8 +- tsrc/test/assertions/stderr.js | 32 ++ 26 files changed, 860 insertions(+), 24 deletions(-) create mode 100644 newtests/shadow_file_command/_flowconfig create mode 100644 newtests/shadow_file_command/function_exports.js create mode 100644 newtests/shadow_file_command/named_class_exports.js create mode 100644 newtests/shadow_file_command/named_variable_exports.js create mode 100644 newtests/shadow_file_command/non_flow_file.js create mode 100644 newtests/shadow_file_command/t.js create mode 100644 newtests/shadow_file_command/test.js create mode 100644 newtests/shadow_file_command/type_error.js create mode 100644 src/commands/shadowFileCommand.ml create mode 100644 src/services/interfaceGenerator/InterfaceGenerator.ml create mode 100644 src/services/interfaceGenerator/InterfaceGenerator.mli create mode 100644 src/typing/codegen.ml create mode 100644 tsrc/test/assertions/stderr.js diff --git a/Makefile b/Makefile index 257c1028521..59d7026f016 100644 --- a/Makefile +++ b/Makefile @@ -49,6 +49,7 @@ MODULES=\ src/server\ src/services/autocomplete\ src/services/inference\ + src/services/interfaceGenerator\ src/services/port\ src/stubs\ src/typing\ diff --git a/hack/utils/tty.ml b/hack/utils/tty.ml index 514fc961c71..01d2a4aa13b 100644 --- a/hack/utils/tty.ml +++ b/hack/utils/tty.ml @@ -72,7 +72,7 @@ let style_num = function | NormalWithBG (text, bg) -> (text_num text) ^ ";" ^ (background_num bg) | BoldWithBG (text, bg) -> (text_num text) ^ ";" ^ (background_num bg) ^ ";1" -let print_one ?(color_mode=Color_Auto) c s = +let print_one ?(color_mode=Color_Auto) ?(stderr=false) c s = let should_color = match color_mode with | Color_Always -> true | Color_Never -> false @@ -83,15 +83,16 @@ let print_one ?(color_mode=Color_Auto) c s = | Some term -> Unix.isatty Unix.stdout && term <> "dumb" end in + let printf = if stderr then Printf.eprintf else Printf.printf in if should_color - then Printf.printf "\x1b[%sm%s\x1b[0m" (style_num c) (s) - else Printf.printf "%s" s + then printf "\x1b[%sm%s\x1b[0m" (style_num c) (s) + else printf "%s" s -let cprint ?(color_mode=Color_Auto) strs = - List.iter strs (fun (c, s) -> print_one ~color_mode c s) +let cprint ?(color_mode=Color_Auto) ?(stderr=false) strs = + List.iter strs (fun (c, s) -> print_one ~color_mode ~stderr c s) -let cprintf ?(color_mode=Color_Auto) c = - Printf.ksprintf (print_one ~color_mode c) +let cprintf ?(color_mode=Color_Auto) ?(stderr=false) c = + Printf.ksprintf (print_one ~color_mode ~stderr c) let (spinner, spinner_used) = let state = ref 0 in diff --git a/hack/utils/tty.mli b/hack/utils/tty.mli index 4be36f6afa7..5f7290e19c8 100644 --- a/hack/utils/tty.mli +++ b/hack/utils/tty.mli @@ -38,8 +38,8 @@ type color_mode = * Print a sequence of colorized strings to stdout, using ANSI color escapes * codes. *) -val cprint : ?color_mode:color_mode -> (style * string) list -> unit -val cprintf : ?color_mode:color_mode -> style -> +val cprint : ?color_mode:color_mode -> ?stderr:bool -> (style * string) list -> unit +val cprintf : ?color_mode:color_mode -> ?stderr:bool -> style -> ('a, unit, string, unit) format4 -> 'a (* These two functions provide a four-state TTY-friendly spinner that diff --git a/newtests/shadow_file_command/_flowconfig b/newtests/shadow_file_command/_flowconfig new file mode 100644 index 00000000000..4a58bdcdef3 --- /dev/null +++ b/newtests/shadow_file_command/_flowconfig @@ -0,0 +1,7 @@ +[ignore] + +[include] + +[libs] + +[options] diff --git a/newtests/shadow_file_command/function_exports.js b/newtests/shadow_file_command/function_exports.js new file mode 100644 index 00000000000..ea4626de330 --- /dev/null +++ b/newtests/shadow_file_command/function_exports.js @@ -0,0 +1,4 @@ +// @flow + +export function mono(a: number, b: {c: number}) { return a + b.c; }; +export function poly (a: V) { return a; } diff --git a/newtests/shadow_file_command/named_class_exports.js b/newtests/shadow_file_command/named_class_exports.js new file mode 100644 index 00000000000..f67dc35a4d4 --- /dev/null +++ b/newtests/shadow_file_command/named_class_exports.js @@ -0,0 +1,21 @@ +// @flow + +export class Base { + // Testing infinite type recursion + baseInst: Base; + + // Testing forward references + childInst: Child; + + baseMethod(a: number, b: string) { return a; } + overriddenMethod(a: {b: number, c: number}) { return a.b + a.c; } +}; + +export class Child extends Base { + notExported: NotExportedUsed; + + overriddenMethod(a: {b: number}) { return a.b; } +} + +class NotExportedUsed {} +class NotExportedNotUsed {} diff --git a/newtests/shadow_file_command/named_variable_exports.js b/newtests/shadow_file_command/named_variable_exports.js new file mode 100644 index 00000000000..5d5f1c333ae --- /dev/null +++ b/newtests/shadow_file_command/named_variable_exports.js @@ -0,0 +1,13 @@ +// @flow + +export const constExport = 42; +export let letExport = 43; +export var varExport = 44; + +export type typeExport = number; + +type UnexportedT = string; +export const unexportedAlias = ((0: any): UnexportedT); + +class C {} +export const unexportedNominal = ((0: any): C); diff --git a/newtests/shadow_file_command/non_flow_file.js b/newtests/shadow_file_command/non_flow_file.js new file mode 100644 index 00000000000..6c293163448 --- /dev/null +++ b/newtests/shadow_file_command/non_flow_file.js @@ -0,0 +1 @@ +export function addNum(a: number, b: number) { return a + b; } diff --git a/newtests/shadow_file_command/t.js b/newtests/shadow_file_command/t.js new file mode 100644 index 00000000000..c6795b33345 --- /dev/null +++ b/newtests/shadow_file_command/t.js @@ -0,0 +1,14 @@ +declare class Class0 { +} +declare export class Base { + baseInst: Base; + childInst: Child; + + baseMethod(a: number, b: string): number; +} + +declare export class Child extends Base { + notExported: Class0; +} + + diff --git a/newtests/shadow_file_command/test.js b/newtests/shadow_file_command/test.js new file mode 100644 index 00000000000..635d4aa7adc --- /dev/null +++ b/newtests/shadow_file_command/test.js @@ -0,0 +1,69 @@ +/* @flow */ + + +import {suite, test} from '../../tsrc/test/Tester'; + +export default suite(({addFile, flowCmd}) => [ + test('class exports', [ + addFile('named_class_exports.js'), + flowCmd(['shadow-file', 'named_class_exports.js']).stdout(` + declare class Class0 { + } + declare export class Base { + baseInst: Base; + childInst: Child; + + baseMethod(a: number, b: string): number; + overriddenMethod(a: {b: number, c: number}): number; + } + declare export class Child extends Base { + notExported: Class0; + + overriddenMethod(a: {b: number}): number; + } + `) + .stderr('') + ]), + + test('named variable exports', [ + addFile('named_variable_exports.js'), + flowCmd(['shadow-file', 'named_variable_exports.js']).stderr('').stdout(` + declare class Class0 { + } + declare export var constExport: 42; + declare export var letExport: 43; + export type typeExport = number; + declare export var unexportedAlias: string; + declare export var unexportedNominal: Class0; + declare export var varExport: 44; + `) + ]), + + test('function exports', [ + addFile('function_exports.js'), + flowCmd(['shadow-file', 'function_exports.js']).stderr('').stdout(` + declare export function mono(a: number, b: {c: number}): number; + declare export function poly(a: V): number; + `) + ]), + + test('non-@flow files', [ + addFile('non_flow_file.js'), + flowCmd(['shadow-file', 'non_flow_file.js']).stderr('').stdout(` + declare export function addNum(a: number, b: number): number; + `) + ]), + + test('type errors halt and stderr', [ + addFile('type_error.js'), + flowCmd(['shadow-file', 'type_error.js']).stdout('').stderr(` + type_error.js:3 + 3: export var a: string = 42; + ^^ number. This type is incompatible with + 3: export var a: string = 42; + ^^^^^^ string + Found 1 error + There must be no type errors in order to generate a shadow file! + `) + ]), +]); diff --git a/newtests/shadow_file_command/type_error.js b/newtests/shadow_file_command/type_error.js new file mode 100644 index 00000000000..0437eaf8ff1 --- /dev/null +++ b/newtests/shadow_file_command/type_error.js @@ -0,0 +1,3 @@ +// @flow + +export var a: string = 42; diff --git a/src/commands/commandUtils.ml b/src/commands/commandUtils.ml index 5d21a0a8332..1cd8f5b56cc 100644 --- a/src/commands/commandUtils.ml +++ b/src/commands/commandUtils.ml @@ -25,7 +25,8 @@ let expand_path file = if Path.file_exists path then Path.to_string path else begin - FlowExitStatus.(exit ~msg:"File not found" Input_error) + let msg = Printf.sprintf "File not found: %s" (Path.to_string path) in + FlowExitStatus.(exit ~msg Input_error) end (* line split/transform utils *) diff --git a/src/commands/shadowFileCommand.ml b/src/commands/shadowFileCommand.ml new file mode 100644 index 00000000000..de623588077 --- /dev/null +++ b/src/commands/shadowFileCommand.ml @@ -0,0 +1,66 @@ +open CommandUtils + +let spf = Printf.sprintf + +let name = "shadow-file" +let spec = { + CommandSpec. + name; + doc = "Given a filename, generate a shadow (.js.flow) file."; + usage = Printf.sprintf + "Usage: %s %s [OPTIONS] [FILE] [FILE] [FILE]...\n\n\ + e.g. %s %s foo.js > foo.js.flow\n" + CommandUtils.exe_name + name + CommandUtils.exe_name + name + ; + args = CommandSpec.ArgSpec.( + empty + |> server_flags + |> root_flag + |> error_flags + |> strip_root_flag + |> anon "file" (required string) + ~doc:"The file for which a shadow file should be generated" + ) +} + +let main option_values root error_flags strip_root file () = ( + let root = guess_root ( + match root with + | Some root -> Some root + | None -> Some (expand_path file) + ) in + let filename = ServerProt.FileName (expand_path file) in + let (in_chan, out_chan) = connect option_values root in + ServerProt.cmd_to_channel out_chan (ServerProt.GEN_INTERFACES [filename]); + let response = + (Timeout.input_value in_chan: ServerProt.gen_interface_response) + in + + match response with + | response::[] -> ( + match response with + | Utils_js.Err (ServerProt.GenIface_TypecheckError (_, errors)) -> + let errors = Errors.to_list errors in + Errors.print_error_summary ~stderr:true ~flags:error_flags ~strip_root ~root errors; + FlowExitStatus.exit + ~msg:"\nThere must be no type errors in order to generate a shadow file!" + FlowExitStatus.Type_error; + | Utils_js.Err (ServerProt.GenIface_UnexpectedError (file_path, error)) -> + FlowExitStatus.exit + ~msg:(spf "Error: %s: %s" file_path error) + FlowExitStatus.Unknown_error + | Utils_js.OK (_file, interface) -> + print_endline interface + ) + | response -> + let msg = spf ( + "Internal Error: Expected a single interface description from the " ^^ + "server, but received %d interfaces!" + ) (List.length response) in + prerr_endline msg +) + +let command = CommandSpec.command spec main diff --git a/src/common/errors.ml b/src/common/errors.ml index 35af7a52c1f..2c869708d19 100644 --- a/src/common/errors.ml +++ b/src/common/errors.ml @@ -238,19 +238,19 @@ let format_info info = let formatted = format_reason_color msg in String.concat "" (List.map snd formatted) -let print_reason_color ~first ~one_line ~color (message: message) = +let print_reason_color ?(stderr=false) ~first ~one_line ~color (message: message) = let to_print = format_reason_color ~first ~one_line message in (if first then Printf.printf "\n"); - Tty.cprint ~color_mode:color to_print + Tty.cprint ~color_mode:color ~stderr to_print -let print_error_color_old ~one_line ~color (e : error) = +let print_error_color_old ?(stderr=false) ~one_line ~color (e : error) = let { kind; messages; op; trace; extra } = e in let messages = prepend_kind_message messages kind in let messages = prepend_op_reason messages op in let messages = append_extra_info messages extra in let messages = append_trace_reasons messages trace in - print_reason_color ~first:true ~one_line ~color (List.hd messages); - List.iter (print_reason_color ~first:false ~one_line ~color) (List.tl messages) + print_reason_color ~stderr ~first:true ~one_line ~color (List.hd messages); + List.iter (print_reason_color ~stderr ~first:false ~one_line ~color) (List.tl messages) let file_location_style text = (Tty.Underline Tty.Default, text) let default_style text = (Tty.Normal Tty.Default, text) @@ -531,10 +531,10 @@ let get_pretty_printed_error_new ~stdin_file:stdin_file ~strip_root ~one_line ~r else to_print in (to_print @ [default_style "\n"]) -let print_error_color_new ~stdin_file:stdin_file ~strip_root ~one_line ~color ~root (error : error) = +let print_error_color_new ?(stderr=false) ~stdin_file:stdin_file ~strip_root ~one_line ~color ~root (error : error) = let to_print = get_pretty_printed_error_new ~stdin_file ~strip_root ~one_line ~root error in - Tty.cprint ~color_mode:color to_print + Tty.cprint ~stderr ~color_mode:color to_print (* TODO: deprecate this in favor of Reason.json_of_loc *) let deprecated_json_props_of_loc loc = Loc.( @@ -848,7 +848,7 @@ let print_error_deprecated = flush oc (* Human readable output *) -let print_error_summary ~flags ?(stdin_file=None) ~strip_root ~root errors = +let print_error_summary ?(stderr=false) ~flags ?(stdin_file=None) ~strip_root ~root errors = let error_or_errors n = if n != 1 then "errors" else "error" in let truncate = not (flags.Options.show_all_errors) in let one_line = flags.Options.one_line in @@ -858,17 +858,22 @@ let print_error_summary ~flags ?(stdin_file=None) ~strip_root ~root errors = else print_error_color_new ~stdin_file ~strip_root ~root in let print_error_if_not_truncated curr e = - (if not(truncate) || curr < 50 then print_error_color ~one_line ~color e); + (if not(truncate) || curr < 50 then print_error_color ~one_line ~color ~stderr e); curr + 1 in let total = List.fold_left print_error_if_not_truncated 0 errors in if total > 0 then print_newline (); + let (printf, p_endline) = + if stderr + then (Printf.eprintf, prerr_endline) + else (Printf.printf, print_endline) + in if truncate && total > 50 then ( - Printf.printf + printf "... %d more %s (only 50 out of %d errors displayed)\n" (total - 50) (error_or_errors (total - 50)) total; - print_endline "To see all errors, re-run Flow with --show-all-errors" + p_endline "To see all errors, re-run Flow with --show-all-errors" ) else - Printf.printf "Found %d %s\n" total (error_or_errors total) + printf "Found %d %s\n" total (error_or_errors total) diff --git a/src/common/errors.mli b/src/common/errors.mli index 456b72e785d..57973f94057 100644 --- a/src/common/errors.mli +++ b/src/common/errors.mli @@ -82,6 +82,7 @@ val json_of_errors_with_context : Hh_json.json val print_error_color_new: + ?stderr:bool -> stdin_file:stdin_file -> strip_root:bool -> one_line:bool -> @@ -100,6 +101,7 @@ val print_error_json : (* Human readable output *) val print_error_summary: + ?stderr:bool -> flags:Options.error_flags -> ?stdin_file:stdin_file -> strip_root: bool -> diff --git a/src/flow.ml b/src/flow.ml index cf66626059d..b7855652202 100644 --- a/src/flow.ml +++ b/src/flow.ml @@ -26,6 +26,7 @@ end = struct CoverageCommand.command; FindModuleCommand.command; ForceRecheckCommand.command; + ShadowFileCommand.command; GetDefCommand.command; GetImportersCommand.command; GetImportsCommand.command; diff --git a/src/server/server.ml b/src/server/server.ml index a554128ed86..e14bbd14a62 100644 --- a/src/server/server.ml +++ b/src/server/server.ml @@ -318,6 +318,34 @@ module FlowProgram : Server.SERVER_PROGRAM = struct Marshal.to_channel oc response []; flush oc + let gen_interfaces ~options files oc = + let results = files |> List.map (fun file -> + let file_path = + match file with + | ServerProt.FileContent (Some file_path, _) + | ServerProt.FileName file_path -> file_path + | ServerProt.FileContent (None, _) -> "??" + in + let src_file = Loc.SourceFile file_path in + let content = ServerProt.file_input_get_content file in + + match Types_js.typecheck_contents ~options content src_file with + | _, Some cx, errors, _ -> + if Errors.ErrorSet.cardinal errors > 0 + then Utils_js.Err ( + ServerProt.GenIface_TypecheckError (file_path, errors) + ) else ( + try Utils_js.OK (file_path, InterfaceGenerator.generate_interface cx) + with exn -> Utils_js.Err ( + ServerProt.GenIface_UnexpectedError (file_path, (Printexc.to_string exn)) + ) + ) + | _, _, errors, _ -> + Utils_js.Err (ServerProt.GenIface_TypecheckError (file_path, errors)) + ) in + Marshal.to_channel oc results []; + flush oc + let get_def ~options (file_input, line, col) oc = let filename = ServerProt.file_input_get_filename file_input in let file = Loc.SourceFile filename in @@ -488,6 +516,8 @@ module FlowProgram : Server.SERVER_PROGRAM = struct flush oc; let updates = process_updates genv !env (Utils_js.set_of_list files) in env := recheck genv !env updates + | ServerProt.GEN_INTERFACES files -> + gen_interfaces ~options files oc | ServerProt.GET_DEF (fn, line, char) -> get_def ~options (fn, line, char) oc | ServerProt.GET_IMPORTERS module_names -> diff --git a/src/server/serverProt.ml b/src/server/serverProt.ml index 0454057aed9..5f0133d5d3f 100644 --- a/src/server/serverProt.ml +++ b/src/server/serverProt.ml @@ -40,6 +40,7 @@ type command = | DUMP_TYPES of file_input * bool (* filename, include raw *) * (Path.t option) (* strip_root *) | ERROR_OUT_OF_DATE | FIND_MODULE of string * string +| GEN_INTERFACES of file_input list | GET_DEF of file_input * int * int (* filename, line, char *) | GET_IMPORTERS of string list | GET_IMPORTS of string list @@ -78,6 +79,11 @@ type infer_type_response = ( Loc.t * string ) Utils_js.ok_or_err +type gen_interface_error = + | GenIface_TypecheckError of string * Errors.ErrorSet.t + | GenIface_UnexpectedError of string * string +type gen_interface_response = (string * string, gen_interface_error) Utils_js.ok_or_err list + let cmd_to_channel (oc:out_channel) (cmd:command): unit = let command = { client_logging_context = FlowEventLogger.get_context (); diff --git a/src/services/autocomplete/autocompleteService_js.ml b/src/services/autocomplete/autocompleteService_js.ml index f50bbfb7317..6c0476fa30d 100644 --- a/src/services/autocomplete/autocompleteService_js.ml +++ b/src/services/autocomplete/autocompleteService_js.ml @@ -138,6 +138,7 @@ let autocomplete_member let result_str, t = Autocomplete.(match result with | Success _ -> "SUCCESS", this + | SuccessModule _ -> "SUCCESS", this | FailureMaybeType -> "FAILURE_NULLABLE", this | FailureAnyType -> "FAILURE_NO_COVERAGE", this | FailureUnhandledType t -> "FAILURE_UNHANDLED_TYPE", t) in diff --git a/src/services/interfaceGenerator/InterfaceGenerator.ml b/src/services/interfaceGenerator/InterfaceGenerator.ml new file mode 100644 index 00000000000..5433d3294a1 --- /dev/null +++ b/src/services/interfaceGenerator/InterfaceGenerator.ml @@ -0,0 +1,240 @@ +let spf = Printf.sprintf + +let exports_map cx module_name = + let module_map = Context.module_map cx in + match SMap.get module_name module_map with + | Some module_t -> ( + let module_t = Flow_js.resolve_type cx module_t in + match Flow_js.Autocomplete.extract_members cx module_t with + | Flow_js.Autocomplete.SuccessModule (named, cjs) -> (named, cjs) + | _ -> failwith ( + spf "Failed to extract the exports of %s" (Type.string_of_ctor module_t) + ) + ) + | None -> + failwith (spf "Unable to extract %s from the module_map!" module_name) + +let gen_class_body = + let rec gen_method method_name t env = Codegen.(Type.( + match resolve_type t env with + | AnnotT (_, t) -> gen_method method_name (resolve_type t env) env + | FunT (_, _static, _super, {params_tlist; params_names; return_t; _;}) -> + let is_empty_constructor = + method_name = "constructor" + && List.length params_tlist = 0 + && match resolve_type return_t env with VoidT _ -> true | _ -> false + in + if is_empty_constructor then env else ( + add_str " " env + |> add_str method_name + |> gen_tparams_list + |> add_str "(" + |> gen_fun_params params_names params_tlist + |> add_str "): " + |> gen_type return_t + |> add_str ";\n" + ) + | PolyT (tparams, t) -> + add_tparams tparams env |> gen_method method_name (resolve_type t env) + | t -> + add_str " " env + |> add_str method_name + |> add_str "() {} //" + |> add_str (Type.string_of_ctor t) + |> add_str "\n" + )) in + + fun fields methods env -> Codegen.( + let fields_count = SMap.cardinal fields in + let methods_count = SMap.cardinal methods in + + let env = add_str " {" env in + if fields_count + methods_count = 0 then add_str "}" env else ( + add_str "\n" env + |> SMap.fold (fun field_name t env -> + add_str " " env + |> add_str field_name + |> add_str ": " + |> gen_type t + |> add_str ";\n" + ) fields + (* There's always a "constructor" method *) + |> (if methods_count > 1 then add_str "\n" else (fun env -> env)) + |> SMap.fold gen_method methods + |> add_str "}" + ) +) + +let gen_named_exports = + let rec fold_named_export name t env = Codegen.(Type.( + let env = ( + match resolve_type t env with + | FunT (_, _static, _prototype, { + params_tlist; + params_names; + return_t; + _; + }) -> + add_str "declare export function " env + |> add_str name + |> gen_tparams_list + |> add_str "(" + |> gen_fun_params params_names params_tlist + |> add_str "): " + |> gen_type return_t + |> add_str ";" + + | PolyT (tparams, t) -> add_tparams tparams env |> fold_named_export name t + + | ThisClassT (InstanceT (_, _static, super, { + fields_tmap; + methods_tmap; + (* TODO: The only way to express `mixins` right now is with a + * `declare` statement. This is possible in implementation + * files, but it is extremely rare -- so punting on this for + * now. + *) + mixins = _; + _; + })) -> + let fields = Codegen.find_props fields_tmap env in + let methods = Codegen.find_props methods_tmap env in + let env = add_str "declare export class " env + |> add_str name + |> gen_tparams_list + in + let env = ( + match Codegen.resolve_type super env with + | MixedT _ -> env + | (ThisTypeAppT _) as t -> add_str " extends " env |> gen_type t + | _ -> failwith ( + spf "Unexpected super type for class: %s" (string_of_ctor super) + ) + ) in + gen_class_body fields methods env + + | TypeT (_, t) -> + add_str "export type " env + |> add_str name + |> gen_tparams_list + |> add_str " = " + |> gen_type t + |> add_str ";" + + | t -> + add_str "declare export var " env + |> add_str name + |> add_str ": " + |> gen_type t + |> add_str ";" + ) in + add_str "\n" env + )) in + SMap.fold fold_named_export + +class unexported_class_visitor = object(self) + inherit [Codegen.codegen_env * Type.TypeSet.t] Type_visitor.t as super + + method! tvar cx (env, seen) r id = + let t = Codegen.resolve_type (Type.OpenT (r, id)) env in + self#type_ cx (env, seen) t + + method! type_ cx (env, seen) t = Codegen.(Type.( + if TypeSet.mem t seen then (env, seen) else ( + let seen = TypeSet.add t seen in + match t with + (* class_id = 0 is top of the inheritance chain *) + | InstanceT (_, _, _, {class_id; _;}) when class_id = 0 -> (env, seen) + + | InstanceT (r, _static, extends, { + class_id; + fields_tmap; + methods_tmap; + _; + }) when not (has_declared_class class_id env || Reason.is_lib_reason r) -> + let class_name_id = next_declared_class_name env in + let class_name = spf "Class%d" class_name_id in + + (** + * Add to the list of declared classes *FIRST* to prevent inifite loops + * on recursive references to this class from within itself. + *) + let env = add_declared_class class_id class_name env in + let (env, seen) = super#type_ cx (env, seen) t in + + let env = add_str "declare class " env |> add_str class_name in + + let env = + match resolve_type extends env with + | MixedT _ -> env + | ClassT t when ( + match resolve_type t env with | MixedT _ -> true | _ -> false + ) -> env + | ThisTypeAppT (extends, _, ts) -> + add_str " extends " env + |> gen_type extends + |> add_str "<" + |> gen_separated_list ts ", " gen_type + |> add_str ">" + | extends -> add_str " extends " env |> gen_type extends + in + + let fields = find_props fields_tmap env in + let methods = find_props methods_tmap env in + let env = gen_class_body fields methods env |> add_str "\n" in + (env, seen) + + | t -> super#type_ cx (env, seen) t + ) + )) +end + +let declare_classes = + let rec mark_declared_classes name t env = Codegen.(Type.( + match t with + | ThisClassT (InstanceT (_, _, _, {class_id; _;})) -> + add_declared_class class_id name env + | PolyT (_, t) -> + mark_declared_classes name t env + | _ -> + env + )) in + + let visitor = new unexported_class_visitor in + let gen_unexported_classes _name t env = + fst (visitor#type_ env.Codegen.flow_cx (env, Type.TypeSet.empty) t) + in + + fun named_exports cjs_export env -> + (* Find and mark all the declared *exported* classes first *) + let env = SMap.fold mark_declared_classes named_exports env in + + (* Codegen any referenced, non-exported classes *) + let all_exports = + match cjs_export with + | None -> named_exports + | Some cjs_t -> SMap.add "*CJS*" cjs_t named_exports + in + SMap.fold gen_unexported_classes all_exports env + +let generate_interface cx = + let module_name = Modulename.to_string (Context.module_name cx) in + let (named_exports, cjs_export) = exports_map cx module_name in + + let env = Codegen.mk_env cx (Buffer.create 320) in + let env = declare_classes named_exports cjs_export env in + Codegen.to_string ( + match cjs_export with + | None -> gen_named_exports named_exports env + | Some cjs_t -> + let type_exports = SMap.filter Type.(fun _name t -> + let t = match t with OpenT _ -> Codegen.resolve_type t env | _ -> t in + match t with + | TypeT _ | PolyT (_, TypeT _) -> true + | _ -> false + ) named_exports in + gen_named_exports type_exports env + |> Codegen.add_str "\ndeclare module.exports: " + |> Codegen.gen_type cjs_t + |> Codegen.add_str ";" + ) diff --git a/src/services/interfaceGenerator/InterfaceGenerator.mli b/src/services/interfaceGenerator/InterfaceGenerator.mli new file mode 100644 index 00000000000..1672382df31 --- /dev/null +++ b/src/services/interfaceGenerator/InterfaceGenerator.mli @@ -0,0 +1 @@ +val generate_interface: Context.t -> string diff --git a/src/typing/codegen.ml b/src/typing/codegen.ml new file mode 100644 index 00000000000..f5dd22aaebc --- /dev/null +++ b/src/typing/codegen.ml @@ -0,0 +1,295 @@ +let spf = Printf.sprintf + +type codegen_env = { + buf: Buffer.t; + declared_classes: string IMap.t; + mutable next_declared_class_name: int; + flow_cx: Context.t; + indent: string; + tparams: Type.typeparam list; + applied_tparams: Type.t list; +} + +let add_str str env = Buffer.add_string env.buf str; env +let add_tparams tparams env = + (*let env = add_str (spf " /* Adding %d tparams */ " (List.length tparams)) + * env in*) + {env with tparams;} +let add_applied_tparams applied_tparams env = + (*let env = add_str (spf " /* Adding %d tapp-params */ " (List.length + * applied_tparams)) env in*) + {env with applied_tparams;} +let find_props tmap_id env = Flow_js.find_props env.flow_cx tmap_id +let next_declared_class_name env = + let id = env.next_declared_class_name in + env.next_declared_class_name <- id + 1; + id +let resolve_type t env = Flow_js.resolve_type env.flow_cx t +let to_string env = Buffer.contents env.buf +let with_indent indent fn env = + let orig_indent = env.indent in + let env = {env with indent = indent ^ orig_indent;} in + let env = fn env in + {env with indent = orig_indent;} + +let mk_env flow_cx buf = { + applied_tparams = []; + buf; + declared_classes = IMap.empty; + flow_cx; + indent = ""; + next_declared_class_name = 0; + tparams = []; +} + +let add_declared_class class_id name env = + {env with declared_classes = IMap.add class_id name env.declared_classes;} + +let has_declared_class class_id env = + IMap.mem class_id env.declared_classes + +let gen_separated_list list sep fn env = + let count = List.length list in + let (env, _) = List.fold_left (fun (env, idx) item -> + let idx = idx + 1 in + let env = fn item env in + ((if idx < count then add_str sep env else env), idx) + ) (env, 0) list in + env + +let rec gen_type t env = Type.( + (*let env = add_str (spf " /* Genning %s */ " (string_of_ctor t)) env in*) + match t with + | AnnotT (_, t) -> gen_type t env + | AnyFunT _ -> add_str "Function" env + | AnyObjT _ -> add_str "Object" env + | AnyT _ + | AnyWithLowerBoundT _ + | AnyWithUpperBoundT _ + -> add_str "any" env + | ArrT (_, tparam, ts) -> + (match ts with + | [] -> + add_str "Array<" env + |> gen_type tparam + |> add_str ">" + | _ -> + let t_count = List.length ts in + let env = add_str "[" env in + let (env, _) = List.fold_left (fun (env, idx) t -> + let env = gen_type t env in + let idx = idx + 1 in + let env = + if idx < t_count then add_str ", " env else env + in + (env, idx) + ) (env, 0) ts in + add_str "]" env + ) + | BoolT (_, Some v) -> add_str (spf "%b" v) env + | BoolT (_, None) -> add_str "boolean" env + | BoundT {name; _;} -> add_str name env + | ClassT t -> + add_str "Class<" env + |> gen_type t + |> add_str ">" + | CustomFunT (_, ObjectAssign) -> add_str "Object$Assign" env + | CustomFunT (_, ObjectGetPrototypeOf) -> add_str "Object$GetPrototypeOf" env + | CustomFunT (_, PromiseAll) -> add_str "Promise$All" env + | CustomFunT (_, ReactCreateElement) -> add_str "React$CreateElement" env + | CustomFunT (_, Merge) -> add_str "$Facebookism$Merge" env + | CustomFunT (_, MergeDeepInto) -> add_str "$Facebookism$MergeDeepInto" env + | CustomFunT (_, MergeInto) -> add_str "$Facebookism$MergeInto" env + | CustomFunT (_, Mixin) -> add_str "$Facebookism$Mixin" env + | CustomFunT (_, Idx) -> add_str "$Facebookism$Idx" env + (* TODO: Once predicate types are a little more fleshed out, fill out this + * codegen. + *) + | DepPredT (_, _) -> add_str "mixed /* TODO: Predicate type */" env + | DiffT (t1, t2) -> + add_str "$Diff<" env + |> gen_type t1 + |> add_str ", " + |> gen_type t2 + |> add_str ">" + | ExactT (_, t) -> add_str "$Exact<" env |> gen_type t |> add_str ">" + | FunProtoT _ -> add_str "typeof Function.prototype" env + | FunProtoApplyT _ -> add_str "typeof Function.prototype.apply" env + | FunProtoBindT _ -> add_str "typeof Function.prototype.bind" env + | FunProtoCallT _ -> add_str "typeof Function.prototype.call" env + | FunT (_, _static, _prototype, {params_tlist; params_names; return_t; _;}) -> + gen_tparams_list env + |> add_str "(" + |> gen_fun_params params_names params_tlist + |> add_str ") => " + |> gen_type return_t + | InstanceT (_, _static, _super, {class_id; _;}) -> ( + (* TODO: See if we can preserve class names *) + let env = + match IMap.get class_id env.declared_classes with + | Some name -> add_str name env + | None -> failwith ( + "Encountered an instance type for a class that has not been defined!" + ) + in + gen_tparams_list env + ) + | IntersectionT (_, intersection) -> gen_intersect_list intersection env + | KeysT (_, t) -> add_str "$Keys<" env |> gen_type t |> add_str ">" + | MaybeT t -> add_str "?" env |> gen_type t + | MixedT _ -> add_str "mixed" env + | NumT (_, Literal (_, v)) -> add_str (spf "%s" v) env + | NumT (_, (Truthy|AnyLiteral)) -> add_str "number" env + | NullT _ -> add_str "null" env + | ObjT (_, {flags = _; dict_t; props_tmap; proto_t = _;}) -> ( + match dict_t with + | Some {dict_name; key; value;} -> + let key_name = ( + match dict_name with + | Some n -> n + | None -> "_" + ) in + let key = resolve_type key env in + let value = resolve_type value env in + add_str (spf "{[%s: " key_name) env + |> gen_type key + |> add_str "]: " + |> gen_type value + |> add_str "}" + | None -> + let props = find_props props_tmap env in + let num_props = SMap.cardinal props in + let env = add_str "{" env in + let (env, _) = SMap.fold (fun k t (env, idx) -> + let t = resolve_type t env in + let (sep, t) = ( + match t with + | OptionalT t -> ("?: ", t) + | _ -> (": ", t) + ) in + let idx = idx + 1 in + + let env = + add_str k env + |> add_str sep + |> gen_type t + in + ((if idx < num_props then (add_str ", " env) else env), idx) + ) props (env, 0) in + add_str "}" env + ) + | OptionalT t -> add_str "void | " env |> gen_type t + | OpenT _ -> gen_type (resolve_type t env) env + | PolyT (tparams, t) -> gen_type t (add_tparams tparams env) + | RestT rest -> gen_type rest env + | ShapeT t -> add_str "$Shape<" env |> gen_type t |> add_str ">" + | SingletonBoolT (_, v) -> add_str (spf "%b" v) env + | SingletonNumT (_, (_, v)) -> add_str (spf "%s" v) env + | SingletonStrT (_, v) -> add_str (spf "%S" v) env + | StrT (_, Literal v) -> add_str (spf "%S" v) env + | StrT (_, (Truthy|AnyLiteral)) -> add_str "string" env + | ThisClassT t -> gen_type t env + | ThisTypeAppT (t, _, ts) -> add_applied_tparams ts env |> gen_type t + | TypeAppT (t, ts) -> add_applied_tparams ts env |> gen_type t + | TypeT (_, t) -> gen_type t env + | UnionT (_, union) -> gen_union_list union env + | VoidT _ -> add_str "void" env + + (** + * These types can't be expressed in code well so we fail back to `mixed`. + * + * TODO: This handling is a little low-fidelity which may not work for all + * cases. It works for current needs (best-effort codegen of shadow + * files), but at some point it might make sense to offer other kinds of + * handling for these types depening on the needs of the API user + * (i.e. raise, etc). + *) + | AbstractT _ + | ChoiceKitT _ + | EmptyT _ + | EvalT _ + | ExistsT _ + | ExtendsT _ + | IdxWrapper _ + | ModuleT _ + | TaintT _ + -> add_str (spf "mixed /* UNEXPECTED TYPE: %s */" (string_of_ctor t)) env +) + +and gen_fun_params params_names params_tlist env = + let params = + match params_names with + | Some params_names -> + List.rev (List.fold_left2 (fun params name t -> + (name, t):: params + ) [] params_names params_tlist) + | None -> + List.mapi (fun idx t -> (spf "p%d" idx, t)) params_tlist + in + gen_separated_list params ", " Type.(fun (name, t) env -> + match t with + | RestT t -> + add_str "..." env + |> add_str name + |> add_str ": Array<" + |> gen_type t + |> add_str ">" + | OptionalT t -> + add_str name env + |> add_str "?: " + |> gen_type t + | t -> + add_str name env + |> add_str ": " + |> gen_type t + ) env + +and gen_intersect_list intersection env = + let members = Type.InterRep.members intersection in + gen_separated_list members " & " gen_type env + +and gen_tparams_list = Type.( + let gen_tparam {reason = _; name; bound; polarity; default;} env = + let bound = resolve_type bound env in + let env = ( + match polarity with + | Negative -> add_str "-" env + | Neutral -> env + | Positive -> add_str "+" env + ) in + let env = add_str name env in + let env = ( + match bound with + | MixedT _ -> env + | bound -> add_str ": " env |> gen_type bound + ) in + let env = ( + match default with + | Some default -> add_str " = " env |> gen_type default + | None -> env + ) in + env + in + + fun env -> + let tparams = env.tparams in + let params_count = List.length tparams in + let applied_tparams = env.applied_tparams in + let applied_tparams_count = List.length applied_tparams in + (*let env = add_str (spf " /* Gen %d tparams, %d applied_tparams! */ " + * (List.length tparams) (List.length applied_tparams)) env in*) + match (params_count, applied_tparams_count) with + | (0, 0) -> env + | (_, 0) -> + {env with tparams = []; } + |> add_str "<" + |> gen_separated_list tparams ", " gen_tparam + |> add_str ">" + | _ -> + {env with tparams = []; applied_tparams = []; } + |> add_str "<" |> gen_separated_list applied_tparams ", " gen_type |> add_str ">" +) + +and gen_union_list union env = + let members = Type.UnionRep.members union in + gen_separated_list members " | " gen_type env diff --git a/src/typing/flow_js.ml b/src/typing/flow_js.ml index d1b709e1988..8bdf7326b4d 100644 --- a/src/typing/flow_js.ml +++ b/src/typing/flow_js.ml @@ -7546,6 +7546,7 @@ let intersect_members cx members = module Autocomplete : sig type member_result = | Success of Type.t SMap.t + | SuccessModule of Type.t SMap.t * (Type.t option) | FailureMaybeType | FailureAnyType | FailureUnhandledType of Type.t @@ -7559,13 +7560,17 @@ end = struct type member_result = | Success of Type.t SMap.t + | SuccessModule of Type.t SMap.t * (Type.t option) | FailureMaybeType | FailureAnyType | FailureUnhandledType of Type.t let command_result_of_member_result = function - | Success map -> + | Success map + | SuccessModule (map, None) -> OK map + | SuccessModule (named_exports, Some cjs_export) -> + OK (SMap.add "default" cjs_export named_exports) | FailureMaybeType -> Err "autocomplete on possibly null or undefined value" | FailureAnyType -> @@ -7616,6 +7621,14 @@ end = struct let prot_members = extract_members_as_map cx proto_t in let members = find_props cx flds in Success (SMap.union prot_members members) + | ModuleT (_, {exports_tmap; cjs_export;}) -> + let named_exports = find_props cx exports_tmap in + let cjs_export = + match cjs_export with + | Some t -> Some (resolve_type cx t) + | None -> None + in + SuccessModule (named_exports, cjs_export) | ThisTypeAppT (c, _, ts) | TypeAppT (c, ts) -> let c = resolve_type cx c in diff --git a/src/typing/flow_js.mli b/src/typing/flow_js.mli index 0b98d2f94cf..0c46bbb8541 100644 --- a/src/typing/flow_js.mli +++ b/src/typing/flow_js.mli @@ -66,6 +66,8 @@ val write_prop : Context.t -> int -> SMap.key -> Type.t -> unit val iter_props : Context.t -> int -> (string -> Type.t -> unit) -> unit +val find_props : Context.t -> int -> Type.t SMap.t + val visit_eval_id : Context.t -> int -> (Type.t -> unit) -> unit (* object/method types *) @@ -133,6 +135,7 @@ val possible_types_of_type: Context.t -> Type.t -> Type.t list module Autocomplete : sig type member_result = | Success of Type.t SMap.t + | SuccessModule of Type.t SMap.t * (Type.t option) | FailureMaybeType | FailureAnyType | FailureUnhandledType of Type.t diff --git a/tsrc/test/TestStep.js b/tsrc/test/TestStep.js index ef995fbd22c..ec0702c0b2b 100644 --- a/tsrc/test/TestStep.js +++ b/tsrc/test/TestStep.js @@ -3,6 +3,7 @@ import searchStackForTestAssertion from './searchStackForTestAssertion'; import newErrors from './assertions/newErrors'; import noNewErrors from './assertions/noNewErrors'; +import stderr from './assertions/stderr'; import stdout from './assertions/stdout'; import exitCodes from './assertions/exitCodes'; import noop from './assertions/noop'; @@ -10,7 +11,7 @@ import noop from './assertions/noop'; import type {AssertionLocation, ErrorAssertion, ErrorAssertionResult} from './assertions/assertionTypes'; import type {TestBuilder} from './builder'; import type {FlowResult} from '../flowResult'; -import type {StepEnvReadable, StepEnvWriteable} from './stepEnv' +import type {StepEnvReadable, StepEnvWriteable} from './stepEnv'; type Action = (builder: TestBuilder, envWrite: StepEnvWriteable) => Promise; @@ -99,6 +100,11 @@ class TestStepFirstOrSecondStage extends TestStep { return ret; } + stderr(expected: string): TestStepSecondStage { + const assertLoc = searchStackForTestAssertion(); + return this._cloneWithAssertion(stderr(expected, assertLoc)); + } + stdout(expected: string): TestStepSecondStage { const assertLoc = searchStackForTestAssertion(); return this._cloneWithAssertion(stdout(expected, assertLoc)); diff --git a/tsrc/test/assertions/stderr.js b/tsrc/test/assertions/stderr.js new file mode 100644 index 00000000000..9526d516ca1 --- /dev/null +++ b/tsrc/test/assertions/stderr.js @@ -0,0 +1,32 @@ +/* @flow */ + +import simpleDiffAssertion from './simpleDiffAssertion'; + +import type {AssertionLocation, ErrorAssertion} from './assertionTypes'; + +function formatIfJSON(actual: string) { + try { + return JSON.stringify(JSON.parse(actual), null, 2); + } catch (e) { + return actual; + } +} + +export default function( + expected: string, + assertLoc: ?AssertionLocation, +): ErrorAssertion { + return (reason: ?string, env) => { + const actual = formatIfJSON(env.getStderr()); + expected = formatIfJSON(expected); + const suggestion = { method: 'stdout', args: [formatIfJSON(actual)] }; + return simpleDiffAssertion( + expected, + actual, + assertLoc, + reason, + "stderr", + suggestion, + ); + }; +}