diff --git a/src/flow_dot_js.ml b/src/flow_dot_js.ml index 3c0ae2c7f53..0593579443f 100644 --- a/src/flow_dot_js.ml +++ b/src/flow_dot_js.ml @@ -304,23 +304,23 @@ let mk_loc file line col = } let infer_type filename content line col = - let filename = File_key.SourceFile filename in - let root = Path.dummy_path in - match parse_content filename content with - | Error _ -> failwith "parse error" - | Ok (ast, file_sig) -> - let file_sig = File_sig.abstractify_locs file_sig in - let cx, typed_ast = infer_and_merge ~root filename ast file_sig in - let file = Context.file cx in - let loc = mk_loc filename line col in Query_types.( - let result = type_at_pos_type ~full_cx:cx ~file ~file_sig ~expand_aliases:false - ~omit_targ_defaults:false ~typed_ast loc in - match result with - | FailureNoMatch -> Loc.none, Error "No match" - | FailureUnparseable (loc, _, _) -> loc, Error "Unparseable" - | Success (loc, t) -> - loc, Ok (Ty_printer.string_of_t ~force_single_line:true t) - ) + let filename = File_key.SourceFile filename in + let root = Path.dummy_path in + match parse_content filename content with + | Error _ -> failwith "parse error" + | Ok (ast, file_sig) -> + let file_sig = File_sig.abstractify_locs file_sig in + let cx, typed_ast = infer_and_merge ~root filename ast file_sig in + let file = Context.file cx in + let loc = mk_loc filename line col in Query_types.( + let result = type_at_pos_type ~full_cx:cx ~file ~file_sig ~expand_aliases:false + ~omit_targ_defaults:false ~typed_ast loc in + match result with + | FailureNoMatch -> Loc.none, Error "No match" + | FailureUnparseable (loc, _, _) -> loc, Error "Unparseable" + | Success (loc, t) -> + loc, Ok (Ty_printer.string_of_t ~force_single_line:true t) + ) let types_to_json types ~strip_root = let open Hh_json in @@ -336,21 +336,96 @@ let types_to_json types ~strip_root = ) in JSON_Array types_json +let coverage_to_json ~strip_root ~trust (types : (Loc.t * Coverage_response.expression_coverage) list) content = + let accum_coverage (untainted, tainted, empty, total) (_loc, cov) = + match cov with + | Coverage_response.Uncovered -> (untainted, tainted, empty, total + 1) + | Coverage_response.Empty -> (untainted, tainted, empty + 1, total + 1) + | Coverage_response.Untainted -> (untainted + 1, tainted, empty, total + 1) + | Coverage_response.Tainted -> (untainted, tainted + 1, empty, total + 1) + in + + let accum_coverage_locs (untainted, tainted, empty, uncovered) (loc, cov) = + match cov with + | Coverage_response.Uncovered -> (untainted, tainted, empty, loc::uncovered) + | Coverage_response.Empty -> (untainted, tainted, loc::empty, loc::uncovered) + | Coverage_response.Untainted -> (loc::untainted, tainted, empty, uncovered) + | Coverage_response.Tainted -> (untainted, loc::tainted, empty, uncovered) + in + + let offset_table = lazy (Offset_utils.make content) in + let untainted, tainted, empty, total = + Core_list.fold_left ~f:accum_coverage ~init:(0, 0, 0, 0) types in + + (* In trust mode, we only consider untainted locations covered. In normal mode we consider both *) + let covered = if trust then untainted else untainted + tainted in + (* let percent = if total = 0 then 100. else (float_of_int covered /. float_of_int total) *. 100. in *) + + let offset_table = Some (Lazy.force offset_table) in + let untainted_locs, tainted_locs, empty_locs, uncovered_locs = + let untainted, tainted, empty, uncovered = + Core_list.fold_left ~f:accum_coverage_locs ~init:([], [], [], []) types + in + Core_list.rev untainted, Core_list.rev tainted, Core_list.rev empty, Core_list.rev uncovered + in + let open Hh_json in + let open Reason in + let covered_data = if trust then + [ + "untainted_count", int_ untainted; + "untainted_locs", JSON_Array (Core_list.map ~f:(json_of_loc ~strip_root ~offset_table) untainted_locs); + "tainted_count", int_ tainted; + "tainted_locs", JSON_Array (Core_list.map ~f:(json_of_loc ~strip_root ~offset_table) tainted_locs); + ] + else + let covered_locs = untainted_locs @ tainted_locs |> Core_list.sort ~cmp:compare in + [ + "covered_count", int_ covered; + "covered_locs", JSON_Array (Core_list.map ~f:(json_of_loc ~strip_root ~offset_table) covered_locs); + ] + in + JSON_Object [ + "expressions", JSON_Object (covered_data @ [ + "uncovered_count", int_ (total - covered); + "uncovered_locs", JSON_Array (Core_list.map ~f:(json_of_loc ~strip_root ~offset_table) uncovered_locs); + "empty_count", int_ empty; + "empty_locs", JSON_Array (Core_list.map ~f:(json_of_loc ~strip_root ~offset_table) empty_locs); + ]); + ] + let dump_types js_file js_content = - let filename = File_key.SourceFile (Js.to_string js_file) in - let root = Path.dummy_path in - let content = Js.to_string js_content in - match parse_content filename content with - | Error _ -> failwith "parse error" - | Ok (ast, file_sig) -> - let file_sig = File_sig.abstractify_locs file_sig in - let cx, typed_ast = infer_and_merge ~root filename ast file_sig in - let printer = Ty_printer.string_of_t in - let types = Query_types.dump_types ~printer cx file_sig typed_ast in - let strip_root = None in - let types_json = types_to_json types ~strip_root in - - js_of_json types_json + let filename = File_key.SourceFile (Js.to_string js_file) in + let root = Path.dummy_path in + let content = Js.to_string js_content in + match parse_content filename content with + | Error _ -> failwith "parse error" + | Ok (ast, file_sig) -> + let file_sig = File_sig.abstractify_locs file_sig in + let cx, typed_ast = infer_and_merge ~root filename ast file_sig in + let printer = Ty_printer.string_of_t in + let types = Query_types.dump_types ~printer cx file_sig typed_ast in + let strip_root = None in + let types_json = types_to_json types ~strip_root in + + js_of_json types_json + +let coverage js_file js_content = + let filename = File_key.SourceFile (Js.to_string js_file) in + let root = Path.dummy_path in + let content = Js.to_string js_content in + match parse_content filename content with + | Error _ -> + Js.raise_js_error (Js.Unsafe.new_obj Js.error_constr [| + Js.Unsafe.inject (Js.string "parse error") + |]) + | Ok (ast, file_sig) -> + let file_sig = File_sig.abstractify_locs file_sig in + let cx, typed_ast = infer_and_merge ~root filename ast file_sig in + let types = Query_types.covered_types ~should_check:true ~check_trust:false cx typed_ast in + let strip_root = None in + let coverage_json = coverage_to_json types content ~trust:false ~strip_root in + + js_of_json coverage_json let type_at_pos js_file js_content js_line js_col = let filename = Js.to_string js_file in @@ -381,6 +456,8 @@ let () = Js.Unsafe.set exports "checkContent" (Js.wrap_callback check_content_js) let () = Js.Unsafe.set exports "dumpTypes" (Js.wrap_callback dump_types) +let () = Js.Unsafe.set exports + "coverage" (Js.wrap_callback coverage) let () = Js.Unsafe.set exports "jsOfOcamlVersion" (Js.string Sys_js.js_of_ocaml_version) let () = Js.Unsafe.set exports diff --git a/website/_assets/css/_try.scss b/website/_assets/css/_try.scss index 9362042ee7e..32563cb1c8f 100644 --- a/website/_assets/css/_try.scss +++ b/website/_assets/css/_try.scss @@ -12,6 +12,23 @@ html.site-fullscreen .content { position: relative; height: 100%; + .options { + position: absolute; + right: 50%; + z-index: 3; + + label { + cursor: pointer; + float: left; + font-size: 14px; + padding: 7px 15px; + + input { + margin-right: 8px; + } + } + } + .code, .results { position: absolute; @@ -163,17 +180,26 @@ html.site-fullscreen .content { } @-webkit-keyframes sk-bouncedelay { - 0%, 80%, 100% { -webkit-transform: scale(0) } - 40% { -webkit-transform: scale(1.0) } + 0%, + 80%, + 100% { + -webkit-transform: scale(0); + } + 40% { + -webkit-transform: scale(1); + } } @keyframes sk-bouncedelay { - 0%, 80%, 100% { + 0%, + 80%, + 100% { -webkit-transform: scale(0); transform: scale(0); - } 40% { - -webkit-transform: scale(1.0); - transform: scale(1.0); + } + 40% { + -webkit-transform: scale(1); + transform: scale(1); } } @@ -205,3 +231,90 @@ html.site-fullscreen .content { width: 100%; } } + +/* New images */ + +.CodeMirror-lint-mark-error { + background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%206%203'%20enable-background%3D'new%200%200%206%203'%20height%3D'3'%20width%3D'6'%3E%3Cg%20fill%3D'%23d60a0a'%3E%3Cpolygon%20points%3D'5.5%2C0%202.5%2C3%201.1%2C3%204.1%2C0'%2F%3E%3Cpolygon%20points%3D'4%2C0%206%2C2%206%2C0.6%205.4%2C0'%2F%3E%3Cpolygon%20points%3D'0%2C2%201%2C3%202.4%2C3%200%2C0.6'%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E"); +} + +.CodeMirror-lint-mark-warning { + background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%206%203'%20enable-background%3D'new%200%200%206%203'%20height%3D'3'%20width%3D'6'%3E%3Cg%20fill%3D'%23117711'%3E%3Cpolygon%20points%3D'5.5%2C0%202.5%2C3%201.1%2C3%204.1%2C0'%2F%3E%3Cpolygon%20points%3D'4%2C0%206%2C2%206%2C0.6%205.4%2C0'%2F%3E%3Cpolygon%20points%3D'0%2C2%201%2C3%202.4%2C3%200%2C0.6'%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E"); +} + +.CodeMirror-lint-mark-info { + background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%206%203'%20enable-background%3D'new%200%200%206%203'%20height%3D'3'%20width%3D'6'%3E%3Cg%20fill%3D'%23008000'%3E%3Cpolygon%20points%3D'5.5%2C0%202.5%2C3%201.1%2C3%204.1%2C0'%2F%3E%3Cpolygon%20points%3D'4%2C0%206%2C2%206%2C0.6%205.4%2C0'%2F%3E%3Cpolygon%20points%3D'0%2C2%201%2C3%202.4%2C3%200%2C0.6'%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E"); +} + +/* The lint marker gutter */ + +.CodeMirror-lint-markers { + width: 16px; +} + +.CodeMirror-lint-tooltip { + background-color: #ffd; + border: 1px solid black; + border-radius: 4px 4px 4px 4px; + color: black; + font-family: monospace; + font-size: 10pt; + overflow: hidden; + padding: 2px 5px; + position: fixed; + white-space: pre; + white-space: pre-wrap; + z-index: 100; + max-width: 600px; + opacity: 0; + transition: opacity 0.4s; + -moz-transition: opacity 0.4s; + -webkit-transition: opacity 0.4s; + -o-transition: opacity 0.4s; + -ms-transition: opacity 0.4s; +} + +.CodeMirror-lint-mark-error, +.CodeMirror-lint-mark-warning, +.CodeMirror-lint-mark-info { + display: inline-block; + background-position: left bottom; + background-repeat: repeat-x; +} + +.CodeMirror-lint-marker-error, +.CodeMirror-lint-marker-warning { + background-position: center center; + background-repeat: no-repeat; + cursor: pointer; + display: inline-block; + height: 16px; + width: 16px; + vertical-align: middle; + position: relative; +} + +.CodeMirror-lint-message-error, +.CodeMirror-lint-message-warning { + padding-left: 18px; + background-position: top left; + background-repeat: no-repeat; +} + +.CodeMirror-lint-marker-error, +.CodeMirror-lint-message-error { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII="); +} + +.CodeMirror-lint-marker-warning, +.CodeMirror-lint-message-warning { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII="); +} + +.CodeMirror-lint-marker-multiple { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC"); + background-repeat: no-repeat; + background-position: right bottom; + width: 100%; + height: 100%; +} diff --git a/website/_assets/js/tryFlow.js.es6.liquid b/website/_assets/js/tryFlow.js.es6.liquid index 8b57fd90786..6ca28d0056c 100644 --- a/website/_assets/js/tryFlow.js.es6.liquid +++ b/website/_assets/js/tryFlow.js.es6.liquid @@ -8,9 +8,7 @@ import 'codemirror/mode/jsx/jsx'; import * as LZString from 'lz-string'; import {load as initFlowLocally} from 'flow-loader'; -CodeMirror.defineOption('flow', null, function(editor) { - editor.performLint(); -}); +CodeMirror.defineOption('flow', null); function appendMsg(container, msg, editor) { const clickHandler = (msg) => { @@ -105,13 +103,42 @@ function removeChildren(node) { while (node.lastChild) node.removeChild(node.lastChild); } +function coverage(text, flow, isCoverageEnabled, editor) { + const isCoverageEnabled = editor.getOption('coverageEnabled'); + if (isCoverageEnabled) { + return flow + .then(flowProxy => flowProxy.coverage('-', text)) + .then(coverage => { + CodeMirror.signal(editor, 'flowCoverage', coverage); + return coverage.expressions.uncovered_locs + }) + .catch(err => { + console.error(err) + return [] + }) + } + return Promise.resolve([]) +} + +function checkContent(text, flow, editor) { + return flow + .then(flowProxy => flowProxy.checkContent('-', text)) + .then(errors => { + CodeMirror.signal(editor, 'flowErrors', errors); + return errors + }) +} + function getAnnotations(text, callback, options, editor) { - Promise.resolve(editor.getOption('flow')) - .then(flowProxy => flowProxy.checkContent('-', text)) - .then(errors => { - CodeMirror.signal(editor, 'flowErrors', errors); + const flow = Promise.resolve(editor.getOption('flow')); - var lint = errors.map(function(err) { + const sources = Promise.all([ + coverage(text, flow, editor), + checkContent(text, flow, editor), + ]); + + sources.then(([coverage, errors]) => { + const errorsLint = errors.map(function(err) { var messages = err.message; var firstLoc = messages[0].loc; var message = messages.map(function(msg) { @@ -127,7 +154,21 @@ function getAnnotations(text, callback, options, editor) { message: message }; }); - callback(lint); + const coverageLint = coverage.map(function (loc) { + return { + from: CodeMirror.Pos( + loc.start.line - 1, + loc.start.column - 1, + ), + to: CodeMirror.Pos(loc.end.line - 1, loc.end.column), + severity: 'info', + message: 'Not covered by flow' + } + }); + callback([ + ...errorsLint, + ...coverageLint, + ]); }); } getAnnotations.async = true; @@ -217,6 +258,17 @@ class AsyncLocalFlow { return Promise.resolve(this._flow.checkContent(filename, body)); } + coverage(filename, body) { + if (this._flow.coverage) { + return Promise.resolve(this._flow.coverage(filename, body)) + } + return Promise.reject(Error('coverage method is missing')) + } + + dumpTypes(filename, body) { + return Promise.resolve(this._flow.dumpTypes(filename, body)) + } + typeAtPos(filename, body, line, col) { return Promise.resolve(this._flow.typeAtPos(filename, body, line, col)); } @@ -239,6 +291,14 @@ class AsyncWorkerFlow { return this._worker.send({ type: 'checkContent', filename, body }); } + coverage(filename, body) { + return this._worker.send({ type: 'coverage', filename, body }) + } + + dumpTypes(filename, body) { + return this._worker.send({ type: 'dumpTypes', filename, body }) + } + typeAtPos(filename, body, line, col) { return this._worker.send({ type: 'typeAtPos', filename, body, line, col }); } @@ -263,6 +323,7 @@ function initFlow(version) { function createEditor( flowVersion, + optionsNode, domNode, resultsNode, flowVersions @@ -348,6 +409,14 @@ function createEditor( resultsNode.className += " show-errors"; + const coverageOption = document.createElement('label'); + const coverageCheckbox = document.createElement('input'); + coverageCheckbox.type = 'checkbox'; + coverageOption.appendChild(coverageCheckbox); + coverageOption.appendChild(document.createTextNode(' coverage')); + + optionsNode.appendChild(coverageOption); + const cursorPositionNode = document.querySelector('footer .cursor-position'); const typeAtPosNode = document.querySelector('footer .type-at-pos'); @@ -383,7 +452,8 @@ function createEditor( typeAtPosNode.title = typeAtPos ? typeAtPos.c : ''; typeAtPosNode.textContent = typeAtPos ? typeAtPos.c : ''; }) - .catch(() => { + .catch((err) => { + console.error(err); typeAtPosNode.title = ''; typeAtPosNode.textContent = ''; }); @@ -451,6 +521,12 @@ function createEditor( }); editor.setOption('flow', flowProxy); }); + + coverageCheckbox.addEventListener('change', function(evt) { + const checked = evt.target.checked; + editor.setOption('coverageEnabled', checked); + editor.performLint(); + }); }); } diff --git a/website/_assets/js/tryFlowWorker.js b/website/_assets/js/tryFlowWorker.js index 7ceb59be461..9a9fdc07ae2 100644 --- a/website/_assets/js/tryFlowWorker.js +++ b/website/_assets/js/tryFlowWorker.js @@ -19,11 +19,30 @@ this.onmessage = function(e) { case "checkContent": getFlow(data.version).then(function(flow) { var result = flow.checkContent(data.filename, data.body); - postMessage({id: data.id, type: "checkContent", result: result }); + postMessage({id: data.id, type: "checkContent", result: result}); })["catch"](function (e) { postMessage({id: data.id, type: "checkContent", err: e}); }); return; + case "dumpTypes": + getFlow(data.version).then(function(flow) { + var result = flow.dumpTypes(data.filename, data.body); + postMessage({id: data.id, type: "dumpTypes", result: result}); + })["catch"](function (e) { + postMessage({id: data.id, type: "dumpTypes", err: e}); + }); + return; + case "coverage": + getFlow(data.version).then(function(flow) { + if (flow.coverage) { + var result = flow.coverage(data.filename, data.body); + postMessage({id: data.id, type: "coverage", result: result}); + } else { + postMessage({id: data.id, type: "coverage", err: Error('coverage method is missing')}); + } + })["catch"](function (e) { + postMessage({id: data.id, type: "coverage", err: e}); + }) case "typeAtPos": getFlow(data.version).then(function(flow) { var result = flow.typeAtPos( diff --git a/website/_layouts/default.html b/website/_layouts/default.html index d371f4535f3..fdcc7750ca7 100644 --- a/website/_layouts/default.html +++ b/website/_layouts/default.html @@ -26,7 +26,6 @@ {% endif %} {% if page.codemirror or layout.codemirror %} - {% endif %}