From 47488628a534fdd5d314fac76856505d0fa1815d Mon Sep 17 00:00:00 2001 From: Jeff Morrison Date: Thu, 1 Sep 2016 12:57:46 -0700 Subject: [PATCH] `flow gen-flow-files` Summary: This adds a new command, `flow gen-flow-files path/to/file.js`, which generates the minimal `.js.flow` interface file that can be used for publishing compiled Flow source. Specifically: It generates only `import`, `declare`, and `declare export` statements in order to omit all the implementation text. Background: Currently the best practice for publishing Flow source is (roughly): 1. `cp src/foo.js dist/foo.js.flow` 2. `babel src/foo.js > dist/foo.js` This mostly works, but has a few downsides: 1. The published contents of `dist/*` are much larger than they need to be -- they contain a pre-compiled copy of all the original source. This is basically just crufty bytes that need to be downloaded every time the package is `npm install`-ed. 2. The published `.js.flow` files contain implementation details -- which are much more susceptible to breaking changes across Flow versions. While breaking changes across Flow version are still possible even in `declare` statements, this happens *far* less often. 3. We also want to re-use this type -> code codegen infrastructure to generate libdefs for flow-typed at some point. This particular diff is only about `.js.flow` files (i.e. no `declare module ...` statements) -- but that can be added on in a follow up diff. This diff includes a new `codegen.ml` file which exposes an API intended for generating code from various things (types for now, possibly ASTs at some point if that's useful/performant/worth the effort). It's minimal in terms of feature-set right now but we can expand/improve it later. I didn't use `type_printer.ml` because it only handles some types and many of the types it will print aren't actual code or easily composable with other bits of code. It's also used by lots of stuff that I didn't really want to investigate breaking changes for while building out this feature. At some point it probably makes sense to either improve `Type_printer` enough to subsume `Codegen` or the other way around. I suspect the `Codegen` is going to be easier to generalize, but we'll leave that for a later time to look into. Closes https://github.com/facebook/flow/pull/2184 Reviewed By: gabelevi Differential Revision: D3663107 Pulled By: jeffmo fbshipit-source-id: a791f85235f978fc9e5e46639e0dec37b71fad60 --- Makefile | 1 + hack/utils/tty.ml | 14 +- hack/utils/tty.mli | 8 +- newtests/gen_flow_files_command/_flowconfig | 8 + .../default_class_export.js | 8 + .../default_function_exports.js | 4 + .../default_variable_exports.js | 4 + .../export_imported_type.js | 5 + .../exports_builtins.js | 3 + .../gen_flow_files_command/literal_types.js | 16 + .../named_class_exports.js | 29 ++ .../named_function_exports.js | 4 + .../named_type_exports.js | 4 + .../named_variable_exports.js | 13 + .../gen_flow_files_command/non_flow_file.js | 1 + .../gen_flow_files_command/object_types.js | 13 + .../gen_flow_files_command/optional_types.js | 7 + .../gen_flow_files_command/suppressions.js | 4 + newtests/gen_flow_files_command/test.js | 193 +++++++++ newtests/gen_flow_files_command/type_error.js | 3 + ocp_build_flow.ocp.fb | 11 + src/commands/commandUtils.ml | 3 +- src/commands/genFlowFilesCommand.ml | 127 ++++++ src/common/errors.ml | 30 +- src/common/errors.mli | 2 + src/flow.ml | 1 + src/server/server.ml | 67 +++ src/server/serverProt.ml | 10 + .../autocomplete/autocompleteService_js.ml | 1 + src/services/flowFileGen/flowFileGen.ml | 398 ++++++++++++++++++ src/services/flowFileGen/flowFileGen.mli | 11 + src/services/inference/context_cache.ml | 12 +- src/services/inference/context_cache.mli | 2 + src/services/inference/types_js.ml | 5 +- src/typing/codegen.ml | 392 +++++++++++++++++ src/typing/context.ml | 14 + src/typing/context.mli | 6 + src/typing/flow_js.ml | 15 +- src/typing/flow_js.mli | 3 + src/typing/statement.ml | 4 + src/typing/type_inference_js.ml | 4 +- tsrc/test/TestStep.js | 8 +- tsrc/test/assertions/stderr.js | 32 ++ 43 files changed, 1470 insertions(+), 30 deletions(-) create mode 100644 newtests/gen_flow_files_command/_flowconfig create mode 100644 newtests/gen_flow_files_command/default_class_export.js create mode 100644 newtests/gen_flow_files_command/default_function_exports.js create mode 100644 newtests/gen_flow_files_command/default_variable_exports.js create mode 100644 newtests/gen_flow_files_command/export_imported_type.js create mode 100644 newtests/gen_flow_files_command/exports_builtins.js create mode 100644 newtests/gen_flow_files_command/literal_types.js create mode 100644 newtests/gen_flow_files_command/named_class_exports.js create mode 100644 newtests/gen_flow_files_command/named_function_exports.js create mode 100644 newtests/gen_flow_files_command/named_type_exports.js create mode 100644 newtests/gen_flow_files_command/named_variable_exports.js create mode 100644 newtests/gen_flow_files_command/non_flow_file.js create mode 100644 newtests/gen_flow_files_command/object_types.js create mode 100644 newtests/gen_flow_files_command/optional_types.js create mode 100644 newtests/gen_flow_files_command/suppressions.js create mode 100644 newtests/gen_flow_files_command/test.js create mode 100644 newtests/gen_flow_files_command/type_error.js create mode 100644 src/commands/genFlowFilesCommand.ml create mode 100644 src/services/flowFileGen/flowFileGen.ml create mode 100644 src/services/flowFileGen/flowFileGen.mli create mode 100644 src/typing/codegen.ml create mode 100644 tsrc/test/assertions/stderr.js diff --git a/Makefile b/Makefile index 5c1756a385b..dd85f3fd32d 100644 --- a/Makefile +++ b/Makefile @@ -49,6 +49,7 @@ MODULES=\ src/server\ src/services/autocomplete\ src/services/inference\ + src/services/flowFileGen\ src/services/port\ src/stubs\ src/typing\ diff --git a/hack/utils/tty.ml b/hack/utils/tty.ml index 514fc961c71..f1b1a02d0d7 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) ?(out_channel=stdout) c s = let should_color = match color_mode with | Color_Always -> true | Color_Never -> false @@ -84,14 +84,14 @@ let print_one ?(color_mode=Color_Auto) c s = Unix.isatty Unix.stdout && term <> "dumb" end in if should_color - then Printf.printf "\x1b[%sm%s\x1b[0m" (style_num c) (s) - else Printf.printf "%s" s + then Printf.fprintf out_channel "\x1b[%sm%s\x1b[0m" (style_num c) (s) + else Printf.fprintf out_channel "%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) ?(out_channel=stdout) strs = + List.iter strs (fun (c, s) -> print_one ~color_mode ~out_channel c s) -let cprintf ?(color_mode=Color_Auto) c = - Printf.ksprintf (print_one ~color_mode c) +let cprintf ?(color_mode=Color_Auto) ?(out_channel=stdout) c = + Printf.ksprintf (print_one ~color_mode ~out_channel c) let (spinner, spinner_used) = let state = ref 0 in diff --git a/hack/utils/tty.mli b/hack/utils/tty.mli index 4be36f6afa7..27c566867f3 100644 --- a/hack/utils/tty.mli +++ b/hack/utils/tty.mli @@ -35,11 +35,11 @@ type color_mode = | Color_Auto (* - * Print a sequence of colorized strings to stdout, using ANSI color escapes - * codes. + * Print a sequence of colorized strings to stdout/stderr, 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 -> ?out_channel:out_channel -> (style * string) list -> unit +val cprintf : ?color_mode:color_mode -> ?out_channel:out_channel -> style -> ('a, unit, string, unit) format4 -> 'a (* These two functions provide a four-state TTY-friendly spinner that diff --git a/newtests/gen_flow_files_command/_flowconfig b/newtests/gen_flow_files_command/_flowconfig new file mode 100644 index 00000000000..11afc363e25 --- /dev/null +++ b/newtests/gen_flow_files_command/_flowconfig @@ -0,0 +1,8 @@ +[ignore] + +[include] + +[libs] + +[options] +suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe diff --git a/newtests/gen_flow_files_command/default_class_export.js b/newtests/gen_flow_files_command/default_class_export.js new file mode 100644 index 00000000000..48f2c02c54b --- /dev/null +++ b/newtests/gen_flow_files_command/default_class_export.js @@ -0,0 +1,8 @@ +// @flow + +export class Base { +}; + +export default class Child extends Base { + p: number +} diff --git a/newtests/gen_flow_files_command/default_function_exports.js b/newtests/gen_flow_files_command/default_function_exports.js new file mode 100644 index 00000000000..e379157a3d9 --- /dev/null +++ b/newtests/gen_flow_files_command/default_function_exports.js @@ -0,0 +1,4 @@ +// @flow + +export default function (a: V) { return a; } +export function mono(a: number, b: {c: number}) { return a + b.c; }; diff --git a/newtests/gen_flow_files_command/default_variable_exports.js b/newtests/gen_flow_files_command/default_variable_exports.js new file mode 100644 index 00000000000..e0b36c3c7b0 --- /dev/null +++ b/newtests/gen_flow_files_command/default_variable_exports.js @@ -0,0 +1,4 @@ +// @flow + +export default 42; +export const str = "asdf"; diff --git a/newtests/gen_flow_files_command/export_imported_type.js b/newtests/gen_flow_files_command/export_imported_type.js new file mode 100644 index 00000000000..9c05503b4e4 --- /dev/null +++ b/newtests/gen_flow_files_command/export_imported_type.js @@ -0,0 +1,5 @@ +// @flow + +import {Child} from "./named_class_exports"; + +export const a: Child = new Child(); diff --git a/newtests/gen_flow_files_command/exports_builtins.js b/newtests/gen_flow_files_command/exports_builtins.js new file mode 100644 index 00000000000..592cfc4e0b5 --- /dev/null +++ b/newtests/gen_flow_files_command/exports_builtins.js @@ -0,0 +1,3 @@ +// @flow + +export function fn(a: Array) {}; diff --git a/newtests/gen_flow_files_command/literal_types.js b/newtests/gen_flow_files_command/literal_types.js new file mode 100644 index 00000000000..92a49f4b751 --- /dev/null +++ b/newtests/gen_flow_files_command/literal_types.js @@ -0,0 +1,16 @@ +// @flow + +export var varBool = true; +export var varBoolLiteral: true = true; +export var varNum = 42; +export var varNumLiteral: 42 = 42; +export var varStr = "asdf"; +export var varStrLiteral: "asdf" = "asdf"; + +export function f1(p: number) { + return "asdf"; +}; + +export function f2(p: 42): "asdf" { + return "asdf"; +}; diff --git a/newtests/gen_flow_files_command/named_class_exports.js b/newtests/gen_flow_files_command/named_class_exports.js new file mode 100644 index 00000000000..40a78101f81 --- /dev/null +++ b/newtests/gen_flow_files_command/named_class_exports.js @@ -0,0 +1,29 @@ +// @flow + +export class Base { + static baseStaticMethod(a: number, b: string) { return a; } + static overriddenStaticMethod(a: {b: number, c: number}) { return a.b + a.c; } + + // 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 { + static overriddenStaticMethod(a: {b: number}) { return a.b; } + + notExported: NotExportedUsed; + overriddenMethod(a: {b: number}) { return a.b; } +} + +class NotExportedUsed { + map(f: (x:T) => U): NotExportedUsed { + return new NotExportedUsed(); + }; +} +class NotExportedNotUsed {} diff --git a/newtests/gen_flow_files_command/named_function_exports.js b/newtests/gen_flow_files_command/named_function_exports.js new file mode 100644 index 00000000000..ea4626de330 --- /dev/null +++ b/newtests/gen_flow_files_command/named_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/gen_flow_files_command/named_type_exports.js b/newtests/gen_flow_files_command/named_type_exports.js new file mode 100644 index 00000000000..fcf1e58bc65 --- /dev/null +++ b/newtests/gen_flow_files_command/named_type_exports.js @@ -0,0 +1,4 @@ +// @flow + +export type T1 = number; +export type T2 = Array; diff --git a/newtests/gen_flow_files_command/named_variable_exports.js b/newtests/gen_flow_files_command/named_variable_exports.js new file mode 100644 index 00000000000..5d5f1c333ae --- /dev/null +++ b/newtests/gen_flow_files_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/gen_flow_files_command/non_flow_file.js b/newtests/gen_flow_files_command/non_flow_file.js new file mode 100644 index 00000000000..6c293163448 --- /dev/null +++ b/newtests/gen_flow_files_command/non_flow_file.js @@ -0,0 +1 @@ +export function addNum(a: number, b: number) { return a + b; } diff --git a/newtests/gen_flow_files_command/object_types.js b/newtests/gen_flow_files_command/object_types.js new file mode 100644 index 00000000000..ed77b6e1153 --- /dev/null +++ b/newtests/gen_flow_files_command/object_types.js @@ -0,0 +1,13 @@ +// @flow + +export var emptyObj = {}; + +export var singleProp = {p1: 42}; +export var multiProp = {p1: 42, p2: 42}; +export var nestedObject = {p1: {p2: 42}}; + +export var dict: {[key: string]: string} = {}; +export var dictWithProps: { + p1: string, + [key: string]: number, +} = {p1: "asdf"}; diff --git a/newtests/gen_flow_files_command/optional_types.js b/newtests/gen_flow_files_command/optional_types.js new file mode 100644 index 00000000000..a39df6860cb --- /dev/null +++ b/newtests/gen_flow_files_command/optional_types.js @@ -0,0 +1,7 @@ +// @flow + +var obj: {b?: number} = {b: 42}; + +export {obj}; +export var optNum = obj.b; +export var optFunc = (p?: number) => p; diff --git a/newtests/gen_flow_files_command/suppressions.js b/newtests/gen_flow_files_command/suppressions.js new file mode 100644 index 00000000000..d004dac32c3 --- /dev/null +++ b/newtests/gen_flow_files_command/suppressions.js @@ -0,0 +1,4 @@ +// @flow + +// $FlowFixMe +export function fn() { return ('asdf': number); }; diff --git a/newtests/gen_flow_files_command/test.js b/newtests/gen_flow_files_command/test.js new file mode 100644 index 00000000000..bfc6685d117 --- /dev/null +++ b/newtests/gen_flow_files_command/test.js @@ -0,0 +1,193 @@ +/* @flow */ + + +import {suite, test} from '../../tsrc/test/Tester'; + +export default suite(({addFile, flowCmd}) => [ + test('named class exports', [ + addFile('named_class_exports.js'), + flowCmd(['gen-flow-files', 'named_class_exports.js']).stdout(` + // @flow + + declare class Class0 { + map(f: (x: T) => U): Class0; + } + declare export class Base { + static baseStaticMethod(a: number, b: string): number; + static overriddenStaticMethod(a: {b: number, c: number}): number; + + baseInst: Base; + childInst: Child; + baseMethod(a: number, b: string): number; + overriddenMethod(a: {b: number, c: number}): number; + } + declare export class Child extends Base { + static overriddenStaticMethod(a: {b: number}): number; + + notExported: Class0; + overriddenMethod(a: {b: number}): number; + } + `) + .stderr('') + ]), + + test('named variable exports', [ + addFile('named_variable_exports.js'), + flowCmd(['gen-flow-files', 'named_variable_exports.js']).stderr('').stdout(` + // @flow + + declare class Class0 { + } + declare export var constExport: number; + declare export var letExport: number; + export type typeExport = number; + declare export var unexportedAlias: string; + declare export var unexportedNominal: Class0; + declare export var varExport: number; + `) + ]), + + test('named function exports', [ + addFile('named_function_exports.js'), + flowCmd(['gen-flow-files', 'named_function_exports.js']).stderr('').stdout(` + // @flow + + declare export function mono(a: number, b: {c: number}): number; + declare export function poly(a: V): number; + `) + ]), + + test('named type exports', [ + addFile('named_type_exports.js'), + flowCmd(['gen-flow-files', 'named_type_exports.js']).stderr('').stdout(` + // @flow + + export type T1 = number; + export type T2 = Array; + declare module.exports: {}; + `), + ]), + + test('default class exports', [ + addFile('default_class_export.js'), + flowCmd(['gen-flow-files', 'default_class_export.js']).stderr('').stdout(` + // @flow + + declare export class Base { + } + declare export default class extends Base { + p: number; + } + `), + ]), + + test('default variable exports', [ + addFile('default_variable_exports.js'), + flowCmd(['gen-flow-files', 'default_variable_exports.js']).stderr('').stdout(` + // @flow + + declare export default number; + declare export var str: string; + `), + ]), + + test('default function exports', [ + addFile('default_function_exports.js'), + flowCmd(['gen-flow-files', 'default_function_exports.js']).stderr('').stdout(` + // @flow + + declare export default function(a: V): number; + declare export function mono(a: number, b: {c: number}): number; + `), + ]), + + test('non-@flow files', [ + addFile('non_flow_file.js'), + flowCmd(['gen-flow-files', '--strip-root', 'non_flow_file.js']).stderr('').stdout(` + // This file does not have an @flow at the top! + `), + ]), + + test('type errors halt and print to stderr', [ + addFile('type_error.js'), + flowCmd(['gen-flow-files', '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 + In order to generate a shadow file there must be no type errors! + `) + ]), + + test('imported class types arent redefined', [ + addFile('named_class_exports.js'), + addFile('export_imported_type.js'), + flowCmd(['gen-flow-files', 'export_imported_type.js']).stderr('').stdout(` + // @flow + + import {Child} from "./named_class_exports"; + declare export var a: Child; + `) + ]), + + test('builtin class types arent redefined', [ + addFile('exports_builtins.js'), + flowCmd(['gen-flow-files', 'exports_builtins.js']).stderr('').stdout(` + // @flow + + declare export function fn(a: Array): void; + `) + ]), + + test('suppressed type errors get normalized', [ + addFile('suppressions.js'), + flowCmd(['gen-flow-files', 'suppressions.js']).stderr('').stdout(` + // @flow + + declare export function fn(): number; + `) + ]), + + test('literal types respect polarity', [ + addFile('literal_types.js'), + flowCmd(['gen-flow-files', 'literal_types.js']).stderr('').stdout(` + // @flow + + declare export function f1(p: number): string; + declare export function f2(p: 42): "asdf"; + declare export var varBool: boolean; + declare export var varBoolLiteral: true; + declare export var varNum: number; + declare export var varNumLiteral: 42; + declare export var varStr: string; + declare export var varStrLiteral: "asdf"; + `) + ]), + + test('optional types', [ + addFile('optional_types.js'), + flowCmd(['gen-flow-files', 'optional_types.js']).stderr('').stdout(` + // @flow + + declare export var obj: {b?: number}; + declare export function optFunc(p?: number): void | number; + declare export var optNum: void | number; + `) + ]), + + test('object types', [ + addFile('object_types.js'), + flowCmd(['gen-flow-files', 'object_types.js']).stderr('').stdout(` + // @flow + + declare export var dict: {[key: string]: string}; + declare export var dictWithProps: {p1: string, [key: string]: number}; + declare export var emptyObj: {}; + declare export var multiProp: {p1: number, p2: number}; + declare export var nestedObject: {p1: {p2: number}}; + declare export var singleProp: {p1: number}; + `) + ]), +]); diff --git a/newtests/gen_flow_files_command/type_error.js b/newtests/gen_flow_files_command/type_error.js new file mode 100644 index 00000000000..0437eaf8ff1 --- /dev/null +++ b/newtests/gen_flow_files_command/type_error.js @@ -0,0 +1,3 @@ +// @flow + +export var a: string = 42; diff --git a/ocp_build_flow.ocp.fb b/ocp_build_flow.ocp.fb index 3f20b004609..020c7535bf0 100644 --- a/ocp_build_flow.ocp.fb +++ b/ocp_build_flow.ocp.fb @@ -33,6 +33,7 @@ begin library "flow-commands" "flow-server-env" "flow-services-autocomplete" "flow-services-inference" + "flow-services-flowFileGen" "flow-typing" "hh-find" "hh-heap" @@ -231,6 +232,16 @@ begin library "flow-services-inference" ] end +begin library "flow-services-flowFileGen" + sort = true + files = begin fb-glob "src/services/flowFileGen" + end + requires = [ + "flow-typing" + "hh-utils-collections" + ] +end + begin library "flow-services-port" sort = true files = begin fb-glob "src/services/port" 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/genFlowFilesCommand.ml b/src/commands/genFlowFilesCommand.ml new file mode 100644 index 00000000000..3ceb02f565b --- /dev/null +++ b/src/commands/genFlowFilesCommand.ml @@ -0,0 +1,127 @@ +(** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the "flow" directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + *) + +open CommandUtils + +let spf = Printf.sprintf + +let name = "gen-flow-files" +let spec = { + CommandSpec. + name; + + (** + * Still iterating on this command but wanted to leave the eventual + * docs/usage info that we're targeting here in comments here to + * foreshadow what's to come a bit. + *) + (* + doc = "Generate minimal .js.flow files for publishing to npm."; + usage = Printf.sprintf + ("Usage: %s %s [OPTIONS] SRC_DIR OUT_DIR\n" ^^ + " or\n" ^^ + " %s %s [OPTIONS] INPUT_FILE\n" ^^ + "\n" ^^ + "e.g. %s %s ./src ./dist\n" ^^ + "or %s %s ./src/lib/foo.js > ./dist/lib/foo.js.flow\n") + + CommandUtils.exe_name + name + CommandUtils.exe_name + name + CommandUtils.exe_name + name + CommandUtils.exe_name + name + ; + *) + doc = "EXPERIMENTAL: Generate minimal .js.flow files for publishing to npm."; + usage = Printf.sprintf + "Usage (EXPERIMENTAL): %s %s [OPTIONS] [FILE]\n\n\ + e.g. %s %s ./src/foo.js > ./dist/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 "src_dir" (required string) + ~doc:"The source directory to scan and generate .js.flow files from" + |> anon "out_dir" string + ~doc:"The output directory to write generated .js.flow files into" + ) +} + +let main option_values root error_flags strip_root src_dir out_dir () = ( + let root = guess_root ( + match root with + | Some root -> Some root + | None -> Some (expand_path src_dir) + ) in + let filenames = + (** + * If there are 2 args specified, the command was invoked with the form: + * + * flow gen-flow-files ./src ./dist + * + * If there is only 1 arg specified, the command was invoke with the form: + * + * flow gen-flow-files ./src/foo.js > ./dist/foo.js.flow + *) + match out_dir with + | Some _out_dir -> + failwith (Printf.sprintf + ("ERROR: This command is still \"in beta\" and only currently " ^^ + "supports the command form: `%s %s ./src/foo.js > ./dist/foo.js.flow`") + CommandUtils.exe_name + name + ) + | None -> [ServerProt.FileName (expand_path src_dir)] + in + let (in_chan, out_chan) = connect option_values root in + + let open ServerProt in + cmd_to_channel out_chan (GEN_FLOW_FILES filenames); + match (Timeout.input_value in_chan: gen_flow_file_response) with + | Utils_js.Err (GenFlowFile_TypecheckError errors) -> + let errors = Errors.to_list errors in + Errors.print_error_summary + ~out_channel:stderr + ~flags:error_flags + ~strip_root + ~root + errors; + let msg = + "\nIn order to generate a shadow file there must be no type errors!" + in + FlowExitStatus.exit ~msg FlowExitStatus.Type_error; + | Utils_js.Err (GenFlowFile_UnexpectedError error_msg) -> + prerr_endline (spf "Error: %s" error_msg); + FlowExitStatus.exit FlowExitStatus.Unknown_error + | Utils_js.OK results -> + (if List.length results <> 1 then failwith ( + "Error: This command is still experimental and is currently only able " ^ + "to generate .js.flow files for 1 file at a time. Handling multiple " ^ + "files will come soon!" + )); + let (_filepath, result) = List.hd results in + match result with + | GenFlowFile_FlowFile content -> + print_endline content + | GenFlowFile_NonFlowFile -> + print_endline "// This file does not have an @flow at the top!" +) + +let command = CommandSpec.command spec main diff --git a/src/common/errors.ml b/src/common/errors.ml index 76bc41b9989..64a262c503f 100644 --- a/src/common/errors.ml +++ b/src/common/errors.ml @@ -239,19 +239,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 ?(out_channel=stdout) ~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 ~out_channel to_print -let print_error_color_old ~one_line ~color (e : error) = +let print_error_color_old ?(out_channel=stdout) ~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 ~out_channel ~first:true ~one_line ~color (List.hd messages); + List.iter (print_reason_color ~out_channel ~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) @@ -534,10 +534,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 ?(out_channel=stdout) ~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 ~out_channel ~color_mode:color to_print (* TODO: deprecate this in favor of Reason.json_of_loc *) let deprecated_json_props_of_loc loc = Loc.( @@ -852,7 +852,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 ?(out_channel=stdout) ~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 @@ -862,7 +862,9 @@ 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 ~out_channel e; + curr + 1 in let total = @@ -870,9 +872,13 @@ let print_error_summary ~flags ?(stdin_file=None) ~strip_root ~root errors = in if total > 0 then print_newline (); if truncate && total > 50 then ( - Printf.printf + Printf.fprintf + out_channel "... %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" + Printf.fprintf + out_channel + "To see all errors, re-run Flow with --show-all-errors"; + flush out_channel ) else - Printf.printf "Found %d %s\n" total (error_or_errors total) + Printf.fprintf out_channel "Found %d %s\n" total (error_or_errors total) diff --git a/src/common/errors.mli b/src/common/errors.mli index 456b72e785d..78724da6e95 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: + ?out_channel:out_channel -> 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: + ?out_channel:out_channel -> flags:Options.error_flags -> ?stdin_file:stdin_file -> strip_root: bool -> diff --git a/src/flow.ml b/src/flow.ml index cf66626059d..8d37a883de8 100644 --- a/src/flow.ml +++ b/src/flow.ml @@ -26,6 +26,7 @@ end = struct CoverageCommand.command; FindModuleCommand.command; ForceRecheckCommand.command; + GenFlowFilesCommand.command; GetDefCommand.command; GetImportersCommand.command; GetImportsCommand.command; diff --git a/src/server/server.ml b/src/server/server.ml index 1e8daec9b2f..97c315c1538 100644 --- a/src/server/server.ml +++ b/src/server/server.ml @@ -319,6 +319,71 @@ module FlowProgram : Server.SERVER_PROGRAM = struct Marshal.to_channel oc response []; flush oc + let gen_flow_files ~options env files oc = + let errors = env.ServerEnv.errorl in + let result = + if List.length errors > 0 then Utils_js.Err ( + ServerProt.GenFlowFile_TypecheckError (Errors.ErrorSet.of_list errors) + ) else ( + let cache = new Context_cache.context_cache in + let (flow_files, flow_file_cxs, non_flow_files, error) = + List.fold_left (fun (flow_files, cxs, non_flow_files, error) file -> + if error <> None then (flow_files, cxs, non_flow_files, error) else + match file with + | ServerProt.FileContent _ -> + let error_msg = "This command only works with file paths." in + let error = + Some (ServerProt.GenFlowFile_UnexpectedError error_msg) + in + (flow_files, cxs, non_flow_files, error) + | ServerProt.FileName file_path -> + let src_file = Loc.SourceFile file_path in + (* TODO: Use InfoHeap as the definitive way to detect @flow vs + * non-@flow + *) + match cache#read_safe src_file with + | None -> + (flow_files, cxs, src_file::non_flow_files, error) + | Some cx -> + (src_file::flow_files, cx::cxs, non_flow_files, error) + ) ([], [], [], None) files + in + match error with + | Some e -> Utils_js.Err e + | None -> ( + try + (if List.length flow_file_cxs > 0 then + try Merge_service.merge_strict_context ~options cache flow_file_cxs + with exn -> failwith ( + spf "Error merging contexts: %s" (Printexc.to_string exn) + ) + ); + + (* Non-@flow files *) + let result_contents = non_flow_files |> List.map (fun file -> + (Loc.string_of_filename file, ServerProt.GenFlowFile_NonFlowFile) + ) in + + (* Codegen @flow files *) + let result_contents = List.fold_left2 (fun results file cx -> + let file_path = Loc.string_of_filename file in + try + let code = FlowFileGen.flow_file cx in + (file_path, ServerProt.GenFlowFile_FlowFile code)::results + with exn -> + failwith (spf "%s: %s" file_path (Printexc.to_string exn)) + ) result_contents flow_files flow_file_cxs in + + Utils_js.OK result_contents + with exn -> Utils_js.Err ( + ServerProt.GenFlowFile_UnexpectedError (Printexc.to_string exn) + ) + ) + ) + in + Marshal.to_channel oc result []; + 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 @@ -490,6 +555,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_FLOW_FILES files -> + gen_flow_files ~options !env 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 772041f4fb7..c78b097f162 100644 --- a/src/server/serverProt.ml +++ b/src/server/serverProt.ml @@ -41,6 +41,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_FLOW_FILES of file_input list | GET_DEF of file_input * int * int (* filename, line, char *) | GET_IMPORTERS of string list | GET_IMPORTS of string list @@ -79,6 +80,15 @@ type infer_type_response = ( Loc.t * string ) Utils_js.ok_or_err +type gen_flow_file_error = + | GenFlowFile_TypecheckError of Errors.ErrorSet.t + | GenFlowFile_UnexpectedError of string +type gen_flow_file_result = + | GenFlowFile_FlowFile of string + | GenFlowFile_NonFlowFile +type gen_flow_file_response = + ((string * gen_flow_file_result) list, gen_flow_file_error) Utils_js.ok_or_err + 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/flowFileGen/flowFileGen.ml b/src/services/flowFileGen/flowFileGen.ml new file mode 100644 index 00000000000..8a1c342e6f5 --- /dev/null +++ b/src/services/flowFileGen/flowFileGen.ml @@ -0,0 +1,398 @@ +(** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the "flow" directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + *) + +module Ast = Spider_monkey_ast + +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 rec mark_declared_classes name t env = Codegen.(Type.( + match resolve_type t env with + | ThisClassT (InstanceT (_, _, _, {class_id; _;})) -> + set_class_name class_id name env + | PolyT (_, t) -> + mark_declared_classes name t env + | _ -> + env +)) + +let gen_imports env = + (** + * Print import statements. + * + * TODO: For now we just print all import statements, but it would be nice to + * only print the ones that are actually used by the declares. + *) + let import_stmts = Context.import_stmts env.Codegen.flow_cx in + let env = List.fold_left (fun env stmt -> + let open Ast in + let open Statement in + let open ImportDeclaration in + let {importKind; source; specifiers;} = stmt in + let (named, default, ns) = + List.fold_left (fun (named, default, ns) spec -> + match spec with + | ImportNamedSpecifier s -> + (s::named, default, ns) + | ImportDefaultSpecifier (_, {Identifier.name; _;}) -> + (named, Some name, ns) + | ImportNamespaceSpecifier (_, (_, {Identifier.name; _;})) -> + (named, default, Some name) + ) ([], None, None) specifiers + in + let named = List.rev named in + let source = + match source with + | (_, {Literal.value = Literal.String s; _;}) -> s + | _ -> failwith ( + "Internal error: Parsed a non-string for the `from` clause of an " ^ + "import!" + ) + in + + let env = Codegen.add_str "import " env in + let env = + match importKind with + | ImportType -> Codegen.add_str "type " env + | ImportTypeof -> Codegen.add_str "typeof " env + | ImportValue -> env + in + let env = + match default with + | Some default -> + let env = Codegen.add_str default env in + if ns <> None || List.length named > 0 + then Codegen.add_str ", " env + else env + | None -> env + in + let env = + match ns with + | Some ns -> + Codegen.add_str "* as " env |> Codegen.add_str ns + | None -> env + in + let env = if List.length named = 0 then env else ( + let env = Codegen.add_str "{" env in + let env = + Codegen.gen_separated_list named ", " (fun {local; remote} env -> + let (_, {Identifier.name = remote; _;}) = remote in + match local with + | Some (_, {Identifier.name = local; _;}) when local <> remote -> + Codegen.add_str remote env + |> Codegen.add_str " as " + |> Codegen.add_str local + | Some _ | None -> Codegen.add_str remote env + ) env + in + Codegen.add_str "}" env + ) in + Codegen.add_str " from \"" env + |> Codegen.add_str source + |> Codegen.add_str "\";\n" + ) env import_stmts in + + (** + * For each imported type, mark any imported class types so that they are not + * re-declared. + *) + let imported_ts = Context.imported_ts env.Codegen.flow_cx in + (imported_ts, SMap.fold mark_declared_classes imported_ts env) + +let gen_class_body = + let rec gen_method ~static method_name t env = Codegen.(Type.( + match resolve_type t env with + | AnnotT (_, t) -> gen_method ~static method_name t env + | FunT (_, _static, _super, {params_tlist; params_names; return_t; _;}) -> + let is_empty_constructor = + method_name = "constructor" + && (not static) + && 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 + |> gen_if static (add_str "static ") + |> add_str method_name + |> gen_tparams_list + |> add_str "(" + |> gen_func_params params_names params_tlist + |> add_str "): " + |> gen_type return_t + |> add_str ";\n" + ) + | PolyT (tparams, t) -> + add_tparams tparams env |> gen_method ~static method_name t + | t -> failwith ( + spf "Internal Error: Unexpected method type: %s" (string_of_ctor t) + ) + )) in + + let gen_field ~static field_name t env = Codegen.(Type.( + (** + * All classes have an implicit `static name: string` field on them. + * No need to re-print this. + *) + let is_static_name_field = static && field_name = "name" && ( + match resolve_type t env with + | StrT (_, AnyLiteral) -> true + | _ -> false + ) in + + if is_static_name_field then env else ( + add_str " " env + |> gen_if static (add_str "static ") + |> add_str field_name + |> add_str ": " + |> gen_type t + |> add_str ";\n" + ) + )) in + + fun static fields methods env -> Codegen.( + let (static_fields, static_methods) = Type.( + match static with + | InstanceT (_, _, _, {fields_tmap; methods_tmap; _;}) -> + (find_props fields_tmap env, find_props methods_tmap env) + | t -> failwith ( + spf + "Internal Error: Unexpected class static type: %s" + (string_of_ctor t) + ) + ) in + + let fields_count = SMap.cardinal fields in + let static_fields_count = SMap.cardinal static_fields in + let methods_count = SMap.cardinal methods in + let static_methods_count = SMap.cardinal static_methods in + let total_members_count = + fields_count + static_fields_count + methods_count + static_methods_count + in + + let env = add_str " {" env in + if total_members_count = 0 then add_str "}" env else ( + add_str "\n" env + |> SMap.fold (gen_field ~static:true) static_fields + |> SMap.fold (gen_method ~static:true) static_methods + |> add_str "\n" + |> SMap.fold (gen_field ~static:false) fields + |> SMap.fold (gen_method ~static:false) methods + |> add_str "}" + ) +) + +class unexported_class_visitor = object(self) + inherit [Codegen.codegen_env * Type.TypeSet.t * ISet.t] Type_visitor.t as super + + method! tvar cx (env, seen, imported_classids) r id = + let t = Codegen.resolve_type (Type.OpenT (r, id)) env in + self#type_ cx (env, seen, imported_classids) t + + method! type_ cx (env, seen, imported_classids) t = Codegen.(Type.( + if TypeSet.mem t seen then (env, seen, imported_classids) 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 || ISet.mem class_id imported_classids -> + (env, seen, imported_classids) + + | InstanceT (r, static, extends, { + class_id; + fields_tmap; + methods_tmap; + _; + }) when not (has_class_name class_id env || Reason.is_lib_reason r) -> + let class_name = next_class_name env in + + (** + * Add to the list of declared classes *FIRST* to prevent inifite loops + * on recursive references to this class from within itself. + *) + let env = set_class_name class_id class_name env in + let (env, seen, imported_classids) = super#type_ cx (env, seen, imported_classids) 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 static fields methods env |> add_str "\n" in + (env, seen, imported_classids) + + | t -> super#type_ cx (env, seen, imported_classids) t + ) + )) +end + +let gen_local_classes = + let visitor = new unexported_class_visitor in + let gen_unexported_classes imported_classids _name t env = + let (env, _, _) = + visitor#type_ + env.Codegen.flow_cx + (env, Type.TypeSet.empty, imported_classids) + t + in + env + in + + fun named_exports cjs_export env -> + let (imported_ts, env) = gen_imports env in + + (* Find and mark all the declared *exported* classes first *) + let env = SMap.fold mark_declared_classes named_exports env in + + (** + * Codegen any classes that are referenced but not exported. We're careful + * to not codegen classes that are referenced but *imported* as well. + *) + let all_exports = + match cjs_export with + | None -> named_exports + | Some cjs_t -> SMap.add "*CJS*" cjs_t named_exports + in + let rec fold_imported_classid _name t set = Type.( + match Codegen.resolve_type t env with + | ThisClassT (InstanceT (_, _, _, {class_id; _;})) -> + ISet.add class_id set + | PolyT (_, t) -> fold_imported_classid _name t set + | _ -> set + ) in + let imported_classids = + SMap.fold fold_imported_classid imported_ts ISet.empty + in + SMap.fold (gen_unexported_classes imported_classids) all_exports env + +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; + _; + }) -> + let env = + if name = "default" + then add_str "declare export default function" env + else add_str "declare export function " env |> add_str name + in + gen_tparams_list env + |> add_str "(" + |> gen_func_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 = + if name = "default" + then add_str "declare export default class" env + else add_str "declare export class " env |> add_str name + in + let env = gen_tparams_list env 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 static fields methods env + + | TypeT (_, t) -> + add_str "export type " env + |> add_str name + |> gen_tparams_list + |> add_str " = " + |> gen_type t + |> add_str ";" + + | t -> + let env = + if name = "default" + then add_str "declare export default " env + else add_str "declare export var " env |> add_str name |> add_str ": " + in + gen_type t env |> add_str ";" + ) in + add_str "\n" env + )) in + SMap.fold fold_named_export + +let gen_exports named_exports cjs_export env = + 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 ";" + +let flow_file cx = + let module_name = Modulename.to_string (Context.module_name cx) in + let (named_exports, cjs_export) = exports_map cx module_name in + + Codegen.mk_env cx + |> Codegen.add_str "// @flow\n\n" + |> gen_local_classes named_exports cjs_export + |> gen_exports named_exports cjs_export + |> Codegen.to_string diff --git a/src/services/flowFileGen/flowFileGen.mli b/src/services/flowFileGen/flowFileGen.mli new file mode 100644 index 00000000000..85987ffb467 --- /dev/null +++ b/src/services/flowFileGen/flowFileGen.mli @@ -0,0 +1,11 @@ +(** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the "flow" directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + *) + +val flow_file: Context.t -> string diff --git a/src/services/inference/context_cache.ml b/src/services/inference/context_cache.ml index fa7f66214e2..8da4f543dd1 100644 --- a/src/services/inference/context_cache.ml +++ b/src/services/inference/context_cache.ml @@ -51,7 +51,7 @@ let remove_sig_batch cxs = graphs is not only OK, but we rely on it during merging, so it is both safe and necessary to cache the local copies. As a side effect, this probably helps performance too by avoiding redundant copying. *) -class context_cache = object +class context_cache = object(self) val cached_infer_contexts = Hashtbl.create 0 (* find a context in the cache *) @@ -69,12 +69,16 @@ class context_cache = object let cx = Context.copy_of_context orig_cx in Hashtbl.add cached_infer_contexts file cx; cx + + method read_safe file = + try Some (self#read file) + with Key_not_found _ -> None end (* Similar to above, but for "signature contexts." The only differences are that the underlying heap is SigContextHeap instead of ContextHeap, and that `read` returns both the original and the copied version of a context. *) -class sig_context_cache = object +class sig_context_cache = object(self) val cached_merge_contexts = Hashtbl.create 0 (* find a context in the cache *) @@ -92,4 +96,8 @@ class sig_context_cache = object let cx = Context.copy_of_context orig_cx in Hashtbl.add cached_merge_contexts file cx; orig_cx, cx + + method read_safe file = + try Some (self#read file) + with Key_not_found _ -> None end diff --git a/src/services/inference/context_cache.mli b/src/services/inference/context_cache.mli index c81f87ff48c..3df65e9efa8 100644 --- a/src/services/inference/context_cache.mli +++ b/src/services/inference/context_cache.mli @@ -11,11 +11,13 @@ class context_cache : object method find: Loc.FilenameKey.t -> Context.t option method read: Loc.FilenameKey.t -> Context.t + method read_safe: Loc.FilenameKey.t -> Context.t option end class sig_context_cache : object method find: Loc.FilenameKey.t -> Context.t option method read: Loc.FilenameKey.t -> Context.t * Context.t + method read_safe: Loc.FilenameKey.t -> (Context.t * Context.t) option end val add: Context.t -> unit diff --git a/src/services/inference/types_js.ml b/src/services/inference/types_js.ml index 96a5a1c8efa..488297e4277 100644 --- a/src/services/inference/types_js.ml +++ b/src/services/inference/types_js.ml @@ -195,7 +195,10 @@ let typecheck_contents ~options ?verbose ?(check_syntax=false) (* infer *) let timing, cx = with_timer "Infer" timing (fun () -> Type_inference_js.infer_ast - ~metadata ~filename ~module_name:(Modulename.String "-") ast + ~metadata + ~filename + ~module_name:(Modulename.String "-") + ast ) in (* write graphml of (unmerged) types, if requested *) diff --git a/src/typing/codegen.ml b/src/typing/codegen.ml new file mode 100644 index 00000000000..f8f24f8bdaa --- /dev/null +++ b/src/typing/codegen.ml @@ -0,0 +1,392 @@ +(** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the "flow" directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + *) + +(** + * This file is a general-purpose utility for generating code. It is + * Context.t-aware, which allows it to resolve and codegen type syntax from + * types themselves. + * + * Example usage: + * + * let code_str = + * Codegen.mk_env cx + * |> Codegen.add_str "/* Before printed type */\n" + * |> Codegen.gen_type NullT.t + * |> Codegen.add_str "\n/* After printed type */\n" + * |> Codegen.to_string + * in + * print_endline code_str + *) + +let spf = Printf.sprintf + +type codegen_env = { + buf: Buffer.t; + class_names: string IMap.t; + mutable next_class_name: int; + flow_cx: Context.t; + tparams: Type.typeparam list; + applied_tparams: Type.t list; +} + +let add_applied_tparams applied_tparams env = {env with applied_tparams;} +let add_str str env = Buffer.add_string env.buf str; env +let add_tparams tparams env = {env with tparams;} +let find_props tmap_id env = Flow_js.find_props env.flow_cx tmap_id +let has_class_name class_id env = IMap.mem class_id env.class_names +let next_class_name env = + let id = env.next_class_name in + env.next_class_name <- id + 1; + spf "Class%d" id +let resolve_type t env = Flow_js.resolve_type env.flow_cx t +let set_class_name class_id name env = + {env with class_names = IMap.add class_id name env.class_names;} +let to_string env = Buffer.contents env.buf + +let mk_env merged_flow_cx = { + applied_tparams = []; + buf = Buffer.create 320; + class_names = IMap.empty; + flow_cx = merged_flow_cx; + next_class_name = 0; + tparams = []; +} + +(** + * Just a helper function to simplify this: + * + * let env = + * add_str "first" env + * |> add_str "second" + * |> add_str "third" + * in + * let env = + * if conditional + * then add_str "maybe fourth" env + * else env + * in + * add_str "fifth" env + * |> add_str "sixth" + * |> add_str "seventh" + * + * into this: + * + * add_str "first" env + * |> add_str "second" + * |> add_str "third" + * |> gen_if conditional (add_str "maybe fourth") + * |> add_str "fifth" + * |> add_str "sixth" + * |> add_str "seventh" + *) +let gen_if conditional gen_fn env = + if conditional then gen_fn env else env + +(** + * Given a type which must be a built-in class instance type, trace out the + * built-in's name and codegen it. + * + * NOTE: It would be good to come back to this and find a more general + * (less fragile) way of preserving class names alongside instance types. + *) +let gen_builtin_class_type t env = Type.( + (* AVERT YOUR EYES *) + let reason = reason_of_t t in + let builtin_name = Reason.desc_of_reason reason in + + (** + * Assert that the builtin name we found does match with the class_id we're + * backtracking. This is super defensive just because our method of getting + * a builtin's name is so hacky. Once we make that better, we can be less + * defensive here. + *) + let classid = + match t with + | InstanceT (_, _, _, {class_id; _;}) -> class_id + | t -> failwith ( + spf + ("Internal error: Expected an InstanceT while looking up a builtin " ^^ + "class name, but got a %s!") + (string_of_ctor t) + ) + in + + let builtin_t = Flow_js.get_builtin env.flow_cx builtin_name reason in + let builtin_classid = + match resolve_type builtin_t env with + | ThisClassT(InstanceT(_, _, _, {class_id; _;})) -> + class_id + | PolyT(_, ThisClassT(InstanceT(_, _, _, {class_id; _;}))) -> + class_id + | builtin_t -> failwith (spf "Unexpected global type: %s" (string_of_ctor builtin_t)) + in + + if builtin_classid = classid + then add_str builtin_name env + else failwith ( + "Internal error: Encountered an instance type for a class that " ^ + "has not been defined!" + ) +) + +(* Helper to generate a list of items with some separator between. *) +let gen_separated_list list sep gen_fn env = + let count = List.length list in + let (env, _) = List.fold_left (fun (env, idx) item -> + let idx = idx + 1 in + let env = gen_fn item env in + ((if idx < count then add_str sep env else env), idx) + ) (env, 0) list in + env + +(* Generate type syntax for a given type *) +let rec gen_type t env = Type.( + match t with + | AbstractT t -> add_str "$Abstract<" env |> gen_type t |> add_str ">" + | 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 _) -> + (* TODO: Consider polarity and print the literal type when appropriate *) + add_str "boolean" 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. + *) + | OpenPredT (_, _, _, _) -> add_str "mixed /* TODO: OpenPredT */" 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_func_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.class_names with + | Some name -> add_str name env + | None -> gen_builtin_class_type t env + in + gen_tparams_list env + ) + | IntersectionT (_, intersection) -> gen_intersection_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 _) -> + (* TODO: Consider polarity and print the literal type when appropriate *) + add_str "number" env + | NumT (_, (Truthy|AnyLiteral)) -> add_str "number" env + | NullT _ -> add_str "null" env + | ObjT (_, {flags = _; dict_t; props_tmap; proto_t = _;}) -> ( + let env = add_str "{" env in + + (* Generate prop entries *) + let props = find_props props_tmap env in + let props = SMap.elements props |> List.sort (fun (k1, _) (k2, _) -> + Pervasives.compare k1 k2 + ) in + let env = gen_separated_list props ", " (fun (k, t) env -> + let (sep, t) = + match resolve_type t env with + | OptionalT t -> ("?: ", resolve_type t env) + | t -> (": ", t) + in + add_str k env |> add_str sep |> gen_type t + ) env in + + (* Generate potential dict entry *) + let env = + 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 + gen_if (List.length props > 0) (add_str ", ") env + |> add_str "[" + |> add_str key_name + |> add_str ": " + |> gen_type key + |> add_str "]: " + |> gen_type value + | None -> env + 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 _) -> + (* TODO: Consider polarity and print the literal type when appropriate *) + add_str "string" 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). + *) + | ChoiceKitT _ + | EmptyT _ + | EvalT _ + | ExistsT _ + | ExtendsT _ + | IdxWrapper _ + | ModuleT _ + | TaintT _ + -> add_str (spf "mixed /* UNEXPECTED TYPE: %s */" (string_of_ctor t)) env +) + +and gen_func_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_intersection_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 + 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/context.ml b/src/typing/context.ml index acf4f2fb968..1a87a2a3dc2 100644 --- a/src/typing/context.ml +++ b/src/typing/context.ml @@ -8,6 +8,8 @@ * *) +module Ast = Spider_monkey_ast + type env = Scope.t list type metadata = { @@ -44,6 +46,9 @@ type t = { mutable require_loc: Loc.t SMap.t; mutable module_exports_type: module_exports_type; + mutable import_stmts: Ast.Statement.ImportDeclaration.t list; + mutable imported_ts: Type.t SMap.t; + (* map from tvar ids to nodes (type info structures) *) mutable graph: Constraint.node IMap.t; @@ -123,6 +128,9 @@ let make metadata file module_name = { require_loc = SMap.empty; module_exports_type = CommonJSModule(None); + import_stmts = []; + imported_ts = SMap.empty; + graph = IMap.empty; tvar_reasons = IMap.empty; envs = IMap.empty; @@ -167,6 +175,8 @@ let find_module cx m = SMap.find_unsafe m cx.modulemap let find_tvar_reason cx id = IMap.find_unsafe id cx.tvar_reasons let globals cx = cx.globals let graph cx = cx.graph +let import_stmts cx = cx.import_stmts +let imported_ts cx = cx.imported_ts let is_checked cx = cx.metadata.checked let is_verbose cx = cx.metadata.verbose <> None let is_weak cx = cx.metadata.weak @@ -211,6 +221,10 @@ let add_error_suppression cx loc = Errors.ErrorSuppressions.add loc cx.error_suppressions let add_global cx name = cx.globals <- SSet.add name cx.globals +let add_import_stmt cx stmt = + cx.import_stmts <- stmt::cx.import_stmts +let add_imported_t cx name t = + cx.imported_ts <- SMap.add name t cx.imported_ts let add_module cx name tvar = cx.modulemap <- SMap.add name tvar cx.modulemap let add_property_map cx id pmap = diff --git a/src/typing/context.mli b/src/typing/context.mli index f4029088fcb..629b1c5778f 100644 --- a/src/typing/context.mli +++ b/src/typing/context.mli @@ -8,6 +8,8 @@ * *) +module Ast = Spider_monkey_ast + type env = Scope.t list type t @@ -61,6 +63,8 @@ val find_module: t -> string -> Type.t val find_tvar_reason: t -> Constraint.ident -> Reason.t val globals: t -> SSet.t val graph: t -> Constraint.node IMap.t +val import_stmts: t -> Ast.Statement.ImportDeclaration.t list +val imported_ts: t -> Type.t SMap.t val is_checked: t -> bool val is_verbose: t -> bool val is_weak: t -> bool @@ -93,6 +97,8 @@ val add_env: t -> int -> env -> unit val add_error: t -> Errors.error -> unit val add_error_suppression: t -> Loc.t -> unit val add_global: t -> string -> unit +val add_import_stmt: t -> Ast.Statement.ImportDeclaration.t -> unit +val add_imported_t: t -> string -> Type.t -> unit val add_module: t -> string -> Type.t -> unit val add_property_map: t -> Constraint.ident -> Type.properties -> unit val add_require: t -> string -> Loc.t -> unit diff --git a/src/typing/flow_js.ml b/src/typing/flow_js.ml index b33c4c5343b..eeb27853de9 100644 --- a/src/typing/flow_js.ml +++ b/src/typing/flow_js.ml @@ -8061,6 +8061,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 @@ -8074,13 +8075,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 -> @@ -8131,6 +8136,14 @@ end = struct let prot_members = extract_members_as_map cx proto_t in let members = find_props cx flds in Success (AugmentableSMap.augment prot_members ~with_bindings:members) + | ModuleT (_, {exports_tmap; cjs_export; has_every_named_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 702ceceddbe..9b90a2f0d24 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/src/typing/statement.ml b/src/typing/statement.ml index af7437d781b..e5c297622e8 100644 --- a/src/typing/statement.ml +++ b/src/typing/statement.ml @@ -1729,6 +1729,7 @@ and statement cx = Ast.Statement.( | (import_loc, ImportDeclaration import_decl) -> let open ImportDeclaration in + Context.add_import_stmt cx import_decl; let module_name = ( match (snd import_decl.source).Ast.Literal.value with @@ -1757,6 +1758,7 @@ and statement cx = Ast.Statement.( else ImportNamedT (get_reason, import_kind, remote_export_name, t) in + Context.add_imported_t cx local_name t; Flow.flow cx (module_t, import_type) ) in @@ -1844,6 +1846,7 @@ and statement cx = Ast.Statement.( in let module_ns_typeof = Flow.mk_tvar_where cx bind_reason (fun t -> + Context.add_imported_t cx local_name t; Flow.flow cx (module_ns_t, ImportTypeofT (bind_reason, "*", t)) ) @@ -1856,6 +1859,7 @@ and statement cx = Ast.Statement.( let module_ns_t = import_ns cx reason module_name (fst import_decl.source) in + Context.add_imported_t cx local_name module_ns_t; let bind_reason = mk_reason import_reason_str (fst local) in (bind_reason, local_name, module_ns_t) ) diff --git a/src/typing/type_inference_js.ml b/src/typing/type_inference_js.ml index 3c5e8dde14b..444c195f08e 100644 --- a/src/typing/type_inference_js.ml +++ b/src/typing/type_inference_js.ml @@ -72,7 +72,9 @@ let infer_ast ~metadata ~filename ~module_name ast = let _, statements, comments = ast in - let cx = Flow_js.fresh_context metadata filename module_name in + let cx = + Flow_js.fresh_context metadata filename module_name + in let checked = Context.is_checked cx in let exported_module_name = Modulename.to_string module_name in 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..1b9d313e7c0 --- /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: 'stderr', args: [formatIfJSON(actual)] }; + return simpleDiffAssertion( + expected, + actual, + assertLoc, + reason, + "stderr", + suggestion, + ); + }; +}