diff --git a/graph/javascript/engine/call-edge-generation/calls.dl b/graph/javascript/engine/call-edge-generation/calls.dl index 39dbe789..5745b29d 100644 --- a/graph/javascript/engine/call-edge-generation/calls.dl +++ b/graph/javascript/engine/call-edge-generation/calls.dl @@ -170,6 +170,29 @@ call_chain_edge(ce, caller, "-", m, "client", "multi_inferred", kind) :- wrapper_call_target(ce, m), !call_over_cap(ce), call_from(ce, caller), invocation_site(ce, kind). variable_reassigned(v) :- expr_kind(_, "ASSIGNMENT", _, a), expr_child(_, a, "ASSIGNMENT_TARGET", _, tgt), expr_binding(_, v, tgt). +// ── a call of what a PROJECT wrapper returned runs what that site was handed ── +// `export default promisable(eachItem, 3)`, `const add = curry2(function add(a, b) {…})`: +// the wrapper returns a closure that calls its parameter, and every export of the module +// is that closure. The closure's call of the parameter is read context-insensitively, so +// it held every function any of the wrapper's sites passed (past the fan cap: nothing), +// and a test calling the export reached the closure and stopped there. The site is +// already a value of its own, ("wrap", site), beside the closure (value-flow.dl), and a +// call through it takes its RESULT from that site's argument alone; the same site says +// what the call RUNS: the function it was handed at the forwarded position, what the +// wrapper writes into that parameter, or what a wrapped argument itself runs +// (`memoize(once(f))`). One of a set, beside the edge to the closure the call keeps, and +// under the same fan cap: a callee that may hold more wrapped values than --dispatch-cap +// (a parameter every wrapped export is passed to) runs one of them, and naming all of +// them says nothing. +wrap_runs(s, g) :- wrap_site_arg(s, _, arg), expr_value(arg, "func", g). +wrap_runs(s, g) :- wrap_site_arg(s, _, arg), expr_value(arg, "wrap", s2), wrap_runs(s2, g). +wrap_runs(s, g) :- wrap_site(s, h, pos), wrapper_param_written(h, pos, o), expr_value(o, "func", g). +wrapped_call_target(ce, g) :- callee_value(ce, "wrap", s), wrap_runs(s, g), method_prov(g, "client"), + !call_site(_, "FUNCTION_CALL_BIND", _, _, _, _, ce, _, _). +wrapped_target_count(ce, n) :- wrapped_call_target(ce, _), n = count : { wrapped_call_target(ce, _) }. +wrapped_over_cap(ce) :- wrapped_target_count(ce, n), dispatch_cap(c), n > to_number(c). +call_chain_edge(ce, caller, "-", g, "client", "multi_inferred", kind) :- + wrapped_call_target(ce, g), !call_over_cap(ce), !wrapped_over_cap(ce), call_from(ce, caller), invocation_site(ce, kind). // `(c ? a : b)()`, `(0, cb)()`, `make()()`: the callee is computed by an expression. // `new Function(s)()` / `Function(s)()` runs code built at run time. unresolved_value_callee(ce, "dynamic_code") :- value_callee_unresolved(ce, c), callee_builds_code(c). diff --git a/graph/javascript/engine/resolution/fan-cap.dl b/graph/javascript/engine/resolution/fan-cap.dl index af6bdd93..ad69acdb 100644 --- a/graph/javascript/engine/resolution/fan-cap.dl +++ b/graph/javascript/engine/resolution/fan-cap.dl @@ -80,14 +80,48 @@ direct_export_func(mod, n, m) :- export_decl(_, n, _, _, _, _, _, se, mod, _), s // shares its name with hundreds of unrelated sites and must not be capped by them, // while a module-level `each` or `merge` called under its own name from everywhere // is exactly the generic utility the cap exists for. +// +// The PROGRAM's arguments are decided by the program's sites alone. A suite calls the +// API it tests over and over, and counted with the program those calls made exactly the +// functions under test "generic utilities": their parameters went untracked, so a +// callback the program hands a constructor and stores on the instance (`this.decode = +// options.decode`) was lost, and the program's own call through that field resolved only +// to the fallback beside it. So there are two verdicts: +// method_is_hot every site counts (as before): decides whether a TEST's +// arguments flow in +// method_is_hot_in_program test files' sites do not count: decides whether the +// PROGRAM's arguments flow in +// Nothing that flowed before stops flowing (a test handing the program a plugin it calls +// back still reaches it). A function hot only because of its tests now receives what the +// program passes it and still nothing from the tests: their callbacks would be the widest +// values it holds, and with them the call through the stored field would be over the +// dispatch cap and answer nothing. param_flow_capped_at(n) :- dispatch_cap(s), n = to_number(s). direct_call_count(m, n) :- direct_call_site(_, m), n = count : { direct_call_site(_, m) }. method_is_hot(m, n) :- direct_call_count(m, n), param_flow_capped_at(c), n > c. name_call_count(cn, n) :- call_site(_, _, cn, _, _, _, _, _, _), cn != "", n = count : { call_site(_, _, cn, _, _, _, _, _, _) }. +program_direct_site(ce, m) :- direct_call_site(ce, m), !site_in_test_module(ce). +program_direct_count(m, n) :- program_direct_site(_, m), n = count : { program_direct_site(_, m) }. +method_is_hot_in_program(m, n) :- program_direct_count(m, n), param_flow_capped_at(c), n > c. +// The NAME count is the overcount for module-level functions. A test's member call that +// only shares the name (`lib.add(1, 2)` on an export that is a different function, a +// curried closure) stays in it, as before; a test's bare call or `new` of the name +// (`build(...)`, `new lib.Codec(...)`) denotes the function itself and leaves it. +test_names_itself(ce) :- site_in_test_module(ce), call_site(_, ck, _, _, _, _, ce, _, _), call_kind_is_callee_form(ck). +test_names_itself(ce) :- site_in_test_module(ce), call_site(_, "CONSTRUCTOR_CALL", _, _, _, _, ce, _, _). +program_name_site(m, ce) :- module_level_function(m, cn), call_site(_, _, cn, _, _, _, ce, _, _), !test_names_itself(ce). +program_name_count(m, n) :- program_name_site(m, _), n = count : { program_name_site(m, _) }. +// ── test_module / site_in_test_module ────────────── +// By path, as runners find their files: a `test`/`tests`/`__tests__`/`spec(s)` directory, +// or a `.test.` / `.spec.` file. +test_module(mod) :- module_file("client", fp, mod), + match("(.*/)?(tests?|__tests__|specs?)/.*|.*[.](test|spec)[.][cm]?[jt]sx?", fp). +site_in_test_module(ce) :- expr_owner(_, _, mod, ce), test_module(mod). module_level_function(m, n) :- method_decl(_, n, "FUNCTION_DECLARATION", _, _, "", mod, m), method_scopes(_, _, _, init, m), module_init(_, init, mod). module_level_function(m, n) :- var_decl(_, n, _, _, _, mod, v), function_binding_value(v, m), method_decl(_, _, _, _, _, "", mod, m), method_scopes(_, _, _, init, m), module_init(_, init, mod). method_is_hot(m, n) :- module_level_function(m, cn), name_call_count(cn, n), param_flow_capped_at(c), n > c. +method_is_hot_in_program(m, n) :- program_name_count(m, n), param_flow_capped_at(c), n > c. // ── param_fan_capped(ParamHash, SiteCount) — countable ────────────────────── -param_fan_capped(p, n) :- method_is_hot(m, n), param_decl(_, _, _, m, p). +param_fan_capped(p, n) :- method_is_hot_in_program(m, n), param_decl(_, _, _, m, p). diff --git a/graph/javascript/engine/resolution/value-flow.dl b/graph/javascript/engine/resolution/value-flow.dl index 2c79e52b..e78863e2 100644 --- a/graph/javascript/engine/resolution/value-flow.dl +++ b/graph/javascript/engine/resolution/value-flow.dl @@ -264,8 +264,15 @@ param_value(p, "inst", t) :- param_type_name(_, _, tr, p), type_ref_type(tr, t). // values on every export, a 20-minute solve — because a generic library is exactly // the shape whose parameters unify everything. What a library does with a callback it // is handed is a MODEL (resolution/frameworks.dl), stated per library, not inferred. -param_value(p, k, i) :- call_arg(ce, pos, arg), expr_resolves_to_method(ce, m), !method_is_hot(m, _), - method_decl("client", _, _, _, _, _, _, m), +// The program's sites pass their arguments unless the PROGRAM calls the function from +// more places than the cap; a test file's sites only while every site together stays +// under it (fan-cap.dl). +param_value(p, k, i) :- call_arg(ce, pos, arg), expr_resolves_to_method(ce, m), !site_in_test_module(ce), + !method_is_hot_in_program(m, _), method_decl("client", _, _, _, _, _, _, m), + param_decl(_, _, ppos, m, p), to_number(ppos) = pos, !param_is_rest(_, p), + expr_value(arg, k, i). +param_value(p, k, i) :- call_arg(ce, pos, arg), expr_resolves_to_method(ce, m), site_in_test_module(ce), + !method_is_hot(m, _), method_decl("client", _, _, _, _, _, _, m), param_decl(_, _, ppos, m, p), to_number(ppos) = pos, !param_is_rest(_, p), expr_value(arg, k, i). // THE ONE LIBRARY EXCEPTION: a UMD wrapper (#710). `(function (root, factory) { @@ -283,8 +290,12 @@ param_value(p, "func", f) :- call_site(_, "IIFE_CALL", _, _, _, _, ce, _, _), ca expr_value(arg, "func", f). // A rest parameter IS an array; the arguments at and past its position are its elements. param_value(p, "arr", p) :- param_is_rest(_, p). -elem_value(p, k, i) :- call_arg(ce, pos, arg), expr_resolves_to_method(ce, m), !method_is_hot(m, _), - method_decl("client", _, _, _, _, _, _, m), +elem_value(p, k, i) :- call_arg(ce, pos, arg), expr_resolves_to_method(ce, m), !site_in_test_module(ce), + !method_is_hot_in_program(m, _), method_decl("client", _, _, _, _, _, _, m), + param_decl(_, _, ppos, m, p), param_is_rest(_, p), to_number(ppos) <= pos, + expr_value(arg, k, i). +elem_value(p, k, i) :- call_arg(ce, pos, arg), expr_resolves_to_method(ce, m), site_in_test_module(ce), + !method_is_hot(m, _), method_decl("client", _, _, _, _, _, _, m), param_decl(_, _, ppos, m, p), param_is_rest(_, p), to_number(ppos) <= pos, expr_value(arg, k, i). // A default value: `cb = () => {}`. The parser gives the parameter the default's diff --git a/graph/javascript/souffle/decls_all.dl b/graph/javascript/souffle/decls_all.dl index b7b56647..bc489cb6 100644 --- a/graph/javascript/souffle/decls_all.dl +++ b/graph/javascript/souffle/decls_all.dl @@ -112,6 +112,10 @@ .decl wrapper_holder_call(c0:symbol, c1:symbol) .decl wrapper_runs(c0:symbol, c1:symbol) .decl wrapper_call_target(c0:symbol, c1:symbol) +.decl wrap_runs(c0:symbol, c1:symbol) +.decl wrapped_call_target(c0:symbol, c1:symbol) +.decl wrapped_target_count(c0:symbol, c1:number) +.decl wrapped_over_cap(c0:symbol) .decl live_export_variable(c0:symbol, c1:symbol) .decl this_type_open(c0:symbol) @@ -462,6 +466,14 @@ .decl param_fan_capped(c0:symbol, c1:number) .decl import_binding_is_default_like(c0:symbol) .decl name_call_count(c0:symbol, c1:number) +.decl test_module(c0:symbol) +.decl site_in_test_module(c0:symbol) +.decl program_direct_site(c0:symbol, c1:symbol) +.decl program_direct_count(c0:symbol, c1:number) +.decl method_is_hot_in_program(c0:symbol, c1:number) +.decl test_names_itself(c0:symbol) +.decl program_name_site(c0:symbol, c1:symbol) +.decl program_name_count(c0:symbol, c1:number) .decl module_level_function(c0:symbol, c1:symbol) .decl ambient_collection(c0:symbol) .decl coll_method_call(c0:symbol, c1:symbol, c2:symbol) diff --git a/graph/test/javascript/expected/64-memoize-wrapper-result.edges b/graph/test/javascript/expected/64-memoize-wrapper-result.edges index c3d46e25..46a76a11 100644 --- a/graph/test/javascript/expected/64-memoize-wrapper-result.edges +++ b/graph/test/javascript/expected/64-memoize-wrapper-result.edges @@ -1,6 +1,8 @@ app.js:11:3 FUNCTION_CALL getAlpha -> known_edge memoize.js:5:10 +app.js:11:3 FUNCTION_CALL getAlpha -> multi_inferred app.js:7:26 app.js:11:3 METHOD_CALL getAlpha().getHooks -> known_edge alpha.js:2:3 getHooks app.js:12:3 FUNCTION_CALL getBeta -> known_edge memoize.js:5:10 +app.js:12:3 FUNCTION_CALL getBeta -> multi_inferred app.js:8:25 app.js:12:3 METHOD_CALL getBeta().getHooks -> known_edge beta.js:2:3 getHooks app.js:16:20 FUNCTION_CALL once -> callback_registered app.js:16:25 app.js:16:20 FUNCTION_CALL once -> known_edge once.js:2:1 once @@ -15,12 +17,16 @@ app.js:19:19 FUNCTION_CALL lazy -> callback_registered app.js:19:24 app.js:19:19 FUNCTION_CALL lazy -> known_edge once.js:14:1 lazy app.js:19:30 CONSTRUCTOR_CALL AlphaPlugin -> implicit_constructor - app.js:22:3 FUNCTION_CALL firstAlpha -> known_edge once.js:5:10 +app.js:22:3 FUNCTION_CALL firstAlpha -> multi_inferred app.js:16:25 app.js:22:3 METHOD_CALL firstAlpha().run -> known_edge alpha.js:3:3 run app.js:23:3 FUNCTION_CALL makeBeta -> known_edge once.js:15:10 +app.js:23:3 FUNCTION_CALL makeBeta -> multi_inferred app.js:17:23 app.js:23:3 METHOD_CALL makeBeta().run -> known_edge beta.js:3:3 run app.js:24:3 FUNCTION_CALL firstBeta -> known_edge once.js:5:10 +app.js:24:3 FUNCTION_CALL firstBeta -> multi_inferred app.js:18:24 app.js:24:3 METHOD_CALL firstBeta().run -> known_edge beta.js:3:3 run app.js:25:3 FUNCTION_CALL makeAlpha -> known_edge once.js:15:10 +app.js:25:3 FUNCTION_CALL makeAlpha -> multi_inferred app.js:19:24 app.js:25:3 METHOD_CALL makeAlpha().run -> known_edge alpha.js:3:3 run app.js:29:18 FUNCTION_CALL constant -> known_edge once.js:18:1 constant app.js:29:27 CONSTRUCTOR_CALL BetaPlugin -> implicit_constructor - @@ -57,10 +63,14 @@ declared.js:27:16 FUNCTION_CALL pickShared -> callback_registered declared.js declared.js:27:16 FUNCTION_CALL pickShared -> known_edge declared.js:20:1 pickShared declared.js:27:33 CONSTRUCTOR_CALL BetaPlugin -> implicit_constructor - declared.js:30:3 FUNCTION_CALL declAlpha -> known_edge declared.js:9:3 inner +declared.js:30:3 FUNCTION_CALL declAlpha -> multi_inferred declared.js:22:28 declared.js:30:3 METHOD_CALL declAlpha().run -> known_edge alpha.js:3:3 run declared.js:31:3 FUNCTION_CALL declBeta -> known_edge declared.js:9:3 inner +declared.js:31:3 FUNCTION_CALL declBeta -> multi_inferred declared.js:23:27 declared.js:31:3 METHOD_CALL declBeta().run -> known_edge beta.js:3:3 run declared.js:32:3 FUNCTION_CALL declOfGetter -> known_edge declared.js:9:3 inner +declared.js:32:3 FUNCTION_CALL declOfGetter -> multi_inferred declared.js:24:26 +declared.js:32:3 FUNCTION_CALL declOfGetter -> multi_inferred memoize.js:5:10 declared.js:32:3 METHOD_CALL declOfGetter().run -> known_edge alpha.js:3:3 run declared.js:33:3 FUNCTION_CALL tappedDecl -> known_edge declared.js:15:3 inner declared.js:33:3 METHOD_CALL tappedDecl().markOnly -> known_edge declared.js:13:14 markOnly @@ -202,24 +212,33 @@ shapes.js:45:27 FUNCTION_CALL memoize -> callback_registered shapes.js:45:35 shapes.js:45:27 FUNCTION_CALL memoize -> known_edge memoize.js:2:17 shapes.js:45:41 CONSTRUCTOR_CALL BetaPlugin -> implicit_constructor - shapes.js:48:3 FUNCTION_CALL getFb -> known_edge shapes.js:15:10 +shapes.js:48:3 FUNCTION_CALL getFb -> multi_inferred shapes.js:36:28 shapes.js:48:3 METHOD_CALL getFb().run -> known_edge alpha.js:3:3 run shapes.js:49:3 FUNCTION_CALL getFb -> known_edge shapes.js:15:10 +shapes.js:49:3 FUNCTION_CALL getFb -> multi_inferred shapes.js:36:28 shapes.js:49:3 METHOD_CALL getFb().fallbackOnly -> known_edge shapes.js:8:18 fallbackOnly shapes.js:50:3 FUNCTION_CALL getOr -> known_edge shapes.js:22:10 +shapes.js:50:3 FUNCTION_CALL getOr -> multi_inferred shapes.js:38:22 shapes.js:50:3 METHOD_CALL getOr().run -> ambient_terminal - shapes.js:50:3 METHOD_CALL getOr().run -> multi_inferred beta.js:3:3 run shapes.js:51:3 FUNCTION_CALL getOr -> known_edge shapes.js:22:10 +shapes.js:51:3 FUNCTION_CALL getOr -> multi_inferred shapes.js:38:22 shapes.js:51:3 METHOD_CALL getOr().mockOnly -> ambient_terminal - shapes.js:51:3 METHOD_CALL getOr().mockOnly -> multi_inferred shapes.js:9:14 mockOnly shapes.js:52:3 FUNCTION_CALL getD -> known_edge shapes.js:29:10 +shapes.js:52:3 FUNCTION_CALL getD -> multi_inferred shapes.js:39:26 shapes.js:52:3 METHOD_CALL getD().run -> known_edge alpha.js:3:3 run shapes.js:53:3 FUNCTION_CALL getD -> known_edge shapes.js:29:10 +shapes.js:53:3 FUNCTION_CALL getD -> multi_inferred shapes.js:39:26 shapes.js:53:3 METHOD_CALL getD().defaultsOnly -> known_edge shapes.js:25:20 defaultsOnly shapes.js:54:3 FUNCTION_CALL getDefault -> known_edge shapes.js:33:44 shapes.js:54:3 METHOD_CALL getDefault().defaultOnly -> known_edge shapes.js:10:19 defaultOnly shapes.js:55:3 FUNCTION_CALL getSpread -> known_edge shapes.js:33:44 shapes.js:55:3 METHOD_CALL getSpread().run -> ambiguous_unknown - shapes.js:56:3 FUNCTION_CALL warm -> known_edge shapes.js:34:32 +shapes.js:56:3 FUNCTION_CALL warm -> multi_inferred shapes.js:43:20 shapes.js:56:3 METHOD_CALL warm().run -> known_edge alpha.js:3:3 run shapes.js:57:3 FUNCTION_CALL getNested -> known_edge memoize.js:5:10 +shapes.js:57:3 FUNCTION_CALL getNested -> multi_inferred memoize.js:5:10 +shapes.js:57:3 FUNCTION_CALL getNested -> multi_inferred shapes.js:45:35 shapes.js:57:3 METHOD_CALL getNested().run -> known_edge beta.js:3:3 run diff --git a/graph/test/javascript/expected/65-wrapper-param-reassigned.edges b/graph/test/javascript/expected/65-wrapper-param-reassigned.edges index d56c695d..8373c4e3 100644 --- a/graph/test/javascript/expected/65-wrapper-param-reassigned.edges +++ b/graph/test/javascript/expected/65-wrapper-param-reassigned.edges @@ -2,51 +2,66 @@ app.js:10:23 CONSTRUCTOR_CALL A -> implicit_constructor - app.js:10:3 FUNCTION_CALL lazyOrDefault -> callback_registered app.js:10:17 app.js:10:3 FUNCTION_CALL lazyOrDefault -> known_edge wrap.js:17:1 lazyOrDefault app.js:10:3 FUNCTION_CALL lazyOrDefault(() => new A()) -> known_edge wrap.js:19:10 +app.js:10:3 FUNCTION_CALL lazyOrDefault(() => new A()) -> multi_inferred app.js:10:17 +app.js:10:3 FUNCTION_CALL lazyOrDefault(() => new A()) -> multi_inferred wrap.js:18:15 app.js:10:3 METHOD_CALL lazyOrDefault(() => new A())().aOnly -> known_edge wrap.js:2:11 aOnly app.js:11:3 FUNCTION_CALL lazyNullish -> known_edge wrap.js:22:1 lazyNullish app.js:11:3 FUNCTION_CALL lazyNullish() -> known_edge wrap.js:24:10 +app.js:11:3 FUNCTION_CALL lazyNullish() -> multi_inferred wrap.js:23:10 app.js:11:3 METHOD_CALL lazyNullish()().dfltOnly -> known_edge wrap.js:4:14 dfltOnly app.js:15:15 CONSTRUCTOR_CALL A -> implicit_constructor - app.js:15:3 FUNCTION_CALL plain -> callback_registered app.js:15:9 app.js:15:3 FUNCTION_CALL plain -> known_edge wrap.js:27:1 plain app.js:15:3 FUNCTION_CALL plain(() => new A()) -> known_edge wrap.js:28:10 +app.js:15:3 FUNCTION_CALL plain(() => new A()) -> multi_inferred app.js:15:9 app.js:15:3 METHOD_CALL plain(() => new A())().aOnly -> known_edge wrap.js:2:11 aOnly app.js:16:15 CONSTRUCTOR_CALL A -> implicit_constructor - app.js:16:3 FUNCTION_CALL plain -> callback_registered app.js:16:9 app.js:16:3 FUNCTION_CALL plain -> known_edge wrap.js:27:1 plain app.js:16:3 FUNCTION_CALL plain(() => new A()) -> known_edge wrap.js:28:10 +app.js:16:3 FUNCTION_CALL plain(() => new A()) -> multi_inferred app.js:16:9 app.js:16:3 METHOD_CALL plain(() => new A())().replacedOnly -> ambiguous_unknown - app.js:17:22 CONSTRUCTOR_CALL A -> implicit_constructor - app.js:17:3 FUNCTION_CALL otherWritten -> callback_registered app.js:17:16 app.js:17:3 FUNCTION_CALL otherWritten -> known_edge wrap.js:31:1 otherWritten app.js:17:3 FUNCTION_CALL otherWritten(() => new A(), null) -> known_edge wrap.js:33:10 +app.js:17:3 FUNCTION_CALL otherWritten(() => new A(), null) -> multi_inferred app.js:17:16 app.js:17:3 METHOD_CALL otherWritten(() => new A(), null)().aOnly -> known_edge wrap.js:2:11 aOnly app.js:18:22 CONSTRUCTOR_CALL A -> implicit_constructor - app.js:18:3 FUNCTION_CALL otherWritten -> callback_registered app.js:18:16 app.js:18:3 FUNCTION_CALL otherWritten -> known_edge wrap.js:31:1 otherWritten app.js:18:3 FUNCTION_CALL otherWritten(() => new A(), null) -> known_edge wrap.js:33:10 +app.js:18:3 FUNCTION_CALL otherWritten(() => new A(), null) -> multi_inferred app.js:18:16 app.js:18:3 METHOD_CALL otherWritten(() => new A(), null)().replacedOnly -> ambiguous_unknown - app.js:4:14 FUNCTION_CALL swapped -> callback_registered app.js:4:22 app.js:4:14 FUNCTION_CALL swapped -> known_edge wrap.js:7:1 swapped app.js:4:28 CONSTRUCTOR_CALL A -> implicit_constructor - app.js:5:3 FUNCTION_CALL sw -> known_edge wrap.js:9:10 +app.js:5:3 FUNCTION_CALL sw -> multi_inferred wrap.js:8:8 app.js:5:3 METHOD_CALL sw().replacedOnly -> known_edge wrap.js:3:18 replacedOnly app.js:6:3 FUNCTION_CALL sw -> known_edge wrap.js:9:10 +app.js:6:3 FUNCTION_CALL sw -> multi_inferred wrap.js:8:8 app.js:6:3 METHOD_CALL sw().aOnly -> ambiguous_unknown - app.js:7:3 FUNCTION_CALL lazyGuard -> known_edge wrap.js:12:1 lazyGuard app.js:7:3 FUNCTION_CALL lazyGuard(undefined) -> known_edge wrap.js:14:10 +app.js:7:3 FUNCTION_CALL lazyGuard(undefined) -> multi_inferred wrap.js:13:38 app.js:7:3 METHOD_CALL lazyGuard(undefined)().replacedOnly -> known_edge wrap.js:3:18 replacedOnly app.js:8:19 CONSTRUCTOR_CALL A -> implicit_constructor - app.js:8:3 FUNCTION_CALL lazyGuard -> callback_registered app.js:8:13 app.js:8:3 FUNCTION_CALL lazyGuard -> known_edge wrap.js:12:1 lazyGuard app.js:8:3 FUNCTION_CALL lazyGuard(() => new A()) -> known_edge wrap.js:14:10 +app.js:8:3 FUNCTION_CALL lazyGuard(() => new A()) -> multi_inferred app.js:8:13 +app.js:8:3 FUNCTION_CALL lazyGuard(() => new A()) -> multi_inferred wrap.js:13:38 app.js:8:3 METHOD_CALL lazyGuard(() => new A())().aOnly -> known_edge wrap.js:2:11 aOnly app.js:9:3 FUNCTION_CALL lazyOrDefault -> known_edge wrap.js:17:1 lazyOrDefault app.js:9:3 FUNCTION_CALL lazyOrDefault() -> known_edge wrap.js:19:10 +app.js:9:3 FUNCTION_CALL lazyOrDefault() -> multi_inferred wrap.js:18:15 app.js:9:3 METHOD_CALL lazyOrDefault()().dfltOnly -> known_edge wrap.js:4:14 dfltOnly many.js:11:3 FUNCTION_CALL s21 -> known_edge wrap.js:9:10 +many.js:11:3 FUNCTION_CALL s21 -> multi_inferred wrap.js:8:8 many.js:11:3 METHOD_CALL s21().replacedOnly -> known_edge wrap.js:3:18 replacedOnly many.js:12:3 FUNCTION_CALL s21 -> known_edge wrap.js:9:10 +many.js:12:3 FUNCTION_CALL s21 -> multi_inferred wrap.js:8:8 many.js:12:3 METHOD_CALL s21().aOnly -> ambiguous_unknown - many.js:3:13 FUNCTION_CALL swapped -> callback_registered many.js:3:21 many.js:3:13 FUNCTION_CALL swapped -> known_edge wrap.js:7:1 swapped diff --git a/parser/src/parsers/javascript/extractors/js-module-edge-extractor.ts b/parser/src/parsers/javascript/extractors/js-module-edge-extractor.ts index 50bd20c4..d07fcb1c 100644 --- a/parser/src/parsers/javascript/extractors/js-module-edge-extractor.ts +++ b/parser/src/parsers/javascript/extractors/js-module-edge-extractor.ts @@ -1214,8 +1214,20 @@ class JsModuleEdgeExtractor { ).resolvedModule?.resolvedFileName; // tsc resolves no `.vue` import itself (its extensions are fixed), so a // component in this program is looked up by path. - const resolved = resolveIn(mode) ?? (mode === ts.ModuleKind.ESNext ? resolveIn(ts.ModuleKind.CommonJS) : undefined) + let resolved = resolveIn(mode) ?? (mode === ts.ModuleKind.ESNext ? resolveIn(ts.ModuleKind.CommonJS) : undefined) ?? resolveVueSpecifier(specifier, this.options.absoluteFilePath, this.options.compilerOptions); + // The compiler prefers a DECLARATION to the code it describes: `require('../')` in a package + // whose package.json names `types: typings/index.d.ts`, or `import './lib.js'` beside a + // hand-written `lib.d.ts`, resolves to the .d.ts. JavaScript never loads a declaration, so + // the module it runs is asked for again with declarations out of sight; left alone, every + // value imported that way was external and each call through it untyped. + if (resolved !== undefined && isDeclarationPath(resolved) + && !resolved.includes(`${path.sep}node_modules${path.sep}`) && !resolved.includes('/node_modules/')) { + const runtime = resolveRuntimeFile(specifier, this.options.absoluteFilePath, this.options.compilerOptions, mode); + if (runtime !== undefined) { + resolved = runtime; + } + } // BOTH SIDES CANONICAL. `projectModuleHashes` is keyed by the files the analyzer // walked from a root it has already resolved through its symlinks; the resolver // answers with the real path for a package found under `node_modules` but does NOT @@ -1280,6 +1292,36 @@ class JsModuleEdgeExtractor { // --------------------------------------------------------------------------- +const DECLARATION_PATH = /\.d\.[cm]?ts$/; + +function isDeclarationPath(file: string): boolean { + return DECLARATION_PATH.test(file); +} + +/** + * The file a JavaScript runtime would load for `specifier`: the compiler's resolution with every + * declaration file hidden from it, so `types`/`typings` and a `.d.ts` beside a `.js` fall through to + * `main`, the `exports` code conditions and the `.js` itself. `undefined` when nothing but a + * declaration answers (a types-only package), which leaves the first answer standing. + */ +function resolveRuntimeFile( + specifier: string, + fromFile: string, + options: ts.CompilerOptions, + mode: ts.ResolutionMode +): string | undefined { + const host: ts.ModuleResolutionHost = { + ...ts.sys, + fileExists: (f) => !isDeclarationPath(f) && ts.sys.fileExists(f), + }; + const answer = (m: ts.ResolutionMode): string | undefined => { + const r = ts.resolveModuleName(specifier, fromFile, { ...options, allowJs: true }, host, undefined, undefined, m) + .resolvedModule?.resolvedFileName; + return r !== undefined && !isDeclarationPath(r) ? r : undefined; + }; + return answer(mode) ?? (mode === ts.ModuleKind.ESNext ? answer(ts.ModuleKind.CommonJS) : undefined); +} + /** `createRequire(import.meta.url)`, however it was imported. */ function isCreateRequireCall(node: ts.CallExpression): boolean { const callee = node.expression; diff --git a/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/case.json b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/case.json new file mode 100644 index 00000000..dcb66027 --- /dev/null +++ b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/case.json @@ -0,0 +1,16 @@ +{"lang": "javascript", "src": "src", + "checks": [ + {"why": "calling what `promisable(op3Impl, 2)` returned runs op3Impl: the test that calls the export reaches the function the module wrapped", + "run": ["impact", "op3Impl", "--tests"], + "want": ["op3.test.js"], + "avoid": ["tests: 0 of", "op4.test.js"]}, + {"why": "the same through a member of a re-exporting object: lib.op4(...) runs op4Impl, and no other wrapped function", + "run": ["impact", "op4Impl", "--tests"], + "want": ["op4.test.js"], + "avoid": ["op3.test.js"]}, + {"why": "the wrapper's own call of its parameter still holds every wrapped function only per site: op3's test is not a test of op5Impl", + "run": ["impact", "op5Impl", "--tests"], + "avoid": ["op3.test.js", "op4.test.js"]}, + {"why": "CONTROL: a wrapper whose closure never calls its parameter runs nothing it was handed, so calling the labelled export does not reach secret", + "run": ["impact", "secret", "--tests"], + "avoid": ["named.test.js"]}]} diff --git a/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/index.js b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/index.js new file mode 100644 index 00000000..0ca257ee --- /dev/null +++ b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/index.js @@ -0,0 +1,3 @@ +import op3 from './op3.js'; +import op4 from './op4.js'; +export default { op3, op4 }; diff --git a/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/label.js b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/label.js new file mode 100644 index 00000000..f1fd17f2 --- /dev/null +++ b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/label.js @@ -0,0 +1,3 @@ +export default function label(fn) { + return function labelled() { return fn.name; }; +} diff --git a/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/named.js b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/named.js new file mode 100644 index 00000000..2fda0a4c --- /dev/null +++ b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/named.js @@ -0,0 +1,3 @@ +import label from './label.js'; +function secret() { return 42; } +export default label(secret); diff --git a/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op0.js b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op0.js new file mode 100644 index 00000000..512a36e3 --- /dev/null +++ b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op0.js @@ -0,0 +1,3 @@ +import promisable from './promisable.js'; +function op0Impl(coll, cb) { return cb(null, coll.length + 0); } +export default promisable(op0Impl, 2); diff --git a/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op1.js b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op1.js new file mode 100644 index 00000000..64b59b8b --- /dev/null +++ b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op1.js @@ -0,0 +1,3 @@ +import promisable from './promisable.js'; +function op1Impl(coll, cb) { return cb(null, coll.length + 1); } +export default promisable(op1Impl, 2); diff --git a/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op10.js b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op10.js new file mode 100644 index 00000000..942919bf --- /dev/null +++ b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op10.js @@ -0,0 +1,3 @@ +import promisable from './promisable.js'; +function op10Impl(coll, cb) { return cb(null, coll.length + 10); } +export default promisable(op10Impl, 2); diff --git a/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op11.js b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op11.js new file mode 100644 index 00000000..fd06b995 --- /dev/null +++ b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op11.js @@ -0,0 +1,3 @@ +import promisable from './promisable.js'; +function op11Impl(coll, cb) { return cb(null, coll.length + 11); } +export default promisable(op11Impl, 2); diff --git a/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op12.js b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op12.js new file mode 100644 index 00000000..0e1cc8ec --- /dev/null +++ b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op12.js @@ -0,0 +1,3 @@ +import promisable from './promisable.js'; +function op12Impl(coll, cb) { return cb(null, coll.length + 12); } +export default promisable(op12Impl, 2); diff --git a/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op13.js b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op13.js new file mode 100644 index 00000000..7333efb6 --- /dev/null +++ b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op13.js @@ -0,0 +1,3 @@ +import promisable from './promisable.js'; +function op13Impl(coll, cb) { return cb(null, coll.length + 13); } +export default promisable(op13Impl, 2); diff --git a/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op14.js b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op14.js new file mode 100644 index 00000000..2151714c --- /dev/null +++ b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op14.js @@ -0,0 +1,3 @@ +import promisable from './promisable.js'; +function op14Impl(coll, cb) { return cb(null, coll.length + 14); } +export default promisable(op14Impl, 2); diff --git a/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op15.js b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op15.js new file mode 100644 index 00000000..39b600b4 --- /dev/null +++ b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op15.js @@ -0,0 +1,3 @@ +import promisable from './promisable.js'; +function op15Impl(coll, cb) { return cb(null, coll.length + 15); } +export default promisable(op15Impl, 2); diff --git a/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op16.js b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op16.js new file mode 100644 index 00000000..4b219615 --- /dev/null +++ b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op16.js @@ -0,0 +1,3 @@ +import promisable from './promisable.js'; +function op16Impl(coll, cb) { return cb(null, coll.length + 16); } +export default promisable(op16Impl, 2); diff --git a/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op17.js b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op17.js new file mode 100644 index 00000000..3f40d043 --- /dev/null +++ b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op17.js @@ -0,0 +1,3 @@ +import promisable from './promisable.js'; +function op17Impl(coll, cb) { return cb(null, coll.length + 17); } +export default promisable(op17Impl, 2); diff --git a/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op18.js b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op18.js new file mode 100644 index 00000000..2844d30b --- /dev/null +++ b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op18.js @@ -0,0 +1,3 @@ +import promisable from './promisable.js'; +function op18Impl(coll, cb) { return cb(null, coll.length + 18); } +export default promisable(op18Impl, 2); diff --git a/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op19.js b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op19.js new file mode 100644 index 00000000..5ae0a6cb --- /dev/null +++ b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op19.js @@ -0,0 +1,3 @@ +import promisable from './promisable.js'; +function op19Impl(coll, cb) { return cb(null, coll.length + 19); } +export default promisable(op19Impl, 2); diff --git a/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op2.js b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op2.js new file mode 100644 index 00000000..6332264f --- /dev/null +++ b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op2.js @@ -0,0 +1,3 @@ +import promisable from './promisable.js'; +function op2Impl(coll, cb) { return cb(null, coll.length + 2); } +export default promisable(op2Impl, 2); diff --git a/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op20.js b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op20.js new file mode 100644 index 00000000..f69644bc --- /dev/null +++ b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op20.js @@ -0,0 +1,3 @@ +import promisable from './promisable.js'; +function op20Impl(coll, cb) { return cb(null, coll.length + 20); } +export default promisable(op20Impl, 2); diff --git a/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op21.js b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op21.js new file mode 100644 index 00000000..bfed47cf --- /dev/null +++ b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op21.js @@ -0,0 +1,3 @@ +import promisable from './promisable.js'; +function op21Impl(coll, cb) { return cb(null, coll.length + 21); } +export default promisable(op21Impl, 2); diff --git a/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op22.js b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op22.js new file mode 100644 index 00000000..b7b24386 --- /dev/null +++ b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op22.js @@ -0,0 +1,3 @@ +import promisable from './promisable.js'; +function op22Impl(coll, cb) { return cb(null, coll.length + 22); } +export default promisable(op22Impl, 2); diff --git a/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op23.js b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op23.js new file mode 100644 index 00000000..bf228d69 --- /dev/null +++ b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op23.js @@ -0,0 +1,3 @@ +import promisable from './promisable.js'; +function op23Impl(coll, cb) { return cb(null, coll.length + 23); } +export default promisable(op23Impl, 2); diff --git a/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op3.js b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op3.js new file mode 100644 index 00000000..19c2ac86 --- /dev/null +++ b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op3.js @@ -0,0 +1,3 @@ +import promisable from './promisable.js'; +function op3Impl(coll, cb) { return cb(null, coll.length + 3); } +export default promisable(op3Impl, 2); diff --git a/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op4.js b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op4.js new file mode 100644 index 00000000..f38f30c4 --- /dev/null +++ b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op4.js @@ -0,0 +1,3 @@ +import promisable from './promisable.js'; +function op4Impl(coll, cb) { return cb(null, coll.length + 4); } +export default promisable(op4Impl, 2); diff --git a/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op5.js b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op5.js new file mode 100644 index 00000000..1898ad42 --- /dev/null +++ b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op5.js @@ -0,0 +1,3 @@ +import promisable from './promisable.js'; +function op5Impl(coll, cb) { return cb(null, coll.length + 5); } +export default promisable(op5Impl, 2); diff --git a/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op6.js b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op6.js new file mode 100644 index 00000000..820234a4 --- /dev/null +++ b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op6.js @@ -0,0 +1,3 @@ +import promisable from './promisable.js'; +function op6Impl(coll, cb) { return cb(null, coll.length + 6); } +export default promisable(op6Impl, 2); diff --git a/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op7.js b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op7.js new file mode 100644 index 00000000..53e4e1a6 --- /dev/null +++ b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op7.js @@ -0,0 +1,3 @@ +import promisable from './promisable.js'; +function op7Impl(coll, cb) { return cb(null, coll.length + 7); } +export default promisable(op7Impl, 2); diff --git a/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op8.js b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op8.js new file mode 100644 index 00000000..873712ec --- /dev/null +++ b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op8.js @@ -0,0 +1,3 @@ +import promisable from './promisable.js'; +function op8Impl(coll, cb) { return cb(null, coll.length + 8); } +export default promisable(op8Impl, 2); diff --git a/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op9.js b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op9.js new file mode 100644 index 00000000..f7dfa907 --- /dev/null +++ b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/op9.js @@ -0,0 +1,3 @@ +import promisable from './promisable.js'; +function op9Impl(coll, cb) { return cb(null, coll.length + 9); } +export default promisable(op9Impl, 2); diff --git a/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/promisable.js b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/promisable.js new file mode 100644 index 00000000..85733214 --- /dev/null +++ b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/lib/promisable.js @@ -0,0 +1,12 @@ +export default function promisable(fn, arity) { + function run(...args) { + if (typeof args[arity - 1] === 'function') { + return fn.apply(this, args); + } + return new Promise((resolve) => { + args[arity - 1] = (err, value) => resolve(value); + fn.apply(this, args); + }); + } + return run; +} diff --git a/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/package.json b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/package.json new file mode 100644 index 00000000..5a4a9bf6 --- /dev/null +++ b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/package.json @@ -0,0 +1 @@ +{"name": "wrapkit", "type": "module"} diff --git a/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/test/named.test.js b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/test/named.test.js new file mode 100644 index 00000000..82c77371 --- /dev/null +++ b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/test/named.test.js @@ -0,0 +1,2 @@ +import named from '../lib/named.js'; +it('named', () => { named(); }); diff --git a/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/test/op3.test.js b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/test/op3.test.js new file mode 100644 index 00000000..9ef95bfc --- /dev/null +++ b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/test/op3.test.js @@ -0,0 +1,2 @@ +import op3 from '../lib/op3.js'; +it('op3', (done) => { op3([1], done); }); diff --git a/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/test/op4.test.js b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/test/op4.test.js new file mode 100644 index 00000000..c82a1032 --- /dev/null +++ b/tests/cases/javascript/call-through-a-wrapper-runs-its-argument/src/test/op4.test.js @@ -0,0 +1,2 @@ +import lib from '../lib/index.js'; +it('op4', (done) => { lib.op4([1, 2], done); }); diff --git a/tests/cases/javascript/declaration-beside-the-code/case.json b/tests/cases/javascript/declaration-beside-the-code/case.json new file mode 100644 index 00000000..2257c3b4 --- /dev/null +++ b/tests/cases/javascript/declaration-beside-the-code/case.json @@ -0,0 +1,62 @@ +{ + "lang": "javascript", + "src": "src", + "checks": [ + { + "why": "require('../') in a package whose package.json names a hand-written declaration (types / an exports types condition) loads index.js at runtime: the test that calls the class it exports reaches the method", + "run": [ + "impact", + "Reader.skipBlank", + "--tests" + ], + "want": [ + "reader.test.js", + "tests: 1 of" + ], + "avoid": [ + "tests: 0 of" + ] + }, + { + "why": "require('../lib/format.js') beside a format.d.ts loads the .js: the test's call is an exact edge, not a name match", + "run": [ + "path", + "src/test/format.test.js", + "wrap" + ], + "want": [ + "known_edge" + ], + "avoid": [ + "receiver not typed", + "callee is" + ] + }, + { + "why": "path from the test file is an exact chain through the runtime module, not an unknown receiver", + "run": [ + "path", + "src/test/reader.test.js", + "Reader.readAll" + ], + "want": [ + "known_edge" + ], + "avoid": [ + "receiver not typed" + ] + }, + { + "why": "CONTROL: a module that exists only as a declaration has no code to run; a call through it stays unresolved and is never linked to a same-named project method", + "run": [ + "path", + "src/test/shape.test.js", + "Square.area" + ], + "expect_error": true, + "avoid": [ + "known_edge" + ] + } + ] +} diff --git a/tests/cases/javascript/declaration-beside-the-code/src/index.js b/tests/cases/javascript/declaration-beside-the-code/src/index.js new file mode 100644 index 00000000..5343d15f --- /dev/null +++ b/tests/cases/javascript/declaration-beside-the-code/src/index.js @@ -0,0 +1,3 @@ +const { Reader } = require('./lib/reader.js'); +exports.Reader = Reader; +exports.reader = new Reader(); diff --git a/tests/cases/javascript/declaration-beside-the-code/src/lib/format.d.ts b/tests/cases/javascript/declaration-beside-the-code/src/lib/format.d.ts new file mode 100644 index 00000000..a3746a06 --- /dev/null +++ b/tests/cases/javascript/declaration-beside-the-code/src/lib/format.d.ts @@ -0,0 +1 @@ +export function wrap(text: string): string; diff --git a/tests/cases/javascript/declaration-beside-the-code/src/lib/format.js b/tests/cases/javascript/declaration-beside-the-code/src/lib/format.js new file mode 100644 index 00000000..c2f69520 --- /dev/null +++ b/tests/cases/javascript/declaration-beside-the-code/src/lib/format.js @@ -0,0 +1,2 @@ +function wrap(text) { return `[${text}]`; } +module.exports = { wrap }; diff --git a/tests/cases/javascript/declaration-beside-the-code/src/lib/geometry.js b/tests/cases/javascript/declaration-beside-the-code/src/lib/geometry.js new file mode 100644 index 00000000..a7895a60 --- /dev/null +++ b/tests/cases/javascript/declaration-beside-the-code/src/lib/geometry.js @@ -0,0 +1,2 @@ +class Square { area() { return 4; } } +module.exports = { Square }; diff --git a/tests/cases/javascript/declaration-beside-the-code/src/lib/reader.js b/tests/cases/javascript/declaration-beside-the-code/src/lib/reader.js new file mode 100644 index 00000000..e4e12860 --- /dev/null +++ b/tests/cases/javascript/declaration-beside-the-code/src/lib/reader.js @@ -0,0 +1,5 @@ +class Reader { + readAll(argv) { return this.skipBlank(argv); } + skipBlank(argv) { return argv.length; } +} +exports.Reader = Reader; diff --git a/tests/cases/javascript/declaration-beside-the-code/src/lib/shape.d.ts b/tests/cases/javascript/declaration-beside-the-code/src/lib/shape.d.ts new file mode 100644 index 00000000..68e7e66a --- /dev/null +++ b/tests/cases/javascript/declaration-beside-the-code/src/lib/shape.d.ts @@ -0,0 +1 @@ +export interface Shape { area(): number; } diff --git a/tests/cases/javascript/declaration-beside-the-code/src/package.json b/tests/cases/javascript/declaration-beside-the-code/src/package.json new file mode 100644 index 00000000..572becf2 --- /dev/null +++ b/tests/cases/javascript/declaration-beside-the-code/src/package.json @@ -0,0 +1,2 @@ +{"name": "readkit", "main": "./index.js", "types": "./typings/index.d.ts", + "exports": {".": {"require": {"types": "./typings/index.d.ts", "default": "./index.js"}, "default": "./index.js"}}} diff --git a/tests/cases/javascript/declaration-beside-the-code/src/test/format.test.js b/tests/cases/javascript/declaration-beside-the-code/src/test/format.test.js new file mode 100644 index 00000000..29eecc05 --- /dev/null +++ b/tests/cases/javascript/declaration-beside-the-code/src/test/format.test.js @@ -0,0 +1,2 @@ +const { wrap } = require('../lib/format.js'); +test('wrap', () => { expect(wrap('a')).toBe('[a]'); }); diff --git a/tests/cases/javascript/declaration-beside-the-code/src/test/reader.test.js b/tests/cases/javascript/declaration-beside-the-code/src/test/reader.test.js new file mode 100644 index 00000000..d204209c --- /dev/null +++ b/tests/cases/javascript/declaration-beside-the-code/src/test/reader.test.js @@ -0,0 +1,5 @@ +const kit = require('../'); +test('readAll', () => { + const reader = new kit.Reader(); + expect(() => reader.readAll(['x'])).not.toThrow(); +}); diff --git a/tests/cases/javascript/declaration-beside-the-code/src/test/shape.test.js b/tests/cases/javascript/declaration-beside-the-code/src/test/shape.test.js new file mode 100644 index 00000000..77f0280a --- /dev/null +++ b/tests/cases/javascript/declaration-beside-the-code/src/test/shape.test.js @@ -0,0 +1,2 @@ +const shape = require('../lib/shape'); +test('shape', () => { expect(shape.area()).toBe(1); }); diff --git a/tests/cases/javascript/declaration-beside-the-code/src/typings/index.d.ts b/tests/cases/javascript/declaration-beside-the-code/src/typings/index.d.ts new file mode 100644 index 00000000..b647e452 --- /dev/null +++ b/tests/cases/javascript/declaration-beside-the-code/src/typings/index.d.ts @@ -0,0 +1,2 @@ +export class Reader { readAll(argv: string[]): number; } +export const reader: Reader; diff --git a/tests/cases/javascript/tests-do-not-make-the-api-hot/case.json b/tests/cases/javascript/tests-do-not-make-the-api-hot/case.json new file mode 100644 index 00000000..cd47315d --- /dev/null +++ b/tests/cases/javascript/tests-do-not-make-the-api-hot/case.json @@ -0,0 +1,55 @@ +{ + "lang": "javascript", + "src": "src", + "checks": [ + { + "why": "a constructor the tests call 24 times is not a generic utility: the program's own `new Codec(name, {decode})` reaches the parameter, so `codec.decode()` in the loader calls the stored callback and the loader's test reaches it", + "run": [ + "impact", + "decode_alpha", + "--tests" + ], + "want": [ + "load", + "load.test.js" + ], + "avoid": [ + "tests: 0 of" + ] + }, + { + "why": "a test's argument does not flow into the program's parameter: the loader's call is one of the three library callbacks and the fallback, never a test's own decoder", + "run": [ + "impact", + "customDecode3" + ], + "avoid": [ + "load " + ] + }, + { + "why": "CONTROL: a utility the PROGRAM calls from 23 places stays capped: what it is handed is not followed, so the callback is not a callee of each", + "run": [ + "impact", + "onItem" + ], + "avoid": [ + "each " + ] + }, + { + "why": "a test still hands the program what it calls back: a plugin passed from a test to core.extend is called with the class, so the method it adds is what the test's `core().shout()` calls (what flowed before keeps flowing)", + "run": [ + "path", + "src/test/plugin.test.js", + "shout" + ], + "want": [ + "known_edge" + ], + "avoid": [ + "receiver not typed" + ] + } + ] +} diff --git a/tests/cases/javascript/tests-do-not-make-the-api-hot/src/lib/codec.js b/tests/cases/javascript/tests-do-not-make-the-api-hot/src/lib/codec.js new file mode 100644 index 00000000..cbf366d7 --- /dev/null +++ b/tests/cases/javascript/tests-do-not-make-the-api-hot/src/lib/codec.js @@ -0,0 +1,7 @@ +'use strict'; +function Codec(name, options) { + options = options || {}; + this.name = name; + this.decode = options['decode'] || function () { return true; }; +} +module.exports = Codec; diff --git a/tests/cases/javascript/tests-do-not-make-the-api-hot/src/lib/codecs/alpha.js b/tests/cases/javascript/tests-do-not-make-the-api-hot/src/lib/codecs/alpha.js new file mode 100644 index 00000000..a5864f2e --- /dev/null +++ b/tests/cases/javascript/tests-do-not-make-the-api-hot/src/lib/codecs/alpha.js @@ -0,0 +1,4 @@ +'use strict'; +var Codec = require('../codec'); +function decode_alpha(data) { return data === 'alpha'; } +module.exports = new Codec('codec:alpha', { decode: decode_alpha }); diff --git a/tests/cases/javascript/tests-do-not-make-the-api-hot/src/lib/codecs/beta.js b/tests/cases/javascript/tests-do-not-make-the-api-hot/src/lib/codecs/beta.js new file mode 100644 index 00000000..8b97967e --- /dev/null +++ b/tests/cases/javascript/tests-do-not-make-the-api-hot/src/lib/codecs/beta.js @@ -0,0 +1,4 @@ +'use strict'; +var Codec = require('../codec'); +function decode_beta(data) { return data === 'beta'; } +module.exports = new Codec('codec:beta', { decode: decode_beta }); diff --git a/tests/cases/javascript/tests-do-not-make-the-api-hot/src/lib/codecs/gamma.js b/tests/cases/javascript/tests-do-not-make-the-api-hot/src/lib/codecs/gamma.js new file mode 100644 index 00000000..e1d9291d --- /dev/null +++ b/tests/cases/javascript/tests-do-not-make-the-api-hot/src/lib/codecs/gamma.js @@ -0,0 +1,4 @@ +'use strict'; +var Codec = require('../codec'); +function decode_gamma(data) { return data === 'gamma'; } +module.exports = new Codec('codec:gamma', { decode: decode_gamma }); diff --git a/tests/cases/javascript/tests-do-not-make-the-api-hot/src/lib/core.js b/tests/cases/javascript/tests-do-not-make-the-api-hot/src/lib/core.js new file mode 100644 index 00000000..8424af35 --- /dev/null +++ b/tests/cases/javascript/tests-do-not-make-the-api-hot/src/lib/core.js @@ -0,0 +1,5 @@ +'use strict'; +class Core {} +function core() { return new Core(); } +core.extend = (plugin) => { plugin(Core); return core; }; +module.exports = core; diff --git a/tests/cases/javascript/tests-do-not-make-the-api-hot/src/lib/each.js b/tests/cases/javascript/tests-do-not-make-the-api-hot/src/lib/each.js new file mode 100644 index 00000000..94936ac2 --- /dev/null +++ b/tests/cases/javascript/tests-do-not-make-the-api-hot/src/lib/each.js @@ -0,0 +1,3 @@ +'use strict'; +function each(list, fn) { for (var i = 0; i < list.length; i++) fn(list[i]); } +module.exports = each; diff --git a/tests/cases/javascript/tests-do-not-make-the-api-hot/src/lib/loader.js b/tests/cases/javascript/tests-do-not-make-the-api-hot/src/lib/loader.js new file mode 100644 index 00000000..2086d532 --- /dev/null +++ b/tests/cases/javascript/tests-do-not-make-the-api-hot/src/lib/loader.js @@ -0,0 +1,10 @@ +'use strict'; +var codecs = [require('./codecs/alpha'), require('./codecs/beta'), require('./codecs/gamma')]; +function load(x) { + for (var i = 0; i < codecs.length; i++) { + var codec = codecs[i]; + if (codec.decode(x)) return codec.name; + } + return null; +} +module.exports = { load, Codec: require('./codec') }; diff --git a/tests/cases/javascript/tests-do-not-make-the-api-hot/src/lib/plugins/shout.js b/tests/cases/javascript/tests-do-not-make-the-api-hot/src/lib/plugins/shout.js new file mode 100644 index 00000000..21df4a2e --- /dev/null +++ b/tests/cases/javascript/tests-do-not-make-the-api-hot/src/lib/plugins/shout.js @@ -0,0 +1,4 @@ +'use strict'; +module.exports = (C) => { + C.prototype.shout = function shout() { return 'HEY'; }; +}; diff --git a/tests/cases/javascript/tests-do-not-make-the-api-hot/src/lib/walk.js b/tests/cases/javascript/tests-do-not-make-the-api-hot/src/lib/walk.js new file mode 100644 index 00000000..bee2917a --- /dev/null +++ b/tests/cases/javascript/tests-do-not-make-the-api-hot/src/lib/walk.js @@ -0,0 +1,29 @@ +'use strict'; +var each = require('./each'); +function onItem(x) { return x; } +function walk(list) { + each(list, function step0(x) { return x + 0; }); + each(list, function step1(x) { return x + 1; }); + each(list, function step2(x) { return x + 2; }); + each(list, function step3(x) { return x + 3; }); + each(list, function step4(x) { return x + 4; }); + each(list, function step5(x) { return x + 5; }); + each(list, function step6(x) { return x + 6; }); + each(list, function step7(x) { return x + 7; }); + each(list, function step8(x) { return x + 8; }); + each(list, function step9(x) { return x + 9; }); + each(list, function step10(x) { return x + 10; }); + each(list, function step11(x) { return x + 11; }); + each(list, function step12(x) { return x + 12; }); + each(list, function step13(x) { return x + 13; }); + each(list, function step14(x) { return x + 14; }); + each(list, function step15(x) { return x + 15; }); + each(list, function step16(x) { return x + 16; }); + each(list, function step17(x) { return x + 17; }); + each(list, function step18(x) { return x + 18; }); + each(list, function step19(x) { return x + 19; }); + each(list, function step20(x) { return x + 20; }); + each(list, function step21(x) { return x + 21; }); + each(list, onItem); +} +module.exports = { walk, onItem }; diff --git a/tests/cases/javascript/tests-do-not-make-the-api-hot/src/package.json b/tests/cases/javascript/tests-do-not-make-the-api-hot/src/package.json new file mode 100644 index 00000000..8b109c46 --- /dev/null +++ b/tests/cases/javascript/tests-do-not-make-the-api-hot/src/package.json @@ -0,0 +1 @@ +{"name": "codeckit"} diff --git a/tests/cases/javascript/tests-do-not-make-the-api-hot/src/test/custom.test.js b/tests/cases/javascript/tests-do-not-make-the-api-hot/src/test/custom.test.js new file mode 100644 index 00000000..d64224b8 --- /dev/null +++ b/tests/cases/javascript/tests-do-not-make-the-api-hot/src/test/custom.test.js @@ -0,0 +1,25 @@ +const { Codec } = require('../lib/loader'); +test('custom 0', () => { const t = new Codec('!c0', { decode: function customDecode0(d) { return d === 0; } }); expect(t.decode(0)).toBe(true); }); +test('custom 1', () => { const t = new Codec('!c1', { decode: function customDecode1(d) { return d === 1; } }); expect(t.decode(1)).toBe(true); }); +test('custom 2', () => { const t = new Codec('!c2', { decode: function customDecode2(d) { return d === 2; } }); expect(t.decode(2)).toBe(true); }); +test('custom 3', () => { const t = new Codec('!c3', { decode: function customDecode3(d) { return d === 3; } }); expect(t.decode(3)).toBe(true); }); +test('custom 4', () => { const t = new Codec('!c4', { decode: function customDecode4(d) { return d === 4; } }); expect(t.decode(4)).toBe(true); }); +test('custom 5', () => { const t = new Codec('!c5', { decode: function customDecode5(d) { return d === 5; } }); expect(t.decode(5)).toBe(true); }); +test('custom 6', () => { const t = new Codec('!c6', { decode: function customDecode6(d) { return d === 6; } }); expect(t.decode(6)).toBe(true); }); +test('custom 7', () => { const t = new Codec('!c7', { decode: function customDecode7(d) { return d === 7; } }); expect(t.decode(7)).toBe(true); }); +test('custom 8', () => { const t = new Codec('!c8', { decode: function customDecode8(d) { return d === 8; } }); expect(t.decode(8)).toBe(true); }); +test('custom 9', () => { const t = new Codec('!c9', { decode: function customDecode9(d) { return d === 9; } }); expect(t.decode(9)).toBe(true); }); +test('custom 10', () => { const t = new Codec('!c10', { decode: function customDecode10(d) { return d === 10; } }); expect(t.decode(10)).toBe(true); }); +test('custom 11', () => { const t = new Codec('!c11', { decode: function customDecode11(d) { return d === 11; } }); expect(t.decode(11)).toBe(true); }); +test('custom 12', () => { const t = new Codec('!c12', { decode: function customDecode12(d) { return d === 12; } }); expect(t.decode(12)).toBe(true); }); +test('custom 13', () => { const t = new Codec('!c13', { decode: function customDecode13(d) { return d === 13; } }); expect(t.decode(13)).toBe(true); }); +test('custom 14', () => { const t = new Codec('!c14', { decode: function customDecode14(d) { return d === 14; } }); expect(t.decode(14)).toBe(true); }); +test('custom 15', () => { const t = new Codec('!c15', { decode: function customDecode15(d) { return d === 15; } }); expect(t.decode(15)).toBe(true); }); +test('custom 16', () => { const t = new Codec('!c16', { decode: function customDecode16(d) { return d === 16; } }); expect(t.decode(16)).toBe(true); }); +test('custom 17', () => { const t = new Codec('!c17', { decode: function customDecode17(d) { return d === 17; } }); expect(t.decode(17)).toBe(true); }); +test('custom 18', () => { const t = new Codec('!c18', { decode: function customDecode18(d) { return d === 18; } }); expect(t.decode(18)).toBe(true); }); +test('custom 19', () => { const t = new Codec('!c19', { decode: function customDecode19(d) { return d === 19; } }); expect(t.decode(19)).toBe(true); }); +test('custom 20', () => { const t = new Codec('!c20', { decode: function customDecode20(d) { return d === 20; } }); expect(t.decode(20)).toBe(true); }); +test('custom 21', () => { const t = new Codec('!c21', { decode: function customDecode21(d) { return d === 21; } }); expect(t.decode(21)).toBe(true); }); +test('custom 22', () => { const t = new Codec('!c22', { decode: function customDecode22(d) { return d === 22; } }); expect(t.decode(22)).toBe(true); }); +test('custom 23', () => { const t = new Codec('!c23', { decode: function customDecode23(d) { return d === 23; } }); expect(t.decode(23)).toBe(true); }); diff --git a/tests/cases/javascript/tests-do-not-make-the-api-hot/src/test/load.test.js b/tests/cases/javascript/tests-do-not-make-the-api-hot/src/test/load.test.js new file mode 100644 index 00000000..9cffc0a6 --- /dev/null +++ b/tests/cases/javascript/tests-do-not-make-the-api-hot/src/test/load.test.js @@ -0,0 +1,2 @@ +const { load } = require('../lib/loader'); +test('alpha', () => { expect(load('alpha')).toBe('codec:alpha'); }); diff --git a/tests/cases/javascript/tests-do-not-make-the-api-hot/src/test/plugin.test.js b/tests/cases/javascript/tests-do-not-make-the-api-hot/src/test/plugin.test.js new file mode 100644 index 00000000..f4478092 --- /dev/null +++ b/tests/cases/javascript/tests-do-not-make-the-api-hot/src/test/plugin.test.js @@ -0,0 +1,4 @@ +const core = require('../lib/core'); +const shoutPlugin = require('../lib/plugins/shout'); +core.extend(shoutPlugin); +test('shout', () => { expect(core().shout()).toBe('HEY'); }); diff --git a/tests/cases/javascript/tests-do-not-make-the-api-hot/src/test/walk.test.js b/tests/cases/javascript/tests-do-not-make-the-api-hot/src/test/walk.test.js new file mode 100644 index 00000000..4234f11f --- /dev/null +++ b/tests/cases/javascript/tests-do-not-make-the-api-hot/src/test/walk.test.js @@ -0,0 +1,2 @@ +const { walk } = require('../lib/walk'); +test('walk', () => { walk([1]); });