@yuneta/gobj-ui 7.26.30 → 7.26.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -56,66 +56,100 @@
56
56
  * parsed and then imported, and its top-level code runs; see
57
57
  * below.) In each file it reads as keys:
58
58
  *
59
- * - the string given to t() / tr(), to an `i18n` or `code`
60
- * property, to a `data-i18n*` attribute (as a property, as a
61
- * member written, `x.i18n = k` / `x["data-i18n-title"] = k`,
62
- * by setAttribute(), or written in markup), and the codes of
59
+ * - the key given to a TRANSLATOR: t() or tr() -- whatever binds
60
+ * the name --, `i18next.t()`, i18next's t imported under any
61
+ * name, an alias of one (`const translate = t`, a `.bind()`,
62
+ * a re-export), called `t?.(k)`, `t.call(x, k)` or
63
+ * `t.apply(x, [k])`; an array there is i18next's list of
64
+ * fallback keys, each one a key (`t([k, "fallback"])`);
65
+ * - the string given to an `i18n` or `code` property, to a
66
+ * `data-i18n*` attribute (as a property, also computed by a
67
+ * const, `{[ATTR]: k}`; as a member written, `x.i18n = k`,
68
+ * `x["data-i18n-title"] = k`, `x[ATTR] = k`,
69
+ * `el.dataset.i18nTitle = k`; by setAttribute(), the name a
70
+ * literal or a const; or written in markup), and the codes of
63
71
  * the schema checks (`add("error", "<code>")`);
64
72
  * - the strings given to a gobj-ui function that RECEIVES a key
65
73
  * as an argument (`yui_shell_show_error(shell, "<key>")`,
66
74
  * `icon_button(..., "<key>")`), at the positions its table
67
- * names: I18N_KEY_FUNCTIONS, src/i18n_key_functions.js. A
68
- * position may hold DOM as well, which is no key: an array
69
- * (a createElement2 spec), createElement2(), createOneHtml()
70
- * or html_markup() of gobj-js, text_node() of gobj-ui,
75
+ * names: I18N_KEY_FUNCTIONS, src/i18n_key_functions.js --
76
+ * however the call reaches it: its name, an import under any
77
+ * name, a re-export, a const alias, a map
78
+ * (`API.info(...)`), `.call()`, `.apply()`. A position may
79
+ * hold DOM as well, which is no key: an array (a
80
+ * createElement2 spec; not an array of string literals,
81
+ * which is none), createElement2(), createOneHtml() or
82
+ * html_markup() of gobj-js, text_node() of gobj-ui,
71
83
  * document.createElement() and the like -- known by where
72
84
  * they come from, never by the name of a variable;
73
85
  * - the keys a module PUBLISHES as data: an exported
74
86
  * `<NAME>_I18N_KEYS` array of string literals (the keys it
75
87
  * hands to t() as variables, yui_dev.js's 51). Read as data
76
- * -- `export const`, `let` or `var`, `export {X}`, wrapped in
77
- * Object.freeze(), spreading another array of the module read
78
- * the same way (`[...BASE_I18N_KEYS, "k"]`: a top-level const,
79
- * or a list it publishes) --; anything so named that cannot be
80
- * read so is a violation, never skipped (a list not exported,
81
- * declared in a function, by destructuring, or exported under
82
- * another name; an element that is a variable, a call, a spread
83
- * of an import or of a `let`).
88
+ * -- `export const`, `const X = [...]; export {X}`, wrapped
89
+ * in Object.freeze(), spreading another array of the module
90
+ * read the same way (`[...BASE_I18N_KEYS, "k"]`: a top-level
91
+ * const) --; anything so named that cannot be read so is a
92
+ * violation, never skipped (a list declared with `let` or
93
+ * `var`, not exported, declared in a function, by
94
+ * destructuring, or exported under another name; an element
95
+ * that is a variable, a call, a spread of an import or of a
96
+ * `let`). So is a list MUTATED, here or where it is imported
97
+ * (`.push()`, `.splice()`, `.unshift()`, `X[i] = k`,
98
+ * `X.length = n`, Object.assign()): what it is given there is
99
+ * asked for by nobody.
84
100
  *
85
101
  * A string is read where it is written, and FOLLOWED where the
86
102
  * source writes it out: a literal, a template with no `${}`,
87
103
  * every branch of `?:`, `||` and `??`, the right one of `&&`; a
88
104
  * `const` to its value (`const KEY = "save"; t(KEY)` asks for
89
- * "save"); a property of a const object literal (`DEFAULTS.ok`;
90
- * every value of it for `NOTES[verdict]`); the element of a const
91
- * array literal its `.map()`, `.forEach()`, ... callback or a
92
- * for...of iterates, and what a destructuring reads from it;
93
- * `String(x)`, and a template that is only `${x}`, as x; the
94
- * default of a parameter; a call of a function of the same module
95
- * whose every `return` is one of these (`t(pick())`, `pick`
96
- * returning "a" or "b", asks for both). A key built by
97
- * concatenation, held in a `let`, or arriving as data cannot be
98
- * known: its module publishes it in a `<NAME>_I18N_KEYS` list.
105
+ * "save"), and what a destructuring reads from one (`const
106
+ * {title} = OPTS`); an import of a const or a function, into the
107
+ * module it names, when that module is one this script reads (a
108
+ * file of the app, a module of the library); a property of a
109
+ * const object literal (`DEFAULTS.ok`; every value of it for
110
+ * `NOTES[verdict]`), or of a copy of one (`{...o}`,
111
+ * `Object.assign({}, o)`); the element of a const array literal
112
+ * its `.map()`, `.forEach()`, ... callback or a for...of
113
+ * iterates, and what a destructuring reads from it;
114
+ * `String(x)`, and a template that is only `${x}`, as x; a
115
+ * parameter however it is read (`opts?.title`, `arguments[1]`,
116
+ * `rest[0]`) and its default; a call of a function the script
117
+ * reads -- of the module, or imported -- whose every `return`
118
+ * is one of these (`t(pick())`, `pick` returning "a" or "b",
119
+ * asks for both). What a function returns is read ONCE, and a
120
+ * recursion is not followed (a call of a function being read is
121
+ * a value it cannot follow): up to 7.26.31 a recursive helper
122
+ * was read again at every call, and three recursive branches
123
+ * hung the build. A key built by concatenation, held in a `let`,
124
+ * or arriving as data cannot be known: its module publishes it
125
+ * in a `<NAME>_I18N_KEYS` list.
99
126
  *
100
127
  * A MARKER at the place a key is read says where the keys of what
101
128
  * follows it come from: `/\* i18n: <NAME>_I18N_KEYS *\/` (that
102
129
  * published list; several by commas) or `/\* i18n: data *\/` (data,
103
- * no key a list can hold). This script checks the markers it
104
- * reads: each one must be a marker and stand before a place a key
105
- * is read, the lists it names must be published by a module the
106
- * app mounts (the app's own included), and the keys it marks that
107
- * can be read must be in them. A `data` marker on what the reader
108
- * follows to keys and nothing else -- a literal, a const that is
109
- * one, a `?:` branch that is one, a call of a function of the
110
- * module that returns literals -- is a violation: it claims data
111
- * where the reader reads a key. A marked `let` / `var` is READ,
112
- * and so is one a marked member or element is read from (`LABELS[k]`,
113
- * `picked = LIST[0]`, the `x` of `LIST.forEach((x) => ...)`):
114
- * everything it is given in its scope (its value, every `=` to
115
- * it; for a member, also what is written into it: `x.a = v`,
116
- * `x.push(v)`, Object.assign()) is followed, each literal is a key
117
- * asked for and must be in the lists named, and one given nothing
118
- * but literals is a violation: it is a const.
130
+ * no key a list can hold). It marks what stands right after it,
131
+ * through a parenthesis (`/\* i18n: data *\/ (a || b)`). This
132
+ * script checks the markers it reads: each one must be a marker
133
+ * and stand before a place a key is read (one that does not is
134
+ * told what it stands before), the lists it names must be
135
+ * published by a module the app mounts (the app's own included),
136
+ * and the keys it marks that can be read must be in them; a
137
+ * template with `${}` it marks must have the shape of some key of
138
+ * them (`monitor ${control}`: "monitor start"). A `data` marker
139
+ * is a claim too: on what the reader follows to keys and nothing
140
+ * else -- a literal, a const that is one, a `?:` branch that is
141
+ * one, a call that returns literals, imported or not -- it is a
142
+ * violation, and so is one that would HIDE a key the reader does
143
+ * not read -- a call whose returns mix keys with data (split the
144
+ * branches), an array of string literals. A marked `let` / `var`
145
+ * is READ, and so is one a marked member or element is read from
146
+ * (`LABELS[k]`, `picked = LIST[0]`, the `x` of
147
+ * `LIST.forEach((x) => ...)`): everything it is given in its
148
+ * scope (its value, every `=` to it; for a member, also what is
149
+ * written into it: `x.a = v`, `x.push(v)`, Object.assign()) is
150
+ * followed, each literal is a key asked for and must be in the
151
+ * lists named, and one given nothing but literals is a
152
+ * violation: it is a const.
119
153
  *
120
154
  * THE RULE PER CALL holds the app's own code, as a test holds the
121
155
  * library (i18n_key_functions.test.js): every value a key position
@@ -125,13 +159,29 @@
125
159
  * else -- a `let` given literals, an options variable, a
126
160
  * concatenation, a parameter of a wrapper of the app's own -- is a
127
161
  * violation, with the file and the line. So is a TRANSLATED text
128
- * handed there, `t("k")`, even marked: the function translates it
129
- * again and marks it data-i18n with the translation, so it never
130
- * changes language; hand it the key. A function of the app is in
131
- * no table, so what a wrapper of its own hands on is marked where
132
- * it hands it -- and what its callers give it is read only if the
133
- * module publishes it.
162
+ * handed there -- `t("k")`, or any translator above --, even
163
+ * marked: the function translates it again and marks it data-i18n
164
+ * with the translation, so it never changes language; hand the
165
+ * key, not t(key). A function of the app is in no table, so what a
166
+ * wrapper of its own hands on is marked where it hands it -- and
167
+ * what its callers give it is read only if the module publishes
168
+ * it.
134
169
  *
170
+ * What stays INVISIBLE, and fails nothing: a key handed to t(),
171
+ * an i18n or a data-i18n* value by an unmarked `let`, by a call
172
+ * the reader does not follow or by a computed string (only the
173
+ * key positions of the table are held to the rule per call); a
174
+ * translator under another name than t / tr that no const or
175
+ * import makes an alias of (a parameter `translate`, a `let`
176
+ * alias, `i18next.getFixedT()`); a key function reached through a
177
+ * parameter, a `let` given more than one value, a computed
178
+ * member (`API[name](...)`); a data-i18n* attribute written
179
+ * through an alias of `dataset`, by Object.assign() into it, or
180
+ * named by a `let`; an export of a `let`; a module of a package
181
+ * (gobj-js, i18next); what is written into a const table after its
182
+ * declaration; a window's `title` given as an attr at
183
+ * gobj_create().
184
+
135
185
  * Options (paths relative to the app's directory):
136
186
  *
137
187
  * --src <dir> a source directory to scan, besides src/
@@ -194,6 +244,8 @@ const PLURAL_RE = /^(.+?)(_ordinal)?_(zero|one|two|few|many|other)$/;
194
244
  const PUBLISHED_LIST_RE = /_I18N_KEYS$/i;
195
245
  const DATA_I18N_ATTR_RE = /^data-i18n(?:-title|-aria-label|-placeholder)?$/;
196
246
  const DATA_I18N_MARKUP_RE = /data-i18n(?:-title|-aria-label|-placeholder)?=["']([^"']+)["']/g;
247
+ /* `el.dataset.i18nTitle` is the attribute data-i18n-title */
248
+ const DATASET_I18N_RE = /^i18n(?:Title|AriaLabel|Placeholder)?$/;
197
249
 
198
250
  /* This package: the library the app builds with. */
199
251
  const LIB = new URL("../", import.meta.url);
@@ -658,9 +710,13 @@ async function main()
658
710
  * A source file as an ESTree (acorn). Throws on a syntax error.
659
711
  * Its comments (the i18n markers live there) and its text ride
660
712
  * along as `ast.comments` and `ast.code`, not enumerable, so
661
- * walk() never visits them.
713
+ * walk() never visits them. `url` is the file it was read from
714
+ * (a URL or its href): with it, an import of the file is followed
715
+ * into the module it names (module_url()).
662
716
  ***************************************************************/
663
- function parse_source(code)
717
+ const PROGRAM_URL = new WeakMap(); // a parsed file -> the URL it was read from
718
+
719
+ function parse_source(code, url = null)
664
720
  {
665
721
  let comments = [];
666
722
  let ast = acorn_parse(code, {
@@ -671,6 +727,44 @@ function parse_source(code)
671
727
  });
672
728
  Object.defineProperty(ast, "comments", {value: comments, enumerable: false});
673
729
  Object.defineProperty(ast, "code", {value: code, enumerable: false});
730
+ if(url) {
731
+ PROGRAM_URL.set(ast, (url instanceof URL)? url : new URL(url));
732
+ }
733
+ return ast;
734
+ }
735
+
736
+ /***************************************************************
737
+ * The URL of a parsed file. One parsed without it (a test's
738
+ * fixture) is a module of the library when `lib_module` names it:
739
+ * its imports of siblings resolve there.
740
+ ***************************************************************/
741
+ function program_url(ast, lib_module = null)
742
+ {
743
+ if(!PROGRAM_URL.has(ast) && lib_module) {
744
+ PROGRAM_URL.set(ast, new URL(lib_module, LIB_SRC));
745
+ }
746
+ return PROGRAM_URL.get(ast) || null;
747
+ }
748
+
749
+ /***************************************************************
750
+ * A module of the disk, parsed once: the scan and the reader that
751
+ * follows an import share it. null when it cannot be read or
752
+ * parsed (the scan reports that itself).
753
+ ***************************************************************/
754
+ const AST_CACHE = new Map(); // url.href -> ast | null
755
+
756
+ function load_ast(url)
757
+ {
758
+ if(AST_CACHE.has(url.href)) {
759
+ return AST_CACHE.get(url.href);
760
+ }
761
+ let ast = null;
762
+ try {
763
+ ast = parse_source(readFileSync(url, "utf8"), url);
764
+ } catch(e) {
765
+ ast = null;
766
+ }
767
+ AST_CACHE.set(url.href, ast);
674
768
  return ast;
675
769
  }
676
770
 
@@ -803,11 +897,14 @@ function pattern_names(p, out = [])
803
897
  * The `var`s a function body declares, hoisted to it (not those
804
898
  * of a function nested in it), and its own name for a function
805
899
  * expression: the names it binds besides its parameters, each
806
- * with the declarators of a `var` (null when it is not a plain
807
- * `var <name>`). Cached: a module asks for them once per name it
808
- * reads.
900
+ * with the declarators of a `var` and, for one declared by a
901
+ * destructuring, what the pattern reads (`var {title} = opts`:
902
+ * the pick of `title`, param_path()); null when a pattern the
903
+ * reader cannot follow declares it (an array pattern). Cached: a
904
+ * module asks for them once per name it reads.
809
905
  *
810
- * -> Map(name -> [VariableDeclarator] | null)
906
+ * -> Map(name -> {declarators: [VariableDeclarator],
907
+ * picks: Map(declarator -> {steps, default})} | null)
811
908
  ***************************************************************/
812
909
  const FN_NAMES = new WeakMap();
813
910
 
@@ -828,8 +925,16 @@ function function_scope_names(fn)
828
925
  for(let d of n.declarations) {
829
926
  for(let name of pattern_names(d.id)) {
830
927
  let plain = (d.id.type === "Identifier");
831
- let had = names.has(name)? names.get(name) : [];
832
- names.set(name, (plain && had)? [...had, d] : null);
928
+ let at = plain? null : param_path(d.id, name);
929
+ let had = names.has(name)? names.get(name) : {declarators: [], picks: new Map()};
930
+ if(!had || (!plain && !at)) {
931
+ names.set(name, null);
932
+ continue;
933
+ }
934
+ if(at) {
935
+ had.picks.set(d, {steps: at.steps, default: at.default});
936
+ }
937
+ names.set(name, {declarators: [...had.declarators, d], picks: had.picks});
833
938
  }
834
939
  }
835
940
  }
@@ -873,10 +978,16 @@ function function_name(fn, parent)
873
978
 
874
979
  /***************************************************************
875
980
  * The names a node that is not a function binds for what is inside
876
- * it: name -> {kind: "const", init} | {kind: "each", of} |
877
- * {kind: "var", declarators} (a plain `let <name>`; a `var` belongs
878
- * to its function, or here to the module) | {kind: "other"}.
879
- * Cached, like function_scope_names().
981
+ * it: name -> {kind: "const", init, steps, default} |
982
+ * {kind: "each", of} | {kind: "each_part", of} | {kind: "var",
983
+ * declarators, picks} (a `let`; a `var` belongs to its function, or
984
+ * here to the module) | {kind: "other", init}. A name declared by a
985
+ * destructuring reads `steps` from the value (`const {title} =
986
+ * opts`: `.title`; `picks` per declarator for a `let`); one an
987
+ * array pattern declares is "other" (its `init`, for
988
+ * param_flows()), and an element of a for...of that an array
989
+ * pattern destructures is "each_part". Cached, like
990
+ * function_scope_names().
880
991
  ***************************************************************/
881
992
  const SCOPE_NAMES = new WeakMap();
882
993
 
@@ -892,7 +1003,8 @@ function scope_names(a)
892
1003
  names.set(name, what);
893
1004
  } else if(had.kind === "var" && what.kind === "var") {
894
1005
  /* `var x = 1; ... var x = 2;`: one binding, two values */
895
- names.set(name, {kind: "var", decl: "var", declarators: [...had.declarators, ...what.declarators]});
1006
+ names.set(name, {kind: "var", decl: "var", declarators: [...had.declarators, ...what.declarators],
1007
+ picks: new Map([...had.picks, ...what.picks])});
896
1008
  }
897
1009
  };
898
1010
  let statements = [];
@@ -914,7 +1026,8 @@ function scope_names(a)
914
1026
  for(let d of a.left.declarations) {
915
1027
  for(let name of pattern_names(d.id)) {
916
1028
  let at = (a.type === "ForOfStatement")? param_path(d.id, name) : null;
917
- declare(name, at? {kind: "each", of: a.right, steps: at.steps} : {kind: "other"});
1029
+ declare(name, at? {kind: "each", of: a.right, steps: at.steps} :
1030
+ {kind: "each_part", of: a.right});
918
1031
  }
919
1032
  }
920
1033
  }
@@ -944,13 +1057,18 @@ function scope_names(a)
944
1057
  }
945
1058
  for(let v of d.declarations) {
946
1059
  let plain = (v.id.type === "Identifier");
947
- let what = {kind: "other"};
948
- if(plain && d.kind === "const" && v.init) {
949
- what = {kind: "const", init: v.init};
950
- } else if(plain && d.kind !== "const") {
951
- what = {kind: "var", decl: d.kind, declarators: [v]};
952
- }
953
1060
  for(let name of pattern_names(v.id)) {
1061
+ let at = plain? {steps: [], default: null} : param_path(v.id, name);
1062
+ let what = {kind: "other", init: v.init || null};
1063
+ if(at && d.kind === "const" && v.init) {
1064
+ what = {kind: "const", init: v.init, steps: at.steps, default: at.default, name};
1065
+ } else if(at && d.kind !== "const") {
1066
+ let picks = new Map();
1067
+ if(!plain) {
1068
+ picks.set(v, {steps: at.steps, default: at.default});
1069
+ }
1070
+ what = {kind: "var", decl: d.kind, declarators: [v], picks};
1071
+ }
954
1072
  declare(name, what);
955
1073
  }
956
1074
  }
@@ -975,11 +1093,14 @@ function scope_names(a)
975
1093
  * it: {steps, path, default}. `steps` are the properties read from
976
1094
  * the value bound to reach it (none for a plain name; member_step()
977
1095
  * form), `path` the same as text (".opts.title"), `default` the
978
- * value written for it when there is none. null when an array
979
- * pattern, a rest element or a computed key stands on the way.
1096
+ * value written for it when there is none. A rest element of an
1097
+ * object pattern holds the other properties of the object: it
1098
+ * reads what the object does. null when an array pattern, the rest
1099
+ * of an array or a computed key stands on the way.
980
1100
  *
981
1101
  * function f(msg = "fallback") msg: {path: "", default: "fallback"}
982
1102
  * function f({title, opts: {label}}) title: ".title", label: ".opts.label"
1103
+ * const {id, ...rest} = opts rest: "" (rest.title is opts.title)
983
1104
  ***************************************************************/
984
1105
  function param_path(p, name, steps = [], dflt = null)
985
1106
  {
@@ -996,8 +1117,15 @@ function param_path(p, name, steps = [], dflt = null)
996
1117
  return param_path(p.left, name, steps, p.right);
997
1118
  case "ObjectPattern":
998
1119
  for(let q of p.properties) {
999
- let key = (q.type === "RestElement")? null : prop_name(q);
1000
- let target = (q.type === "RestElement")? q : q.value;
1120
+ if(q.type === "RestElement") {
1121
+ let r = param_path(q.argument, name, steps, null);
1122
+ if(r) {
1123
+ return r;
1124
+ }
1125
+ continue;
1126
+ }
1127
+ let key = prop_name(q);
1128
+ let target = q.value;
1001
1129
  if(key === null) {
1002
1130
  if(pattern_names(target).includes(name)) {
1003
1131
  return null;
@@ -1019,10 +1147,15 @@ function param_path(p, name, steps = [], dflt = null)
1019
1147
  * What a name used in `scope` (the chain of ancestors of the use,
1020
1148
  * outermost first) is bound to:
1021
1149
  *
1022
- * {kind: "const", init, scope} a `const` with a value; `scope`
1150
+ * {kind: "const", init, steps, default, name, scope}
1151
+ * a `const` with a value; `scope`
1023
1152
  * is the chain down to the block
1024
1153
  * that declares it, where its
1025
- * value is read
1154
+ * value is read; `steps` what a
1155
+ * destructuring reads from the
1156
+ * value (`const {title} = opts`),
1157
+ * `default` the value written for
1158
+ * it there
1026
1159
  * {kind: "each", of, steps, scope}
1027
1160
  * an ELEMENT of `of`: the first
1028
1161
  * parameter of a callback of
@@ -1030,60 +1163,123 @@ function param_path(p, name, steps = [], dflt = null)
1030
1163
  * or the variable of a for...of;
1031
1164
  * `steps`, the properties read
1032
1165
  * from it by a destructuring
1033
- * {kind: "param", fn, name, index, path, default, scope}
1166
+ * {kind: "each_part", of, scope} part of an element of `of`
1167
+ * that an array pattern
1168
+ * destructures:
1169
+ * `Object.entries(o).forEach(([k, v]) => ...)`
1170
+ * {kind: "param", fn, name, index, path, default, rest, scope}
1034
1171
  * parameter `index` of the
1035
1172
  * function `fn`, called `name`;
1036
1173
  * `path` what a destructuring
1037
1174
  * reads from it ("" or
1038
1175
  * ".title"), `default` the
1039
1176
  * value written for it when the
1040
- * caller gives none
1041
- * {kind: "var", decl, name, declarators, scope}
1042
- * a plain `let` or `var` (`decl`
1043
- * says which): what it
1044
- * holds is never followed, except
1045
- * where a marker says what it
1046
- * holds (var_leaves()); `scope`
1047
- * is the chain down to the node
1048
- * that declares it
1177
+ * caller gives none; `rest`: a
1178
+ * rest parameter, the array of
1179
+ * the arguments from `index` on
1180
+ * {kind: "param_part", fn, name, index, scope}
1181
+ * part of parameter `index` that
1182
+ * an array pattern destructures:
1183
+ * no position can name it
1184
+ * {kind: "var", decl, name, declarators, picks, scope}
1185
+ * a `let` or `var` (`decl` says
1186
+ * which): what it holds is never
1187
+ * followed, except where a marker
1188
+ * says what it holds
1189
+ * (var_leaves()); `scope` is the
1190
+ * chain down to the node that
1191
+ * declares it; `picks`, what a
1192
+ * destructuring declarator reads
1049
1193
  * {kind: "function", fn, scope} a function declaration
1050
- * {kind: "import", source, imported}
1194
+ * {kind: "import", source, imported, scope}
1051
1195
  * an import: from module
1052
1196
  * `source`, its name there
1053
1197
  * (or "default", "*")
1054
- * {kind: "other"} anything else: a class, a
1055
- * name an array pattern or a
1056
- * rest element declares
1198
+ * {kind: "other", init, scope} anything else: a class, a
1199
+ * name an array pattern declares
1200
+ * (`init`, what it destructures),
1201
+ * a catch parameter
1057
1202
  * null declared nowhere: a global
1058
1203
  ***************************************************************/
1204
+ const FN_PARAMS = new WeakMap(); // a function -> Map(name -> index of the parameter that declares it)
1205
+
1206
+ function params_of(fn)
1207
+ {
1208
+ let m = FN_PARAMS.get(fn);
1209
+ if(!m) {
1210
+ m = new Map();
1211
+ for(let j = 0; j < fn.params.length; j++) {
1212
+ for(let n of pattern_names(fn.params[j])) {
1213
+ if(!m.has(n)) {
1214
+ m.set(n, j);
1215
+ }
1216
+ }
1217
+ }
1218
+ FN_PARAMS.set(fn, m);
1219
+ }
1220
+ return m;
1221
+ }
1222
+
1223
+ /* Does a parsed file declare a `var` anywhere? Most declare none, and
1224
+ * then no function body has to be searched for one */
1225
+ const HAS_VAR = new WeakMap();
1226
+
1227
+ function has_var(program)
1228
+ {
1229
+ if(!program || program.type !== "Program" || typeof program.code !== "string") {
1230
+ return true;
1231
+ }
1232
+ if(!HAS_VAR.has(program)) {
1233
+ HAS_VAR.set(program, /\bvar\b/.test(program.code));
1234
+ }
1235
+ return HAS_VAR.get(program);
1236
+ }
1237
+
1238
+ /* The nodes that bind names for what is inside them (scope_names()) */
1239
+ const SCOPE_TYPES = new Set(["Program", "BlockStatement", "StaticBlock", "SwitchStatement", "ForStatement",
1240
+ "ForOfStatement", "ForInStatement", "CatchClause", "ClassExpression"]);
1241
+ const FUNCTION_TYPES = new Set(["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"]);
1242
+
1059
1243
  function binding_of(name, scope)
1060
1244
  {
1245
+ let vars = has_var(scope[0]);
1061
1246
  for(let i = scope.length - 1; i >= 0; i--) {
1062
1247
  let a = scope[i];
1063
- if(FUNCTION_RE.test(a.type)) {
1248
+ if(!FUNCTION_TYPES.has(a.type)) {
1249
+ if(!SCOPE_TYPES.has(a.type)) {
1250
+ continue; // an expression, a call, a property: it binds nothing
1251
+ }
1252
+ } else {
1064
1253
  let parent = scope[i - 1];
1065
- for(let j = 0; j < a.params.length; j++) {
1066
- if(!pattern_names(a.params[j]).includes(name)) {
1067
- continue;
1068
- }
1069
- let at = param_path(a.params[j], name);
1070
- if(!at) {
1071
- return {kind: "other"};
1072
- }
1254
+ let j = params_of(a).get(name);
1255
+ if(j !== undefined) {
1256
+ let p = a.params[j];
1257
+ let at = param_path(p, name);
1073
1258
  if(j === 0 && parent && parent.type === "CallExpression" && parent.arguments[0] === a
1074
1259
  && parent.callee.type === "MemberExpression" && !parent.callee.computed
1075
1260
  && ITERATION_METHODS.has(parent.callee.property.name)) {
1076
- return {kind: "each", of: parent.callee.object, steps: at.steps,
1077
- scope: scope.slice(0, i - 1)};
1261
+ return at? {kind: "each", of: parent.callee.object, steps: at.steps,
1262
+ scope: scope.slice(0, i - 1)}
1263
+ : {kind: "each_part", of: parent.callee.object, scope: scope.slice(0, i - 1)};
1264
+ }
1265
+ let base = {fn: a, name: function_name(a, parent), index: j, scope: scope.slice(0, i + 1)};
1266
+ if(at) {
1267
+ return Object.assign({kind: "param", path: at.path, default: at.default, rest: false}, base);
1268
+ }
1269
+ if(p.type === "RestElement" && p.argument.type === "Identifier") {
1270
+ return Object.assign({kind: "param", path: "", default: null, rest: true}, base);
1078
1271
  }
1079
- return {kind: "param", fn: a, name: function_name(a, parent), index: j,
1080
- path: at.path, default: at.default, scope: scope.slice(0, i + 1)};
1272
+ return Object.assign({kind: "param_part"}, base);
1273
+ }
1274
+ if(!vars && !(a.type === "FunctionExpression" && a.id && a.id.name === name)) {
1275
+ continue;
1081
1276
  }
1082
1277
  let hoisted = function_scope_names(a);
1083
1278
  if(hoisted.has(name)) {
1084
- let declarators = hoisted.get(name);
1085
- return declarators? {kind: "var", decl: "var", name, declarators, scope: scope.slice(0, i + 1)}
1086
- : {kind: "other"};
1279
+ let h = hoisted.get(name);
1280
+ return h? {kind: "var", decl: "var", name, declarators: h.declarators, picks: h.picks,
1281
+ scope: scope.slice(0, i + 1)}
1282
+ : {kind: "other", init: null, scope: scope.slice(0, i + 1)};
1087
1283
  }
1088
1284
  continue;
1089
1285
  }
@@ -1092,22 +1288,537 @@ function binding_of(name, scope)
1092
1288
  continue;
1093
1289
  }
1094
1290
  if(d.kind === "const") {
1095
- return {kind: "const", init: d.init, scope: scope.slice(0, i + 1)};
1291
+ return {kind: "const", init: d.init, steps: d.steps || [], default: d.default || null,
1292
+ name: d.name || name, scope: scope.slice(0, i + 1)};
1096
1293
  }
1097
1294
  if(d.kind === "each") {
1098
1295
  return {kind: "each", of: d.of, steps: d.steps || [], scope: scope.slice(0, i)};
1099
1296
  }
1297
+ if(d.kind === "each_part") {
1298
+ return {kind: "each_part", of: d.of, scope: scope.slice(0, i)};
1299
+ }
1100
1300
  if(d.kind === "var") {
1101
- return {kind: "var", decl: d.decl, name, declarators: d.declarators, scope: scope.slice(0, i + 1)};
1301
+ return {kind: "var", decl: d.decl, name, declarators: d.declarators, picks: d.picks || new Map(),
1302
+ scope: scope.slice(0, i + 1)};
1102
1303
  }
1103
1304
  if(d.kind === "function") {
1104
1305
  return {kind: "function", fn: d.fn, scope: scope.slice(0, i + 1)};
1105
1306
  }
1106
- return d;
1307
+ if(d.kind === "import") {
1308
+ return Object.assign({}, d, {scope: scope.slice(0, i + 1)});
1309
+ }
1310
+ return {kind: "other", init: d.init || null, scope: scope.slice(0, i + 1)};
1311
+ }
1312
+ return null;
1313
+ }
1314
+
1315
+ /***************************************************************
1316
+ * The `arguments` of the function a use is in (the nearest one
1317
+ * that is not an arrow: an arrow has none of its own), as a
1318
+ * binding: {kind: "arguments", fn, name, scope}. null outside one.
1319
+ ***************************************************************/
1320
+ function arguments_binding(scope)
1321
+ {
1322
+ for(let i = scope.length - 1; i >= 0; i--) {
1323
+ let a = scope[i];
1324
+ if(a.type === "FunctionDeclaration" || a.type === "FunctionExpression") {
1325
+ return {kind: "arguments", fn: a, name: function_name(a, scope[i - 1]), scope: scope.slice(0, i + 1)};
1326
+ }
1327
+ }
1328
+ return null;
1329
+ }
1330
+
1331
+
1332
+ /***************************
1333
+ * Modules
1334
+ ***************************/
1335
+
1336
+
1337
+ /***************************************************************
1338
+ * The file an import source names, seen from the file `from` (a
1339
+ * URL): a relative path to a JavaScript file (`./x.js`, `../y.mjs`;
1340
+ * `.js` and `/index.js` tried when it names no file), a module of
1341
+ * this library (`@yuneta/gobj-ui/src/x.js`) or its barrel
1342
+ * (`@yuneta/gobj-ui`). null for anything else -- a package
1343
+ * (i18next, gobj-js), a stylesheet, a json --: the reader does not
1344
+ * read it.
1345
+ ***************************************************************/
1346
+ const MODULE_URLS = new Map(); // "<from>\n<source>" -> URL | null
1347
+
1348
+ function module_url(from, source)
1349
+ {
1350
+ if(typeof source !== "string") {
1351
+ return null;
1352
+ }
1353
+ let m = source.match(/^@yuneta\/gobj-ui\/src\/([\w.-]+\.js)$/);
1354
+ if(m) {
1355
+ return new URL(m[1], LIB_SRC);
1356
+ }
1357
+ if(/^@yuneta\/gobj-ui(?:\/index\.js)?$/.test(source)) {
1358
+ return LIB_INDEX;
1359
+ }
1360
+ if(!from || !/^\.\.?\//.test(source)) {
1361
+ return null;
1362
+ }
1363
+ let id = `${from.href}\n${source}`;
1364
+ if(MODULE_URLS.has(id)) {
1365
+ return MODULE_URLS.get(id);
1366
+ }
1367
+ let found = null;
1368
+ for(let candidate of [source, source + ".js", source + "/index.js"]) {
1369
+ if(!/\.m?js$/.test(candidate)) {
1370
+ continue; // a stylesheet, a json: no module the reader reads
1371
+ }
1372
+ let u = new URL(candidate, from);
1373
+ try {
1374
+ if(statSync(u).isFile()) {
1375
+ found = u;
1376
+ break;
1377
+ }
1378
+ } catch(e) {
1379
+ continue;
1380
+ }
1381
+ }
1382
+ MODULE_URLS.set(id, found);
1383
+ return found;
1384
+ }
1385
+
1386
+ /***************************************************************
1387
+ * What a module exports as `name`, followed through re-exports
1388
+ * (`export {a as b} from "./x.js"`, `export * from`, a local
1389
+ * `export {x}` of an import) into the module that declares it:
1390
+ *
1391
+ * {kind: "fn", fn, scope, name, url}
1392
+ * a function: a declaration, or a
1393
+ * const bound to one (`name`, the
1394
+ * one it is declared with)
1395
+ * {kind: "const", init, steps, default, scope, url}
1396
+ * a const bound to anything else
1397
+ * {kind: "external", source, imported}
1398
+ * an import of a package the
1399
+ * reader does not read (i18next's
1400
+ * t, a builder of gobj-js)
1401
+ * null anything else: a `let`, a class,
1402
+ * a name it does not export, a
1403
+ * module that cannot be read
1404
+ *
1405
+ * `scope` is the chain down to the declaration (the module).
1406
+ ***************************************************************/
1407
+ const EXPORTS = new Map(); // "<url>#<name>" -> what it is
1408
+
1409
+ function module_export(url, name, seen = new Set())
1410
+ {
1411
+ let id = `${url.href}#${name}`;
1412
+ if(EXPORTS.has(id)) {
1413
+ return EXPORTS.get(id);
1414
+ }
1415
+ if(seen.has(id)) {
1416
+ return null; // a cycle of re-exports
1417
+ }
1418
+ seen.add(id);
1419
+ let ast = load_ast(url);
1420
+ let found = ast? find_export(ast, url, name, seen) : null;
1421
+ EXPORTS.set(id, found);
1422
+ return found;
1423
+ }
1424
+
1425
+ function find_export(ast, url, name, seen)
1426
+ {
1427
+ let local = (local_name) => {
1428
+ let b = binding_of(local_name, [ast]);
1429
+ return b? declared_value(b, url, seen) : null;
1430
+ };
1431
+ for(let node of ast.body) {
1432
+ if(node.type === "ExportDefaultDeclaration") {
1433
+ if(name !== "default") {
1434
+ continue;
1435
+ }
1436
+ let d = node.declaration;
1437
+ if(FUNCTION_RE.test(d.type)) {
1438
+ return {kind: "fn", fn: d, scope: [ast], name: d.id? d.id.name : "default", url};
1439
+ }
1440
+ if(d.type === "Identifier") {
1441
+ return local(d.name);
1442
+ }
1443
+ return {kind: "const", init: d, steps: [], default: null, scope: [ast], url};
1444
+ }
1445
+ if(node.type === "ExportAllDeclaration") {
1446
+ if(node.exported) {
1447
+ continue; // `export * as ns from`: a namespace, not a value the reader reads
1448
+ }
1449
+ let target = module_url(url, node.source.value);
1450
+ let r = target? module_export(target, name, seen) : null;
1451
+ if(r) {
1452
+ return r;
1453
+ }
1454
+ continue;
1455
+ }
1456
+ if(node.type !== "ExportNamedDeclaration") {
1457
+ continue;
1458
+ }
1459
+ if(node.declaration) {
1460
+ let decl = node.declaration;
1461
+ if(decl.type === "FunctionDeclaration" && decl.id && decl.id.name === name) {
1462
+ return local(name);
1463
+ }
1464
+ if(decl.type === "VariableDeclaration" && decl.declarations.some((d) => pattern_names(d.id).includes(name))) {
1465
+ return local(name);
1466
+ }
1467
+ continue;
1468
+ }
1469
+ for(let spec of node.specifiers) {
1470
+ let exported = spec.exported.name || spec.exported.value;
1471
+ if(exported !== name) {
1472
+ continue;
1473
+ }
1474
+ let inner = spec.local.name || spec.local.value;
1475
+ if(!node.source) {
1476
+ return local(inner);
1477
+ }
1478
+ let target = module_url(url, node.source.value);
1479
+ if(!target) {
1480
+ return {kind: "external", source: node.source.value, imported: inner};
1481
+ }
1482
+ return module_export(target, inner, seen);
1483
+ }
1484
+ }
1485
+ return null;
1486
+ }
1487
+
1488
+ /***************************************************************
1489
+ * What a binding of a module's top level holds, for
1490
+ * module_export(): a function, a const, or an import followed
1491
+ * into its own module
1492
+ ***************************************************************/
1493
+ function declared_value(b, url, seen)
1494
+ {
1495
+ if(b.kind === "function") {
1496
+ return {kind: "fn", fn: b.fn, scope: b.scope, name: b.fn.id.name, url};
1497
+ }
1498
+ if(b.kind === "const") {
1499
+ if(b.steps.length === 0 && FUNCTION_RE.test(b.init.type)) {
1500
+ return {kind: "fn", fn: b.init, scope: b.scope, name: b.name, url};
1501
+ }
1502
+ return {kind: "const", init: b.init, steps: b.steps, default: b.default, scope: b.scope, url};
1503
+ }
1504
+ if(b.kind === "import") {
1505
+ return import_target(b, seen);
1506
+ }
1507
+ return null;
1508
+ }
1509
+
1510
+ /***************************************************************
1511
+ * What an import binding names (binding_of(): {kind: "import"}),
1512
+ * as module_export() says it; a namespace (`import * as ns`) is
1513
+ * {kind: "namespace", url}
1514
+ ***************************************************************/
1515
+ function import_target(b, seen = new Set())
1516
+ {
1517
+ let from = (b.scope && b.scope.length > 0)? (PROGRAM_URL.get(b.scope[0]) || null) : null;
1518
+ let url = module_url(from, b.source);
1519
+ if(!url) {
1520
+ return {kind: "external", source: b.source, imported: b.imported};
1521
+ }
1522
+ if(b.imported === "*") {
1523
+ return {kind: "namespace", url};
1524
+ }
1525
+ return module_export(url, b.imported, seen);
1526
+ }
1527
+
1528
+ /***************************************************************
1529
+ * The module of this library a URL is (its name in src/), or null
1530
+ ***************************************************************/
1531
+ function lib_module_of(url)
1532
+ {
1533
+ if(!url || !url.href.startsWith(LIB_SRC.href)) {
1534
+ return null;
1535
+ }
1536
+ let rest = url.href.slice(LIB_SRC.href.length);
1537
+ return /^[\w.-]+\.js$/.test(rest)? rest : null;
1538
+ }
1539
+
1540
+ /* The file a chain of ancestors is in (its first node, the module) */
1541
+ function url_of(scope)
1542
+ {
1543
+ return (scope && scope.length > 0)? (PROGRAM_URL.get(scope[0]) || null) : null;
1544
+ }
1545
+
1546
+
1547
+ /***************************
1548
+ * What a call calls
1549
+ ***************************/
1550
+
1551
+
1552
+ /* The names a translator goes by: i18next's t(), and the tr() an
1553
+ * app or a module wraps it in */
1554
+ const TRANSLATOR_NAMES = new Set(["t", "tr"]);
1555
+ const TRANSLATOR = Object.freeze({kind: "t", name: "t"});
1556
+
1557
+ /***************************************************************
1558
+ * What a callee IS, as far as the source says it:
1559
+ *
1560
+ * {kind: "t"} a TRANSLATOR: a name `t` or `tr`
1561
+ * that binds no function the reader
1562
+ * reads (a parameter, a `let`, a
1563
+ * global), a member `.t` / `.tr`
1564
+ * (`i18next.t`, `opts.t`), i18next's
1565
+ * `t` imported under any name
1566
+ * (`import {t as tt} from "i18next"`),
1567
+ * or an alias of one of those: a
1568
+ * const (`const translate = t`), a
1569
+ * `.bind()` (`i18next.t.bind(i18next)`),
1570
+ * a re-export of a module the reader
1571
+ * reads
1572
+ * {kind: "fn", fn, scope, name, url}
1573
+ * a FUNCTION the reader reads: a
1574
+ * declaration, a const bound to one
1575
+ * (or a `let` given nothing else), an
1576
+ * import of one followed into the
1577
+ * module that declares it, a member of
1578
+ * a namespace import, a property of a
1579
+ * const object literal holding one, or
1580
+ * an alias of one of those. `name` is
1581
+ * the one it is DECLARED with (the
1582
+ * table's), `url` the file that
1583
+ * declares it
1584
+ * null anything else: a parameter or a
1585
+ * `let` of another name, a method of
1586
+ * an object, a function of a package
1587
+ * the reader does not read
1588
+ ***************************************************************/
1589
+ function callee_of(node, scope, depth = 0)
1590
+ {
1591
+ if(!node || depth > MAX_DEPTH) {
1592
+ return null;
1593
+ }
1594
+ switch(node.type) {
1595
+ case "ChainExpression":
1596
+ return callee_of(node.expression, scope, depth + 1);
1597
+
1598
+ case "Identifier": {
1599
+ let named = TRANSLATOR_NAMES.has(node.name)? TRANSLATOR : null;
1600
+ let b = binding_of(node.name, scope);
1601
+ if(!b) {
1602
+ return named;
1603
+ }
1604
+ switch(b.kind) {
1605
+ case "function":
1606
+ return {kind: "fn", fn: b.fn, scope: b.scope, name: b.fn.id.name, url: url_of(b.scope)};
1607
+ case "const":
1608
+ if(b.steps.length > 0) {
1609
+ return named;
1610
+ }
1611
+ if(FUNCTION_RE.test(b.init.type)) {
1612
+ return {kind: "fn", fn: b.init, scope: b.scope, name: b.name, url: url_of(b.scope)};
1613
+ }
1614
+ return callee_of(b.init, b.scope, depth + 1) || named;
1615
+ case "var": {
1616
+ let fn = var_function(b);
1617
+ return fn? {kind: "fn", fn, scope: b.scope, name: b.name, url: url_of(b.scope)} : named;
1618
+ }
1619
+ case "import":
1620
+ return exported_callee(import_target(b), depth) || named;
1621
+ default:
1622
+ return named;
1623
+ }
1624
+ }
1625
+
1626
+ case "MemberExpression": {
1627
+ let step = member_step(node);
1628
+ if(step.prop === null) {
1629
+ return null;
1630
+ }
1631
+ if(node.object.type === "Identifier") {
1632
+ let b = binding_of(node.object.name, scope);
1633
+ let map = null; // a const object literal the object is: {init, scope}
1634
+ if(b && b.kind === "import" && b.imported === "*") {
1635
+ let ns = import_target(b);
1636
+ if(ns && ns.kind === "namespace") {
1637
+ let r = exported_callee(module_export(ns.url, step.prop), depth);
1638
+ if(r) {
1639
+ return r;
1640
+ }
1641
+ }
1642
+ } else if(b && b.kind === "import") {
1643
+ let x = import_target(b);
1644
+ if(x && x.kind === "const" && x.steps.length === 0 && x.init.type === "ObjectExpression") {
1645
+ map = {init: x.init, scope: x.scope};
1646
+ }
1647
+ } else if(b && b.kind === "const" && b.steps.length === 0 && b.init.type === "ObjectExpression") {
1648
+ map = {init: b.init, scope: b.scope};
1649
+ }
1650
+ if(map) {
1651
+ /* A map: `const API = {info: yui_shell_show_info}; API.info(...)` */
1652
+ let p = map.init.properties.find((q) => q.type === "Property" && prop_name(q) === step.prop
1653
+ && q.kind === "init" && !q.computed);
1654
+ if(p) {
1655
+ let r = callee_of(p.value, [...map.scope, map.init, p], depth + 1);
1656
+ if(r) {
1657
+ return r;
1658
+ }
1659
+ }
1660
+ }
1661
+ }
1662
+ return TRANSLATOR_NAMES.has(step.prop)? TRANSLATOR : null;
1663
+ }
1664
+
1665
+ case "CallExpression":
1666
+ /* f.bind(x) is f */
1667
+ if(node.callee.type === "MemberExpression" && !node.callee.computed
1668
+ && node.callee.property.type === "Identifier" && node.callee.property.name === "bind") {
1669
+ return callee_of(node.callee.object, scope, depth + 1);
1670
+ }
1671
+ return null;
1672
+
1673
+ default:
1674
+ return null;
1675
+ }
1676
+ }
1677
+
1678
+ /***************************************************************
1679
+ * What an export (module_export()) is as a callee: a function, the
1680
+ * i18next `t`, or a const that is an alias of one
1681
+ ***************************************************************/
1682
+ function exported_callee(x, depth)
1683
+ {
1684
+ if(!x) {
1685
+ return null;
1686
+ }
1687
+ if(x.kind === "fn") {
1688
+ return x;
1689
+ }
1690
+ if(x.kind === "external") {
1691
+ return (x.source === "i18next" && x.imported === "t")? TRANSLATOR : null;
1692
+ }
1693
+ if(x.kind === "const" && x.steps.length === 0) {
1694
+ return callee_of(x.init, x.scope, depth + 1);
1107
1695
  }
1108
1696
  return null;
1109
1697
  }
1110
1698
 
1699
+ /***************************************************************
1700
+ * The function a `let` / `var` holds when it is given one and
1701
+ * nothing else (`let tr = (key) => ...`), or null
1702
+ ***************************************************************/
1703
+ const VAR_FUNCTIONS = new WeakMap(); // a declarator -> the function it holds, or null
1704
+
1705
+ function var_function(b)
1706
+ {
1707
+ if(b.declarators.length !== 1 || b.picks.size > 0) {
1708
+ return null;
1709
+ }
1710
+ let d = b.declarators[0];
1711
+ if(!d.init || !FUNCTION_RE.test(d.init.type)) {
1712
+ return null;
1713
+ }
1714
+ if(!VAR_FUNCTIONS.has(d)) {
1715
+ let {values, unknown} = assigned_values(b);
1716
+ VAR_FUNCTIONS.set(d, (values.length === 1 && unknown.length === 0)? d.init : null);
1717
+ }
1718
+ return VAR_FUNCTIONS.get(d);
1719
+ }
1720
+
1721
+ /***************************************************************
1722
+ * What a call calls (callee_of()), and the arguments the callee
1723
+ * receives: `f(a)` a; `f.call(x, a)` a; `f.apply(x, [a, b])` a and
1724
+ * b (null when `.apply()` hands an array the source does not write
1725
+ * out). Cached per call: what it calls does not change.
1726
+ *
1727
+ * -> {target, args} | null
1728
+ ***************************************************************/
1729
+ const CALL_TARGETS = new WeakMap();
1730
+
1731
+ function call_target(call, scope)
1732
+ {
1733
+ if(CALL_TARGETS.has(call)) {
1734
+ return CALL_TARGETS.get(call);
1735
+ }
1736
+ let r = null;
1737
+ let callee = call.callee;
1738
+ let method = (callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier")?
1739
+ callee.property.name : null;
1740
+ if(method === "call" || method === "apply") {
1741
+ let inner = callee_of(callee.object, scope);
1742
+ if(inner) {
1743
+ let args = call.arguments.slice(1);
1744
+ if(method === "apply") {
1745
+ let list = call.arguments[1];
1746
+ args = !list? [] : (list.type === "ArrayExpression")? list.elements : null;
1747
+ }
1748
+ r = {target: inner, args};
1749
+ }
1750
+ }
1751
+ if(!r) {
1752
+ let target = callee_of(callee, scope);
1753
+ r = target? {target, args: call.arguments} : null;
1754
+ }
1755
+ CALL_TARGETS.set(call, r);
1756
+ return r;
1757
+ }
1758
+
1759
+ /***************************************************************
1760
+ * What kind of call a call is, for the reader:
1761
+ *
1762
+ * {kind: "key", entry, target, args}
1763
+ * a call of a KEY FUNCTION of `table`
1764
+ * (I18N_KEY_FUNCTIONS by default): a
1765
+ * function the table names in its
1766
+ * module, however the call reaches it
1767
+ * -- its name, an import under any
1768
+ * name, a re-export, an alias, a map,
1769
+ * `.call()`, `.apply()`
1770
+ * {kind: "t", target, args} a call of a TRANSLATOR (callee_of()),
1771
+ * or of a function called t or tr that
1772
+ * the table does not name
1773
+ * {kind: "fn", target, args} a call of another function the
1774
+ * reader reads
1775
+ * null anything else
1776
+ ***************************************************************/
1777
+ function call_kind(call, scope, table = I18N_KEY_FUNCTIONS)
1778
+ {
1779
+ let r = call_target(call, scope);
1780
+ if(!r) {
1781
+ /* A name a module of the library does not bind, called in it: its
1782
+ * function of the table (a fixture of a test; real code declares it) */
1783
+ let module = lib_module_of(url_of(scope));
1784
+ let fns = module? key_fn_index(table).by_module.get(module) : null;
1785
+ let entry = (fns && call.callee.type === "Identifier" && !binding_of(call.callee.name, scope))?
1786
+ (fns.get(call.callee.name) || null) : null;
1787
+ return entry? {kind: "key", entry, target: {kind: "unbound", name: entry.name}, args: call.arguments} : null;
1788
+ }
1789
+ let {target, args} = r;
1790
+ if(target.kind === "t") {
1791
+ return {kind: "t", target, args};
1792
+ }
1793
+ let module = lib_module_of(target.url);
1794
+ let fns = module? key_fn_index(table).by_module.get(module) : null;
1795
+ let entry = fns? (fns.get(target.name) || null) : null;
1796
+ if(entry) {
1797
+ return {kind: "key", entry, target, args};
1798
+ }
1799
+ if(TRANSLATOR_NAMES.has(target.name)) {
1800
+ return {kind: "t", target, args};
1801
+ }
1802
+ return {kind: "fn", target, args};
1803
+ }
1804
+
1805
+ /***************************************************************
1806
+ * Is a call one of a translator -- t(), or a function called t or
1807
+ * tr (the library's tr() translate too)? Its value is a
1808
+ * TRANSLATED text, never a key.
1809
+ ***************************************************************/
1810
+ function is_translator_call(n, scope)
1811
+ {
1812
+ while(n && n.type === "ChainExpression") {
1813
+ n = n.expression;
1814
+ }
1815
+ if(!n || n.type !== "CallExpression") {
1816
+ return false;
1817
+ }
1818
+ let c = call_kind(n, scope || []);
1819
+ return !!c && (c.kind === "t" || TRANSLATOR_NAMES.has(c.target.name));
1820
+ }
1821
+
1111
1822
 
1112
1823
  /***************************
1113
1824
  * What a value can be
@@ -1123,7 +1834,11 @@ const MAX_DEPTH = 24;
1123
1834
  *
1124
1835
  * {kind: "value", node} an expression it cannot follow
1125
1836
  * further: a literal, an object
1126
- * or array literal, a call
1837
+ * or array literal, a call (one
1838
+ * whose returns mix keys and
1839
+ * what the reader cannot follow
1840
+ * carries those keys in
1841
+ * `partial`: local_call_values())
1127
1842
  * {kind: "nothing", node} `undefined`, or the property of
1128
1843
  * an object that has none
1129
1844
  * {kind: "param", node, binding, path}
@@ -1132,7 +1847,8 @@ const MAX_DEPTH = 24;
1132
1847
  * from it (".title", "[].label",
1133
1848
  * a destructuring included)
1134
1849
  * {kind: "unknown", node} a `let`, a `var` (with its
1135
- * `binding`), an import, a
1850
+ * `binding`), an import of what
1851
+ * the reader does not read, a
1136
1852
  * property of something unknown
1137
1853
  *
1138
1854
  * with `scope` (the ancestors of `node`, to read it further) and
@@ -1147,18 +1863,27 @@ const MAX_DEPTH = 24;
1147
1863
  * - every branch of `?:`, of `||` and `??`, the right one of `&&`
1148
1864
  * (the left one is a condition: when it is the value, it is
1149
1865
  * falsy, not a key);
1150
- * - a `const`, to its value, read in the scope that declares it;
1866
+ * - a `const`, to its value, read in the scope that declares it,
1867
+ * and what a destructuring reads from it (`const {title} =
1868
+ * opts`, with its default);
1869
+ * - an import of a const, into the module it names, when the
1870
+ * reader reads that module -- a file of the app, a module of
1871
+ * this library (module_export());
1151
1872
  * - a property of an object literal it reached (`DEFAULTS.ok`; for
1152
- * a computed one, `NOTES[verdict]`, every value it holds), and an
1873
+ * a computed one, `NOTES[verdict]`, every value it holds), of a
1874
+ * copy of one (`{...opts}`, `Object.assign({}, opts)`), and an
1153
1875
  * element of an array literal (`LIST[i]`);
1154
1876
  * - an ELEMENT of an array literal it reached: the parameter of a
1155
1877
  * `.map()`, `.forEach()`, ... callback over it, the variable of a
1156
1878
  * for...of over it (and what a destructuring there reads from
1157
1879
  * it: `for(const {key} of ROWS)`);
1158
- * - the default of a parameter (`function f(msg = "fallback")`),
1159
- * besides the parameter itself;
1160
- * - a call of a function of the same module whose every return is
1161
- * known (local_call_values());
1880
+ * - a parameter, however it is read: optional chaining
1881
+ * (`opts?.title`), `arguments[1]`, an element of a rest
1882
+ * parameter by its index (`rest[0]`); and its default
1883
+ * (`function f(msg = "fallback")`), besides the parameter itself;
1884
+ * - a call of a function the reader reads -- of the module, or
1885
+ * imported from a module it reads, by `.call()` / `.apply()` too
1886
+ * -- whose every return is known (local_call_values());
1162
1887
  * - Object.freeze(), String(x) and `${x}` (a template that is only
1163
1888
  * that), which change nothing it reads.
1164
1889
  *
@@ -1186,16 +1911,26 @@ function expand(node, ctx, depth = 0)
1186
1911
  return [...(node.operator !== "&&"? expand(node.left, here, depth + 1) : []),
1187
1912
  ...expand(node.right, here, depth + 1)];
1188
1913
 
1914
+ case "ChainExpression":
1915
+ /* opts?.title, f?.(x): what the expression inside is (or nothing) */
1916
+ return expand(node.expression, here, depth + 1);
1917
+
1189
1918
  case "Identifier": {
1190
1919
  if(node.name === "undefined") {
1191
1920
  return [item("nothing")];
1192
1921
  }
1922
+ if(node.name === "arguments") {
1923
+ let a = arguments_binding(ctx.scope);
1924
+ if(a) {
1925
+ return [item("unknown", {binding: a})];
1926
+ }
1927
+ }
1193
1928
  let b = binding_of(node.name, ctx.scope);
1194
1929
  if(!b) {
1195
1930
  return [item("unknown")];
1196
1931
  }
1197
1932
  if(b.kind === "const") {
1198
- return expand(b.init, {scope: b.scope, via: via, off: true}, depth + 1);
1933
+ return const_values(b, node, via, depth);
1199
1934
  }
1200
1935
  if(b.kind === "each") {
1201
1936
  let out = elements_of(b.of, {scope: b.scope, via: via, off: true}, depth + 1);
@@ -1214,11 +1949,21 @@ function expand(node, ctx, depth = 0)
1214
1949
  if(b.kind === "var") {
1215
1950
  return [item("unknown", {binding: b, var_root: {binding: b, steps: []}})];
1216
1951
  }
1217
- return [item("unknown")];
1952
+ if(b.kind === "import") {
1953
+ let x = import_target(b);
1954
+ if(x && x.kind === "const") {
1955
+ return const_values(x, node, via, depth);
1956
+ }
1957
+ }
1958
+ return [item("unknown", {binding: b})];
1218
1959
  }
1219
1960
 
1220
1961
  case "MemberExpression": {
1221
1962
  let step = member_step(node);
1963
+ let special = member_values(node, step, ctx, via, item, depth);
1964
+ if(special) {
1965
+ return special;
1966
+ }
1222
1967
  let out = [];
1223
1968
  for(let o of expand(node.object, here, depth + 1)) {
1224
1969
  out.push(...step_values(o, step, node, depth + 1));
@@ -1226,7 +1971,7 @@ function expand(node, ctx, depth = 0)
1226
1971
  return out;
1227
1972
  }
1228
1973
 
1229
- case "CallExpression":
1974
+ case "CallExpression": {
1230
1975
  if(is_object_call(node, "freeze") && node.arguments.length === 1) {
1231
1976
  return expand(node.arguments[0], here, depth + 1);
1232
1977
  }
@@ -1234,7 +1979,12 @@ function expand(node, ctx, depth = 0)
1234
1979
  && node.arguments.length === 1 && binding_of("String", ctx.scope) === null) {
1235
1980
  return expand(node.arguments[0], here, depth + 1);
1236
1981
  }
1237
- return local_call_values(node, ctx, via, depth) || [item("value")];
1982
+ let r = local_call_values(node, ctx, via);
1983
+ if(r && r.items) {
1984
+ return r.items;
1985
+ }
1986
+ return [item("value", (r && r.partial)? {partial: r.partial} : {})];
1987
+ }
1238
1988
 
1239
1989
  case "TemplateLiteral":
1240
1990
  /* `${x}` and nothing else: x as a string */
@@ -1248,40 +1998,178 @@ function expand(node, ctx, depth = 0)
1248
1998
  }
1249
1999
  }
1250
2000
 
2001
+ /***************************************************************
2002
+ * The values of a const (a binding of binding_of(), or a const
2003
+ * module_export() found in another module): its value read where it
2004
+ * is declared, the steps a destructuring reads from it, its default
2005
+ ***************************************************************/
2006
+ function const_values(b, node, via, depth)
2007
+ {
2008
+ let ctx = {scope: b.scope, via: via, off: true};
2009
+ let out = expand(b.init, ctx, depth + 1);
2010
+ for(let step of b.steps || []) {
2011
+ out = out.flatMap((o) => step_values(o, step, node, depth + 1));
2012
+ }
2013
+ if(b.default) {
2014
+ out.push(...expand(b.default, ctx, depth + 1));
2015
+ }
2016
+ return out;
2017
+ }
2018
+
2019
+ /***************************************************************
2020
+ * The members expand() reads by what their object IS, not by what
2021
+ * it holds: `arguments[1]` (that parameter of the function) and
2022
+ * `ns.NAME` of `import * as ns` (what the module exports as NAME).
2023
+ * null for any other member.
2024
+ ***************************************************************/
2025
+ function member_values(node, step, ctx, via, item, depth)
2026
+ {
2027
+ if(node.object.type !== "Identifier") {
2028
+ return null;
2029
+ }
2030
+ if(node.object.name === "arguments" && node.computed && node.property.type === "Literal"
2031
+ && Number.isInteger(node.property.value) && node.property.value >= 0) {
2032
+ let a = arguments_binding(ctx.scope);
2033
+ if(!a) {
2034
+ return null;
2035
+ }
2036
+ let binding = {kind: "param", fn: a.fn, name: a.name, index: node.property.value, path: "",
2037
+ default: null, rest: false, scope: a.scope};
2038
+ return [item("param", {binding, path: ""})];
2039
+ }
2040
+ if(step.prop === null) {
2041
+ return null;
2042
+ }
2043
+ let b = binding_of(node.object.name, ctx.scope);
2044
+ if(!b || b.kind !== "import" || b.imported !== "*") {
2045
+ return null;
2046
+ }
2047
+ let ns = import_target(b);
2048
+ let x = (ns && ns.kind === "namespace")? module_export(ns.url, step.prop) : null;
2049
+ if(x && x.kind === "const") {
2050
+ return const_values(x, node, via, depth);
2051
+ }
2052
+ return [item("unknown")];
2053
+ }
2054
+
1251
2055
  /* The leaves the reader KNOWS: no key hides behind them */
1252
2056
  const KNOWN = new Set(["key", "nothing", "dom"]);
1253
2057
 
1254
2058
  /***************************************************************
1255
- * What a call of a function of the same module returns, when every
1256
- * value it returns is known (classify(): a key, nothing, DOM): the
1257
- * call is read as those values -- `t(pick())`, `pick` returning
1258
- * "a" or "b", asks for both. null otherwise (a return the reader
1259
- * cannot follow, a parameter of the callee, none at all): the call
1260
- * stays a value it cannot follow. Only a plain call of a name bound
1261
- * to a function declaration or to a const function; not an async
1262
- * function, not a generator.
2059
+ * What a call of a function the reader reads (call_kind(): a
2060
+ * function of the module, or one imported from a module the reader
2061
+ * reads, by `.call()` / `.apply()` too) returns:
2062
+ *
2063
+ * {items} every value it returns is known (classify(): a
2064
+ * key, nothing, DOM): the call is read as those
2065
+ * values -- `t(pick())`, `pick` returning "a" or
2066
+ * "b", asks for both
2067
+ * {partial} some are not (a parameter, a call, data): the
2068
+ * call stays a value the reader cannot follow, and
2069
+ * `partial` holds the keys it also returns, for the
2070
+ * check of a marker that stands on it
2071
+ * null anything else: a call of something the reader
2072
+ * does not read, an async function, a generator, a
2073
+ * function being read already (a recursion)
2074
+ ***************************************************************/
2075
+ function local_call_values(call, ctx, via)
2076
+ {
2077
+ let c = call_kind(call, ctx.scope);
2078
+ if(!c || !c.target || c.target.kind !== "fn") {
2079
+ return null;
2080
+ }
2081
+ let r = fn_returns(c.target);
2082
+ if(!r) {
2083
+ return null;
2084
+ }
2085
+ if(r.complete) {
2086
+ return {items: r.items.map((it) => Object.assign({}, it, {via}))};
2087
+ }
2088
+ return (r.keys.length > 0)? {partial: r.keys} : null;
2089
+ }
2090
+
2091
+ /***************************************************************
2092
+ * What a function returns, read ONCE: every `return` expanded in
2093
+ * the scope that declares the function -- never in that of a call
2094
+ * of it, which is what makes the answer a property of the function
2095
+ * -- and cached:
2096
+ *
2097
+ * -> {items, complete, keys} | null
2098
+ *
2099
+ * `complete`: every value is known; `keys`, the keys among them
2100
+ * (those of a call it returns that mixes keys and data included).
2101
+ * A function that is being read already answers null, and a call
2102
+ * of it is a value the reader cannot follow: the reader does not
2103
+ * follow a recursion. So a recursive helper costs one read, not one
2104
+ * per path through it: 7.26.29 read a function again at every call
2105
+ * of it, `label_of(n)` calling itself in two branches took more
2106
+ * than two minutes, and in three it never ended (REV-CLOUD V22).
2107
+ * A function that is not a plain one (async, a generator) is not
2108
+ * followed.
1263
2109
  ***************************************************************/
1264
- function local_call_values(call, ctx, via, depth)
2110
+ const FN_RETURNS = new WeakMap();
2111
+ const FN_READING = new Set();
2112
+
2113
+ function fn_returns(target)
1265
2114
  {
1266
- if(call.callee.type !== "Identifier") {
2115
+ let fn = target.fn;
2116
+ if(FN_RETURNS.has(fn)) {
2117
+ return FN_RETURNS.get(fn);
2118
+ }
2119
+ if(FN_READING.has(fn) || fn.async || fn.generator) {
1267
2120
  return null;
1268
2121
  }
1269
- let b = binding_of(call.callee.name, ctx.scope);
1270
- let fn = null;
1271
- if(b && b.kind === "function") {
1272
- fn = b.fn;
1273
- } else if(b && b.kind === "const" && FUNCTION_RE.test(b.init.type)) {
1274
- fn = b.init;
2122
+ FN_READING.add(fn);
2123
+ let items = [];
2124
+ try {
2125
+ for(let r of returned_values(fn)) {
2126
+ let values = expand(r.node, {scope: [...target.scope, fn, ...r.path], via: [], off: true}, 0);
2127
+ items = unique_values(items.concat(values));
2128
+ }
2129
+ } finally {
2130
+ FN_READING.delete(fn);
1275
2131
  }
1276
- if(!fn || fn.async || fn.generator) {
1277
- return null;
2132
+ let leaves = items.map(classify);
2133
+ let keys = new Set();
2134
+ for(let l of leaves) {
2135
+ if(l.kind === "key") {
2136
+ keys.add(l.value);
2137
+ }
2138
+ for(let k of l.partial || []) {
2139
+ keys.add(k);
2140
+ }
1278
2141
  }
2142
+ let result = {
2143
+ items: items,
2144
+ complete: leaves.length > 0 && leaves.every((l) => KNOWN.has(l.kind)),
2145
+ keys: [...keys]
2146
+ };
2147
+ FN_RETURNS.set(fn, result);
2148
+ return result;
2149
+ }
2150
+
2151
+ /***************************************************************
2152
+ * The values of expand() without repeats: a value is its node, read
2153
+ * as the same kind along the same path. A function that returns the
2154
+ * call of another three times returns ITS values once, not three
2155
+ * copies of them -- copies multiply along a chain of helpers, and
2156
+ * 16 of them made 86 million values.
2157
+ ***************************************************************/
2158
+ function unique_values(values)
2159
+ {
2160
+ let seen = new Map(); // node -> Set("<kind> <path>")
1279
2161
  let out = [];
1280
- for(let r of returned_values(fn)) {
1281
- out.push(...expand(r.node, {scope: [...b.scope, fn, ...r.path], via: via, off: true}, depth + 1));
1282
- }
1283
- if(out.length === 0 || !out.every((v) => KNOWN.has(classify(v).kind))) {
1284
- return null;
2162
+ for(let v of values) {
2163
+ let id = `${v.kind} ${v.path || ""} ${v.var_root? JSON.stringify(v.var_root.steps) : ""}`;
2164
+ let ids = seen.get(v.node);
2165
+ if(!ids) {
2166
+ ids = new Set();
2167
+ seen.set(v.node, ids);
2168
+ }
2169
+ if(!ids.has(id)) {
2170
+ ids.add(id);
2171
+ out.push(v);
2172
+ }
1285
2173
  }
1286
2174
  return out;
1287
2175
  }
@@ -1321,7 +2209,9 @@ function extend_root(root, step)
1321
2209
  /***************************************************************
1322
2210
  * The values one step (member_step()) reads from a value of expand():
1323
2211
  * a property, or an element. `node` is the node that reads it (null:
1324
- * the value's own)
2212
+ * the value's own). An element of a rest parameter read by its index
2213
+ * is the parameter at that index (`rest[0]`); any other step of one
2214
+ * is no position a table can name, and the reader cannot follow it.
1325
2215
  ***************************************************************/
1326
2216
  function step_values(o, step, node, depth)
1327
2217
  {
@@ -1329,6 +2219,13 @@ function step_values(o, step, node, depth)
1329
2219
  if(step.each) {
1330
2220
  return element_values(o, depth);
1331
2221
  }
2222
+ if(o.kind === "param" && o.binding.rest) {
2223
+ if(step.computed && step.prop !== null && /^\d+$/.test(step.prop)) {
2224
+ let binding = Object.assign({}, o.binding, {index: o.binding.index + Number(step.prop), rest: false});
2225
+ return [Object.assign({}, o, {binding, path: "", node: at})];
2226
+ }
2227
+ return [Object.assign({}, o, {kind: "unknown", var_root: null, node: at})];
2228
+ }
1332
2229
  if(o.kind === "param") {
1333
2230
  return [step.prop !== null? Object.assign({}, o, {path: o.path + "." + step.prop, node: at}) :
1334
2231
  Object.assign({}, o, {kind: "unknown", binding: null, var_root: null, node: at})];
@@ -1336,6 +2233,9 @@ function step_values(o, step, node, depth)
1336
2233
  if(o.kind === "value" && o.node.type === "ObjectExpression") {
1337
2234
  return property_values(o, step.prop, depth + 1);
1338
2235
  }
2236
+ if(o.kind === "value" && is_object_call(o.node, "assign")) {
2237
+ return assigned_properties(o, step, depth + 1);
2238
+ }
1339
2239
  if(o.kind === "value" && o.node.type === "ArrayExpression" && step.computed) {
1340
2240
  return element_values(o, depth + 1);
1341
2241
  }
@@ -1346,9 +2246,43 @@ function step_values(o, step, node, depth)
1346
2246
  var_root: extend_root(o.var_root, step)})];
1347
2247
  }
1348
2248
 
2249
+ /***************************************************************
2250
+ * The values one step reads from `Object.assign(target, ...sources)`:
2251
+ * what each argument holds there (a later one wins, so any of them
2252
+ * may be it); an argument that has no such property is no value of
2253
+ * it
2254
+ ***************************************************************/
2255
+ function assigned_properties(o, step, depth)
2256
+ {
2257
+ let out = [];
2258
+ let scope = [...o.scope, o.node];
2259
+ for(let a of o.node.arguments) {
2260
+ if(a.type === "SpreadElement") {
2261
+ out.push(Object.assign({}, o, {kind: "unknown", binding: null, var_root: null, node: a}));
2262
+ continue;
2263
+ }
2264
+ for(let v of expand(a, {scope, via: o.via, off: o.off}, depth + 1)) {
2265
+ if(v.kind === "nothing" || (v.kind === "value" && v.node.type === "Literal")) {
2266
+ continue;
2267
+ }
2268
+ let r = step_values(v, step, null, depth + 1);
2269
+ if(v.kind === "value" && v.node.type === "ObjectExpression") {
2270
+ r = r.filter((x) => x.kind !== "nothing");
2271
+ }
2272
+ out.push(...r);
2273
+ }
2274
+ }
2275
+ if(out.length === 0) {
2276
+ out.push(Object.assign({}, o, {kind: "nothing"}));
2277
+ }
2278
+ return out;
2279
+ }
2280
+
1349
2281
  /***************************************************************
1350
2282
  * The values of property `name` of an object literal (`name` null:
1351
- * of every property), each followed with expand()
2283
+ * of every property), each followed with expand(). A spread is read
2284
+ * for it: an object literal, or a parameter (`{...opts}` holds
2285
+ * opts.title as its title)
1352
2286
  ***************************************************************/
1353
2287
  function property_values(o, name, depth)
1354
2288
  {
@@ -1362,6 +2296,9 @@ function property_values(o, name, depth)
1362
2296
  let r = property_values(s, name, depth + 1).filter((x) => x.kind !== "nothing");
1363
2297
  found = found || r.length > 0;
1364
2298
  out.push(...r);
2299
+ } else if(s.kind === "param" && name !== null) {
2300
+ found = true;
2301
+ out.push(...step_values(s, {prop: name, computed: false}, null, depth + 1));
1365
2302
  } else if(s.kind !== "nothing") {
1366
2303
  found = true;
1367
2304
  out.push(Object.assign({}, s, {kind: "unknown", binding: null,
@@ -1408,12 +2345,16 @@ function elements_of(node, ctx, depth)
1408
2345
  }
1409
2346
 
1410
2347
  /***************************************************************
1411
- * The elements of one value of expand(): see elements_of()
2348
+ * The elements of one value of expand(): see elements_of(). Each
2349
+ * element of a rest parameter is another parameter: no position can
2350
+ * name one, and the reader cannot follow it.
1412
2351
  ***************************************************************/
1413
2352
  function element_values(v, depth)
1414
2353
  {
1415
2354
  let out = [];
1416
- if(v.kind === "param") {
2355
+ if(v.kind === "param" && v.binding.rest) {
2356
+ out.push(Object.assign({}, v, {kind: "unknown", var_root: null}));
2357
+ } else if(v.kind === "param") {
1417
2358
  out.push(Object.assign({}, v, {path: v.path + "[]"}));
1418
2359
  } else if(v.kind === "nothing" || (v.kind === "value" && v.node.type === "Literal")) {
1419
2360
  return out;
@@ -1525,6 +2466,10 @@ function leaves_of(node, scope = [], root = null)
1525
2466
 
1526
2467
  function classify(v)
1527
2468
  {
2469
+ if(v.kind === "param" && v.binding && v.binding.rest) {
2470
+ /* A rest parameter itself: every argument from there on, no position */
2471
+ return Object.assign({}, v, {kind: "unknown", var_root: null});
2472
+ }
1528
2473
  if(v.kind !== "value") {
1529
2474
  return v;
1530
2475
  }
@@ -1537,12 +2482,53 @@ function classify(v)
1537
2482
  let s = n.quasis[0].value.cooked;
1538
2483
  return Object.assign({}, v, {kind: s? "key" : "nothing", value: s});
1539
2484
  }
2485
+ let strings = string_list(n);
2486
+ if(strings) {
2487
+ return Object.assign({}, v, {kind: "unknown", strings});
2488
+ }
1540
2489
  if(is_dom(n, v.scope)) {
1541
2490
  return Object.assign({}, v, {kind: "dom"});
1542
2491
  }
1543
2492
  return Object.assign({}, v, {kind: "unknown"});
1544
2493
  }
1545
2494
 
2495
+ /***************************************************************
2496
+ * The text of a string literal or of a template with no `${}`,
2497
+ * or null
2498
+ ***************************************************************/
2499
+ function static_string(n)
2500
+ {
2501
+ if(n && n.type === "Literal" && typeof n.value === "string") {
2502
+ return n.value;
2503
+ }
2504
+ if(n && n.type === "TemplateLiteral" && n.expressions.length === 0) {
2505
+ return n.quasis[0].value.cooked;
2506
+ }
2507
+ return null;
2508
+ }
2509
+
2510
+ /***************************************************************
2511
+ * The strings of an array literal of two or more string literals,
2512
+ * or null. Such an array is no createElement2 spec -- a spec holds
2513
+ * its ATTRS in its second slot, an object -- but a list of strings:
2514
+ * given to t(), the keys it tries in turn (i18next's fallbacks).
2515
+ ***************************************************************/
2516
+ function string_list(n)
2517
+ {
2518
+ if(!n || n.type !== "ArrayExpression" || n.elements.length < 2) {
2519
+ return null;
2520
+ }
2521
+ let out = [];
2522
+ for(let el of n.elements) {
2523
+ let s = static_string(el);
2524
+ if(s === null) {
2525
+ return null;
2526
+ }
2527
+ out.push(s);
2528
+ }
2529
+ return out;
2530
+ }
2531
+
1546
2532
  /*
1547
2533
  * What BUILDS DOM, known by where it comes from, never by a name the
1548
2534
  * code chose (`$box` says nothing): the builders of gobj-js and
@@ -1559,16 +2545,17 @@ const DOCUMENT_BUILDERS = new Set([
1559
2545
  ]);
1560
2546
 
1561
2547
  /***************************************************************
1562
- * Is a value DOM? An array literal (a createElement2 spec), or a
1563
- * call of a DOM builder: createElement2(), createOneHtml(),
1564
- * html_markup() imported from gobj-js, text_node() from gobj-ui,
2548
+ * Is a value DOM? An array literal (a createElement2 spec; not a
2549
+ * list of string literals, string_list()), or a call of a DOM
2550
+ * builder: createElement2(), createOneHtml(), html_markup()
2551
+ * imported from gobj-js, text_node() from gobj-ui,
1565
2552
  * document.createElement() and the like. `scope` is the chain of
1566
2553
  * ancestors of the node, where its names are resolved.
1567
2554
  ***************************************************************/
1568
2555
  function is_dom(n, scope = [])
1569
2556
  {
1570
2557
  if(n.type === "ArrayExpression") {
1571
- return true;
2558
+ return !string_list(n);
1572
2559
  }
1573
2560
  if(n.type !== "CallExpression") {
1574
2561
  return false;
@@ -1610,29 +2597,31 @@ function static_keys(node, scope = [])
1610
2597
  /***************************************************************
1611
2598
  * The leaves at a key position of I18N_KEY_FUNCTIONS in one call:
1612
2599
  * "1", "2.title", "1.actions[].label", "2[].label". `scope` is the
1613
- * chain of ancestors of the call. Each leaf is one of leaves_of(),
1614
- * its `via` starting at the call.
2600
+ * chain of ancestors of the call; `args` the arguments as the
2601
+ * callee receives them (call_kind(): those of `.call()` /
2602
+ * `.apply()`; null, an array the source does not write out). Each
2603
+ * leaf is one of leaves_of(), its `via` starting at the call.
1615
2604
  *
1616
2605
  * A position the call does not reach (an argument not given) has no
1617
2606
  * leaf: its default applies. An options object held in a variable,
1618
- * a spread, a parameter: followed like any value.
2607
+ * a spread, a parameter, a copy: followed like any value.
1619
2608
  ***************************************************************/
1620
- function values_at(call, path, scope = [])
2609
+ function values_at(call, path, scope = [], args = call.arguments)
1621
2610
  {
1622
2611
  let segs = String(path).split(".");
1623
2612
  let first = segs.shift().match(/^(\d+)(\[\])?$/);
1624
2613
  let at_call = {kind: "unknown", node: call, scope, via: [call], off: false};
1625
- if(!first) {
2614
+ if(!first || !args) {
1626
2615
  return [at_call];
1627
2616
  }
1628
2617
  let idx = Number(first[1]);
1629
2618
  let call_scope = [...scope, call];
1630
- for(let i = 0; i <= idx && i < call.arguments.length; i++) {
1631
- if(call.arguments[i].type === "SpreadElement") {
1632
- return [Object.assign({}, at_call, {node: call.arguments[i]})];
2619
+ for(let i = 0; i <= idx && i < args.length; i++) {
2620
+ if(args[i] && args[i].type === "SpreadElement") {
2621
+ return [Object.assign({}, at_call, {node: args[i]})];
1633
2622
  }
1634
2623
  }
1635
- let arg = call.arguments[idx];
2624
+ let arg = args[idx];
1636
2625
  if(!arg) {
1637
2626
  return [];
1638
2627
  }
@@ -1646,43 +2635,13 @@ function values_at(call, path, scope = [])
1646
2635
  }
1647
2636
  let next = [];
1648
2637
  for(let v of cur) {
1649
- if(v.kind === "param") {
1650
- next.push(Object.assign({}, v, {path: v.path + "." + m[1]}));
1651
- } else if(v.kind === "value" && v.node.type === "ObjectExpression") {
1652
- next.push(...property_values(v, m[1], 0));
1653
- } else if(v.kind === "value" && is_object_call(v.node, "assign")) {
1654
- let assigned = v.node.arguments.flatMap((a) =>
1655
- expand(a, {scope: [...v.scope, v.node], via: v.via, off: v.off}));
1656
- for(let a of assigned) {
1657
- if(a.kind === "value" && a.node.type === "ObjectExpression") {
1658
- next.push(...property_values(a, m[1], 0)
1659
- .filter((x) => x.kind !== "nothing"));
1660
- } else if(a.kind === "param") {
1661
- next.push(Object.assign({}, a, {path: a.path + "." + m[1]}));
1662
- } else if(a.kind !== "nothing") {
1663
- next.push(Object.assign({}, a, {kind: "unknown", binding: null,
1664
- var_root: extend_root(a.var_root, {prop: m[1], computed: false})}));
1665
- }
1666
- }
1667
- } else if(v.kind === "nothing" || (v.kind === "value" && v.node.type === "Literal")) {
1668
- continue;
1669
- } else {
1670
- next.push(Object.assign({}, v, {kind: "unknown", binding: null,
1671
- var_root: extend_root(v.var_root, {prop: m[1], computed: false})}));
2638
+ if(v.kind === "nothing" || (v.kind === "value" && v.node.type === "Literal")) {
2639
+ continue; // the position is not reached: its default applies
1672
2640
  }
2641
+ next.push(...step_values(v, {prop: m[1], computed: false}, null, 0));
1673
2642
  }
1674
2643
  if(m[2]) {
1675
- let els = [];
1676
- for(let v of next) {
1677
- if(v.kind === "param") {
1678
- els.push(Object.assign({}, v, {path: v.path + "[]"}));
1679
- } else if(v.kind === "value") {
1680
- els.push(...elements_of(v.node, v, 0));
1681
- } else if(v.kind !== "nothing") {
1682
- els.push(Object.assign({}, v, {var_root: extend_root(v.var_root, {each: true})}));
1683
- }
1684
- }
1685
- next = els;
2644
+ next = next.flatMap((v) => (v.kind === "nothing")? [] : element_values(v, 0));
1686
2645
  }
1687
2646
  cur = next;
1688
2647
  }
@@ -1757,9 +2716,12 @@ const ELEMENT_WRITERS = new Map([["push", 0], ["unshift", 0], ["splice", 2], ["f
1757
2716
  * and `Object.assign(x, v)` (v written over x). A `+=` or `++` on a
1758
2717
  * member is unknown at that member.
1759
2718
  *
1760
- * -> {values: [{node, scope, steps, spread, into}], unknown: [{node, steps}]}
2719
+ * -> {values: [{node, scope, steps, spread, into, pick}],
2720
+ * unknown: [{node, steps, scope}]}
1761
2721
  * `node` null: nothing (a declarator with no value); `into`:
1762
- * written into the variable, not given to it
2722
+ * written into the variable, not given to it; `pick`: what a
2723
+ * destructuring declarator reads from its value ({steps,
2724
+ * default}, `let {title} = opts`)
1763
2725
  ***************************************************************/
1764
2726
  function assigned_values(binding, members = false)
1765
2727
  {
@@ -1780,7 +2742,9 @@ function assigned_values(binding, members = false)
1780
2742
  switch(n.type) {
1781
2743
  case "VariableDeclarator":
1782
2744
  if(binding.declarators.includes(n)) {
1783
- values.push(n.init? {node: n.init, scope: chain, steps: []} : {node: null, steps: []});
2745
+ values.push(n.init? {node: n.init, scope: chain, steps: [],
2746
+ pick: (binding.picks && binding.picks.get(n)) || null}
2747
+ : {node: null, steps: []});
1784
2748
  }
1785
2749
  break;
1786
2750
  case "AssignmentExpression": {
@@ -1790,7 +2754,7 @@ function assigned_values(binding, members = false)
1790
2754
  if(readable) {
1791
2755
  values.push({node: n.right, scope: chain, steps: []});
1792
2756
  } else {
1793
- unknown.push({node: n, steps: []});
2757
+ unknown.push({node: n, steps: [], scope: chain});
1794
2758
  }
1795
2759
  }
1796
2760
  } else if(n.left.type === "MemberExpression") {
@@ -1798,21 +2762,21 @@ function assigned_values(binding, members = false)
1798
2762
  if(steps && readable) {
1799
2763
  values.push({node: n.right, scope: chain, steps, into: true});
1800
2764
  } else if(steps) {
1801
- unknown.push({node: n, steps});
2765
+ unknown.push({node: n, steps, scope: chain});
1802
2766
  }
1803
2767
  } else if(pattern_names(n.left).includes(binding.name)
1804
2768
  && is_it({type: "Identifier", name: binding.name}, chain)) {
1805
- unknown.push({node: n, steps: []});
2769
+ unknown.push({node: n, steps: [], scope: chain});
1806
2770
  }
1807
2771
  break;
1808
2772
  }
1809
2773
  case "UpdateExpression":
1810
2774
  if(is_it(n.argument, chain)) {
1811
- unknown.push({node: n, steps: []});
2775
+ unknown.push({node: n, steps: [], scope: chain});
1812
2776
  } else if(n.argument.type === "MemberExpression") {
1813
2777
  let steps = into(n.argument);
1814
2778
  if(steps) {
1815
- unknown.push({node: n, steps});
2779
+ unknown.push({node: n, steps, scope: chain});
1816
2780
  }
1817
2781
  }
1818
2782
  break;
@@ -1841,7 +2805,7 @@ function assigned_values(binding, members = false)
1841
2805
  }
1842
2806
  for(let a of n.arguments.slice(1)) {
1843
2807
  if(a.type === "SpreadElement") {
1844
- unknown.push({node: a, steps});
2808
+ unknown.push({node: a, steps, scope: chain});
1845
2809
  } else {
1846
2810
  values.push({node: a, scope: chain, steps, into: true});
1847
2811
  }
@@ -1852,7 +2816,7 @@ function assigned_values(binding, members = false)
1852
2816
  case "ForOfStatement":
1853
2817
  case "ForInStatement":
1854
2818
  if(is_it(n.left, chain)) {
1855
- unknown.push({node: n, steps: []});
2819
+ unknown.push({node: n, steps: [], scope: chain});
1856
2820
  }
1857
2821
  break;
1858
2822
  default:
@@ -1869,21 +2833,23 @@ function assigned_values(binding, members = false)
1869
2833
  * with expand() and then along the steps of `root`, a `let` met on
1870
2834
  * the way followed into it.
1871
2835
  *
1872
- * -> {leaves: [leaf], complete, written, unread: [node], lets: [binding]}
2836
+ * -> {leaves: [leaf], complete, written, unread: [node],
2837
+ * opaque: [{node, scope}], lets: [binding]}
1873
2838
  *
1874
2839
  * `complete`: every leaf is known (a literal, a const that is one,
1875
2840
  * nothing, DOM) and every write was read. `written`: some leaf was
1876
2841
  * written INTO the variable (`x.a = "k"`, `x.push("k")`), which a
1877
2842
  * const would not carry to the reader. `unread`: the node of each
1878
- * value it could not read. `lets`: the variables it went through,
1879
- * the first one first.
2843
+ * value it could not read; `opaque`, each write it could not read
2844
+ * (`k += x`, a for...of over it), where it is (param_flows()).
2845
+ * `lets`: the variables it went through, the first one first.
1880
2846
  ***************************************************************/
1881
2847
  function var_leaves(root, seen = new Set(), depth = 0)
1882
2848
  {
1883
2849
  let b = root.binding;
1884
2850
  let steps = root.steps;
1885
- let out = {leaves: [], complete: true, written: false, unread: [], lets: [b]};
1886
- let id = `${b.declarators[0].start}:${JSON.stringify(steps)}`;
2851
+ let out = {leaves: [], complete: true, written: false, unread: [], opaque: [], lets: [b]};
2852
+ let id = `${b.declarators[0].start}:${b.name}:${JSON.stringify(steps)}`;
1887
2853
  if(seen.has(id)) {
1888
2854
  return out;
1889
2855
  }
@@ -1898,6 +2864,7 @@ function var_leaves(root, seen = new Set(), depth = 0)
1898
2864
  if(paths_meet(u.steps, steps)) {
1899
2865
  out.complete = false;
1900
2866
  out.unread.push(u.node);
2867
+ out.opaque.push({node: u.node, scope: u.scope});
1901
2868
  }
1902
2869
  }
1903
2870
  for(let v of values) {
@@ -1915,6 +2882,15 @@ function var_leaves(root, seen = new Set(), depth = 0)
1915
2882
  }
1916
2883
  let ctx = {scope: v.scope, via: [], off: false};
1917
2884
  let cur = v.spread? elements_of(v.node, ctx, 0) : expand(v.node, ctx);
2885
+ if(v.pick) {
2886
+ /* `let {title = "x"} = opts`: what the pattern reads from the value */
2887
+ for(let step of v.pick.steps) {
2888
+ cur = cur.flatMap((o) => step_values(o, step, null, 0));
2889
+ }
2890
+ if(v.pick.default) {
2891
+ cur.push(...expand(v.pick.default, ctx));
2892
+ }
2893
+ }
1918
2894
  for(let step of steps.slice(v.steps.length)) {
1919
2895
  cur = cur.flatMap((o) => step_values(o, step, null, 0));
1920
2896
  }
@@ -1924,6 +2900,7 @@ function var_leaves(root, seen = new Set(), depth = 0)
1924
2900
  let r = var_leaves(leaf.var_root, seen, depth + 1);
1925
2901
  out.leaves.push(...r.leaves);
1926
2902
  out.unread.push(...r.unread);
2903
+ out.opaque.push(...r.opaque);
1927
2904
  out.lets.push(...r.lets);
1928
2905
  out.complete = out.complete && r.complete;
1929
2906
  out.written = out.written || r.written;
@@ -1978,6 +2955,163 @@ function source_keys(node, scope = [])
1978
2955
  return {keys, unread};
1979
2956
  }
1980
2957
 
2958
+ /***************************************************************
2959
+ * The PARAMETERS a value is built from, through what the reader
2960
+ * does not follow: a call (`k.toLowerCase()`, `fmt(k)`), a
2961
+ * concatenation, a template with `${}`, an iteration of
2962
+ * Object.entries(), a rest parameter read by a computed index,
2963
+ * `arguments`, a part of a parameter an array pattern
2964
+ * destructures. Each name the value is built from is followed to
2965
+ * what binds it: a const to its value, a `let` / `var` into
2966
+ * everything it is given or written, the element of what a
2967
+ * for...of or a callback walks; a parameter of a function around
2968
+ * it is an answer. The property a computed member reads
2969
+ * (`TABLE[k]`) is a lookup, not the value: it is not followed, nor
2970
+ * is the body of a function, nor a number or a boolean (`!k`,
2971
+ * `k++`).
2972
+ *
2973
+ * -> [{binding, path, priv}]
2974
+ *
2975
+ * `binding` is a parameter (binding_of(): "param", "param_part"),
2976
+ * or an `arguments` (arguments_binding()); `path`, what is read from
2977
+ * it (".title", "[]" an element of it), null when it cannot be said
2978
+ * (through a call, a destructuring, an iteration of entries); `priv`,
2979
+ * read through a gobj's `priv` (`gobj.priv.label`): the gobj's own
2980
+ * state, never a key its caller hands. A METHOD's value comes from
2981
+ * its object when the object comes from somewhere -- `k.trim()` from
2982
+ * k, `priv.errors.get(path)` from the priv (`path` is the key of a
2983
+ * lookup) -- and from its arguments otherwise (`fmt(k)`,
2984
+ * `JSON.stringify(p)`). What a `let` is given and the elements put
2985
+ * in it (`x.push(v)`) are what it holds; a property written into it
2986
+ * (`x.title = v`) is not.
2987
+ *
2988
+ * i18n_key_functions.test.js uses it on what the reader stops at,
2989
+ * at the places a key is read: a function that hands a parameter on
2990
+ * that way hands a key no position can name, and the test fails.
2991
+ ***************************************************************/
2992
+ function param_flows(node, scope, seen = new Set(), depth = 0)
2993
+ {
2994
+ /* Through a call or an iteration a path names no position any more */
2995
+ let unnamed = (flows) => flows.map((f) => (f.path === null)? f : {binding: f.binding, path: null, priv: f.priv});
2996
+ if(!node || typeof node.type !== "string" || depth > MAX_DEPTH) {
2997
+ return [];
2998
+ }
2999
+ let sub = (n, s = scope) => param_flows(n, s, seen, depth + 1);
3000
+ switch(node.type) {
3001
+ case "Identifier": {
3002
+ if(node.name === "arguments") {
3003
+ let a = arguments_binding(scope);
3004
+ return a? [{binding: a, path: null, priv: false}] : [];
3005
+ }
3006
+ let b = binding_of(node.name, scope);
3007
+ if(!b) {
3008
+ return [];
3009
+ }
3010
+ switch(b.kind) {
3011
+ case "param":
3012
+ return [{binding: b, path: b.rest? null : b.path, priv: false}];
3013
+ case "param_part":
3014
+ return [{binding: b, path: null, priv: false}];
3015
+ case "const":
3016
+ if(FUNCTION_RE.test(b.init.type)) {
3017
+ return [];
3018
+ }
3019
+ return (b.steps.length > 0)? unnamed(sub(b.init, b.scope)) : sub(b.init, b.scope);
3020
+ case "each":
3021
+ return sub(b.of, b.scope).map((f) => ({binding: f.binding, priv: f.priv,
3022
+ path: (f.path !== null && b.steps.length === 0)? f.path + "[]" : null}));
3023
+ case "each_part":
3024
+ return unnamed(sub(b.of, b.scope));
3025
+ case "other":
3026
+ return b.init? unnamed(sub(b.init, b.scope)) : [];
3027
+ case "var": {
3028
+ let id = `${b.declarators[0].start}:${b.name}`;
3029
+ if(seen.has(id)) {
3030
+ return [];
3031
+ }
3032
+ seen.add(id);
3033
+ let {values, unknown} = assigned_values(b, true);
3034
+ let out = [];
3035
+ for(let v of values) {
3036
+ /* What it is given, and the elements put in it (`x.push(v)`); a
3037
+ * property written into it (`x.title = v`) is not what it is */
3038
+ if(v.node && (!v.into || (v.steps.length > 0 && v.steps[v.steps.length - 1].each))) {
3039
+ out.push(...(v.into || v.pick? unnamed(sub(v.node, v.scope)) : sub(v.node, v.scope)));
3040
+ }
3041
+ }
3042
+ for(let u of unknown) {
3043
+ if(u.steps.length === 0) {
3044
+ out.push(...unnamed(sub(u.node, u.scope)));
3045
+ }
3046
+ }
3047
+ return out;
3048
+ }
3049
+ default:
3050
+ return [];
3051
+ }
3052
+ }
3053
+ case "MemberExpression": {
3054
+ let step = member_step(node);
3055
+ return sub(node.object).map((f) => {
3056
+ let priv = f.priv || step.prop === "priv";
3057
+ if(f.path === null || step.prop === null || node.computed) {
3058
+ return {binding: f.binding, path: null, priv};
3059
+ }
3060
+ return {binding: f.binding, path: f.path + "." + step.prop, priv};
3061
+ });
3062
+ }
3063
+ case "CallExpression":
3064
+ case "NewExpression": {
3065
+ /* A method's value comes from its object when the object comes
3066
+ * from somewhere (`k.toLowerCase()`; `priv.errors.get(path)` is a
3067
+ * lookup in the gobj's state, `path` only its key); otherwise from
3068
+ * the arguments (`fmt(k)`, `JSON.stringify(p)`, `"x".concat(k)`) */
3069
+ let c = (node.callee.type === "ChainExpression")? node.callee.expression : node.callee;
3070
+ if(c.type === "MemberExpression") {
3071
+ let from_object = sub(c.object);
3072
+ if(from_object.length > 0) {
3073
+ return unnamed(from_object);
3074
+ }
3075
+ } else if(!FUNCTION_RE.test(c.type) && c.type !== "Identifier") {
3076
+ let r = sub(c);
3077
+ if(r.length > 0) {
3078
+ return unnamed(r);
3079
+ }
3080
+ }
3081
+ return unnamed(node.arguments.flatMap((a) => sub(a)));
3082
+ }
3083
+ case "ChainExpression":
3084
+ return sub(node.expression);
3085
+ case "TemplateLiteral":
3086
+ return node.expressions.flatMap((e) => sub(e));
3087
+ case "TaggedTemplateExpression":
3088
+ return node.quasi.expressions.flatMap((e) => sub(e));
3089
+ case "BinaryExpression":
3090
+ case "LogicalExpression":
3091
+ return [...sub(node.left), ...sub(node.right)];
3092
+ case "ConditionalExpression":
3093
+ return [...sub(node.consequent), ...sub(node.alternate)];
3094
+ case "AssignmentExpression":
3095
+ return sub(node.right);
3096
+ case "SequenceExpression":
3097
+ return sub(node.expressions[node.expressions.length - 1]);
3098
+ case "AwaitExpression":
3099
+ case "YieldExpression":
3100
+ case "SpreadElement":
3101
+ return sub(node.argument);
3102
+ case "ForOfStatement":
3103
+ case "ForInStatement":
3104
+ return sub(node.right);
3105
+ case "ArrayExpression":
3106
+ return node.elements.flatMap((e) => (e? sub(e) : []));
3107
+ case "ObjectExpression":
3108
+ return node.properties.flatMap((p) => (p.type === "SpreadElement")? sub(p.argument) :
3109
+ (p.kind === "init" && !p.method)? sub(p.value) : []);
3110
+ default:
3111
+ return [];
3112
+ }
3113
+ }
3114
+
1981
3115
 
1982
3116
  /***************************
1983
3117
  * Markers
@@ -2010,6 +3144,12 @@ const MARKER_LIST_RE = /^[A-Za-z_$][\w$]*_I18N_KEYS$/;
2010
3144
  * all: [marker], errors: [{line, why}]}
2011
3145
  *
2012
3146
  * marker: {kind: "data" | "lists", lists: [name], line, used: false}
3147
+ *
3148
+ * It marks the node that starts right after it, through any
3149
+ * parenthesis: `/\* i18n: data *\/ (a || b)` marks `a || b` (acorn
3150
+ * keeps no node for the parenthesis, so the expression starts after
3151
+ * it), and `/\* i18n: data *\/ (a || b).trim()` the call, which starts
3152
+ * at it.
2013
3153
  ***************************************************************/
2014
3154
  function markers_of(ast)
2015
3155
  {
@@ -2042,11 +3182,53 @@ function markers_of(ast)
2042
3182
  }
2043
3183
  marker.next = next;
2044
3184
  at.set(next, marker);
3185
+ while(next < ast.code.length && /[\s(]/.test(ast.code[next])) {
3186
+ next++;
3187
+ if(ast.code[next - 1] === "(") {
3188
+ at.set(next, marker);
3189
+ }
3190
+ }
2045
3191
  all.push(marker);
2046
3192
  }
2047
3193
  return {at, all, errors};
2048
3194
  }
2049
3195
 
3196
+ /***************************************************************
3197
+ * What an unused marker stands before, said to whoever has to move
3198
+ * it: the code that follows it, and what holds it when that tells
3199
+ * why no key is read there (a declaration, an assignment)
3200
+ ***************************************************************/
3201
+ function marker_misplaced(ast, m)
3202
+ {
3203
+ let found = null;
3204
+ walk(ast, (n, ancestors) => {
3205
+ if(!found && n.start === m.next && n.type !== "Program") {
3206
+ found = {node: n, parent: ancestors[ancestors.length - 1] || null};
3207
+ }
3208
+ });
3209
+ let text = ast.code.slice(m.next, m.next + 60).split("\n")[0].trim();
3210
+ let before = text? `\`${text}${text.length >= 60? "..." : ""}\`` : "the end of the file";
3211
+ let where = "";
3212
+ if(found && found.parent) {
3213
+ let p = found.parent;
3214
+ if(p.type === "VariableDeclarator" && p.init === found.node) {
3215
+ where = ", the value a variable is DECLARED with: mark it where the variable is handed to a place " +
3216
+ "a key is read";
3217
+ } else if(p.type === "AssignmentExpression" && p.right === found.node) {
3218
+ where = ", a value ASSIGNED: mark it where it is handed to a place a key is read";
3219
+ } else if(p.type === "ReturnStatement") {
3220
+ where = ", a value RETURNED: mark the call where it is handed to a place a key is read";
3221
+ } else if(/^(BinaryExpression|TemplateLiteral)$/.test(p.type)) {
3222
+ where = ", inside a concatenation or a template, which the reader does not read into: " +
3223
+ "mark the whole value";
3224
+ }
3225
+ }
3226
+ return `the i18n marker stands before ${before}${where}. A marker marks the value it stands right ` +
3227
+ `before, where a key is read: what is handed to t(), an i18n / code / data-i18n* value, or ` +
3228
+ `a key position of a key function (an argument, a branch of one, a property value, or the ` +
3229
+ `whole call)`;
3230
+ }
3231
+
2050
3232
  /***************************************************************
2051
3233
  * The marker of a leaf: the innermost one standing before a node of
2052
3234
  * the place it was read (its `via`), or null
@@ -2098,72 +3280,51 @@ function key_fn_index(table = I18N_KEY_FUNCTIONS)
2098
3280
  }
2099
3281
 
2100
3282
  /***************************************************************
2101
- * Does an import source name a gobj-ui module (the barrel, a deep
2102
- * `src/` module, or, inside the library, a sibling)?
3283
+ * The calls of a file to a key function, however the call reaches
3284
+ * it (call_kind(): an import of a gobj-ui function under any name, a
3285
+ * re-export, an alias, a map, `.call()` / `.apply()`; in the library
3286
+ * module that defines it, the function itself). `table` is
3287
+ * I18N_KEY_FUNCTIONS by default.
3288
+ *
3289
+ * -> [{entry, call, scope, args}], `scope` the ancestors of the
3290
+ * call, `args` the arguments the function receives
2103
3291
  ***************************************************************/
2104
- function is_gobj_ui_source(src, lib_module)
3292
+ const KEY_CALLS = new WeakMap(); // a parsed file -> Map(table -> its key calls)
3293
+
3294
+ function key_calls_cached(ast, table)
2105
3295
  {
2106
- if(/^@yuneta\/gobj-ui(?:\/index\.js|\/src\/[\w.-]+\.js)?$/.test(src)) {
2107
- return true;
2108
- }
2109
- return !!lib_module && /^\.\/[\w.-]+\.js$/.test(src);
3296
+ let per_table = KEY_CALLS.get(ast);
3297
+ return (per_table && per_table.has(table))? per_table.get(table) : null;
2110
3298
  }
2111
3299
 
2112
- /***************************************************************
2113
- * The calls of a file to a key function, each with what the file
2114
- * bound the callee to: an import of a gobj-ui function (by its
2115
- * exported name), or, in the library module that defines it, the
2116
- * function itself. `table` is I18N_KEY_FUNCTIONS by default.
2117
- *
2118
- * -> [{entry, call, scope}], `scope` the ancestors of the call
2119
- ***************************************************************/
2120
- function key_calls(ast, lib_module, table)
3300
+ function set_key_calls(ast, table, calls)
2121
3301
  {
2122
- let index = key_fn_index(table);
2123
- let direct = new Map(); // local name -> entry
2124
- let namespaces = new Set(); // `import * as ns` of a gobj-ui module
2125
- for(let node of ast.body) {
2126
- if(node.type !== "ImportDeclaration" || !is_gobj_ui_source(node.source.value, lib_module)) {
2127
- continue;
2128
- }
2129
- for(let spec of node.specifiers) {
2130
- if(spec.type === "ImportSpecifier") {
2131
- let name = spec.imported.name || spec.imported.value;
2132
- let entry = index.exported.get(name);
2133
- if(entry) {
2134
- direct.set(spec.local.name, entry);
2135
- }
2136
- } else if(spec.type === "ImportNamespaceSpecifier") {
2137
- namespaces.add(spec.local.name);
2138
- }
2139
- }
2140
- }
2141
- if(lib_module && index.by_module.has(lib_module)) {
2142
- for(let [name, entry] of index.by_module.get(lib_module)) {
2143
- direct.set(name, entry);
2144
- }
2145
- }
2146
- if(direct.size === 0 && namespaces.size === 0) {
2147
- return [];
3302
+ if(!KEY_CALLS.has(ast)) {
3303
+ KEY_CALLS.set(ast, new Map());
2148
3304
  }
3305
+ KEY_CALLS.get(ast).set(table, calls);
3306
+ }
2149
3307
 
3308
+ function key_calls(ast, lib_module, table = I18N_KEY_FUNCTIONS)
3309
+ {
3310
+ program_url(ast, lib_module);
3311
+ let known = key_calls_cached(ast, table);
3312
+ if(known) {
3313
+ return known;
3314
+ }
2150
3315
  let out = [];
2151
3316
  walk(ast, (n, ancestors) => {
2152
3317
  if(n.type !== "CallExpression") {
2153
3318
  return;
2154
3319
  }
2155
- let entry = null;
2156
- let c = n.callee;
2157
- if(c.type === "Identifier") {
2158
- entry = direct.get(c.name) || null;
2159
- } else if(c.type === "MemberExpression" && !c.computed
2160
- && c.object.type === "Identifier" && namespaces.has(c.object.name)) {
2161
- entry = index.exported.get(c.property.name) || null;
2162
- }
2163
- if(entry) {
2164
- out.push({entry, call: n, scope: ancestors.slice()});
3320
+ ancestors.push(n);
3321
+ let c = call_kind(n, ancestors, table);
3322
+ ancestors.pop();
3323
+ if(c && c.kind === "key") {
3324
+ out.push({entry: c.entry, call: n, scope: ancestors.slice(), args: c.args});
2165
3325
  }
2166
3326
  });
3327
+ set_key_calls(ast, table, out);
2167
3328
  return out;
2168
3329
  }
2169
3330
 
@@ -2188,14 +3349,16 @@ function is_forwarded(leaf, lib_module, table)
2188
3349
  }
2189
3350
 
2190
3351
  /***************************************************************
2191
- * Is a leaf a TRANSLATED text, `t("...")` / `i18next.t("...")`?
2192
- * At a key position it is a bug of its own: the function translates
2193
- * it again and marks it data-i18n with the translation, so it never
2194
- * changes language. What goes there is the key.
3352
+ * Is a leaf a TRANSLATED text -- a call of a translator
3353
+ * (is_translator_call(): `t("...")`, `i18next.t("...")`, `tr()`, an
3354
+ * alias, `t?.()`, `t.call()`)? At a key position it is a bug of its
3355
+ * own: the function translates it again and marks it data-i18n with
3356
+ * the translation, so it never changes language. What goes there is
3357
+ * the key.
2195
3358
  ***************************************************************/
2196
3359
  function is_translation(leaf)
2197
3360
  {
2198
- return !!leaf.node && leaf.node.type === "CallExpression" && callee_name(leaf.node.callee) === "t";
3361
+ return !!leaf.node && is_translator_call(leaf.node, leaf.scope);
2199
3362
  }
2200
3363
 
2201
3364
  /***************************************************************
@@ -2223,17 +3386,19 @@ function holds_only_dom(leaf)
2223
3386
  * or DOM (an array spec, createElement2(), ...: is_dom());
2224
3387
  * b) a `const` the reader follows to one of those (also through a
2225
3388
  * const object or array, the element of a const array its
2226
- * `.map()` / for...of iterates, a call of a function of the
2227
- * module whose every return is one), or a plain `let` that is
2228
- * only ever given DOM;
3389
+ * `.map()` / for...of iterates, an import of one, a call of a
3390
+ * function -- of the module or imported -- whose every return
3391
+ * is one), or a plain `let` that is only ever given DOM;
2229
3392
  * c) a key position of the enclosing table function, handed on
2230
3393
  * (only in the library: an app's functions are in no table);
2231
3394
  * d) MARKED where it is handed: `/\* i18n: <NAME>_I18N_KEYS *\/`
2232
3395
  * (a module publishes those keys) or `/\* i18n: data *\/`.
2233
3396
  *
2234
- * A TRANSLATED text, `t("...")`, fails even when marked: what goes
2235
- * there is the key (`why: "translated"`). `table` is
2236
- * I18N_KEY_FUNCTIONS by default.
3397
+ * A TRANSLATED text -- a call of any translator, is_translator_call():
3398
+ * `t("...")`, `tr()`, an alias, `t?.()`, `t.call()` -- fails even when
3399
+ * marked: what goes there is the key (`why: "translated"`). The calls
3400
+ * are found however they reach a key function (key_calls()). `table`
3401
+ * is I18N_KEY_FUNCTIONS by default.
2237
3402
  *
2238
3403
  * -> [{line, call_line, name, path, text, why}]
2239
3404
  ***************************************************************/
@@ -2241,9 +3406,9 @@ function call_rule_violations(ast, lib_module, table)
2241
3406
  {
2242
3407
  let markers = markers_of(ast);
2243
3408
  let out = [];
2244
- for(let {entry, call, scope} of key_calls(ast, lib_module, table)) {
3409
+ for(let {entry, call, scope, args} of key_calls(ast, lib_module, table)) {
2245
3410
  for(let path of entry.keys) {
2246
- for(let leaf of values_at(call, path, scope)) {
3411
+ for(let leaf of values_at(call, path, scope, args)) {
2247
3412
  let why = null;
2248
3413
  if(is_translation(leaf)) {
2249
3414
  why = "translated";
@@ -2275,7 +3440,8 @@ function call_rule_message(label, v)
2275
3440
  let what = `${v.name}() [${v.path}] is given ${v.text.replace(/\s+/g, " ")}`;
2276
3441
  if(v.why === "translated") {
2277
3442
  return `${where}: ${what}: a TRANSLATED text where a KEY goes (translated again and ` +
2278
- `marked data-i18n with the translation, it never changes language): hand it the key`;
3443
+ `marked data-i18n with the translation, it never changes language, and no marker ` +
3444
+ `excuses it): hand the key, not t(key)`;
2279
3445
  }
2280
3446
  return `${where}: ${what}, which the reader cannot follow: hand it a literal or a const, ` +
2281
3447
  `or mark it /* i18n: data */ or /* i18n: <NAME>_I18N_KEYS */`;
@@ -2289,9 +3455,9 @@ function call_rule_message(label, v)
2289
3455
 
2290
3456
  /***************************************************************
2291
3457
  * The `<NAME>_I18N_KEYS` arrays a module exports, READ, never run:
2292
- * `export const|let|var X = [...]`, `const X = [...]; export {X}`,
2293
- * either wrapped in Object.freeze(). Each element is a string literal,
2294
- * a template with no `${}`, or a spread of another array of the same
3458
+ * `export const X = [...]`, `const X = [...]; export {X}`, either
3459
+ * wrapped in Object.freeze(). Each element is a string literal, a
3460
+ * template with no `${}`, or a spread of another array of the same
2295
3461
  * module read the same way -- a top-level `const`, or a list it
2296
3462
  * publishes: `[...BASE_I18N_KEYS, "k"]`.
2297
3463
  *
@@ -2300,11 +3466,14 @@ function call_rule_message(label, v)
2300
3466
  * Anything else so named is an ERROR, never skipped: the keys it
2301
3467
  * holds would go unasked, and the build say OK. That covers an
2302
3468
  * export that is not an array of literals (a spread of anything else
2303
- * included: an import, a `let`, a call), and a list nobody can
2304
- * read: one declared and not exported, one declared inside a
2305
- * function or a block, one declared by destructuring, one exported
2306
- * under a name that does not end in _I18N_KEYS. A re-export
2307
- * (`export {X} from "./y.js"`) is read in its own module.
3469
+ * included: an import, a `let`, a call), a list declared with `let`
3470
+ * or `var` (it can be given other keys, which nobody reads: up to
3471
+ * 7.26.31 it was read, and a `.push()` on it hid keys), and a list
3472
+ * nobody can read: one declared and not exported, one declared
3473
+ * inside a function or a block, one declared by destructuring, one
3474
+ * exported under a name that does not end in _I18N_KEYS. A
3475
+ * re-export (`export {X} from "./y.js"`) is read in its own module.
3476
+ * A list that is MUTATED fails too (list_mutations()).
2308
3477
  ***************************************************************/
2309
3478
  function published_lists(ast)
2310
3479
  {
@@ -2325,13 +3494,13 @@ function published_lists(ast)
2325
3494
  }
2326
3495
  }
2327
3496
 
2328
- /* What a list may spread: a const of the module, or a list it publishes */
3497
+ /* What a list may spread: a const of the module (a list it publishes is one) */
2329
3498
  let base_of = (name) => {
2330
3499
  let d = top.get(name);
2331
- if(!d || !d.init || !d.kind) {
3500
+ if(!d || !d.init || d.kind !== "const") {
2332
3501
  return null;
2333
3502
  }
2334
- return (d.kind === "const" || PUBLISHED_LIST_RE.test(name))? d.init : null;
3503
+ return d.init;
2335
3504
  };
2336
3505
 
2337
3506
  let lists = [];
@@ -2348,6 +3517,11 @@ function published_lists(ast)
2348
3517
  errors.push(`${exported}: ${d.what? "a " + d.what : "declared with no value"}, not an array`);
2349
3518
  return;
2350
3519
  }
3520
+ if(d.kind !== "const") {
3521
+ errors.push(`${exported}: declared with ${d.kind}, which can be given other keys no reader ` +
3522
+ `sees: a published list is an \`export const\` array of literals`);
3523
+ return;
3524
+ }
2351
3525
  let r = read_string_array(d.init, base_of, new Set([local]));
2352
3526
  if(r.error) {
2353
3527
  errors.push(`${exported}: ${r.error}`);
@@ -2461,6 +3635,95 @@ function read_string_array(init, base_of = () => null, seen = new Set())
2461
3635
  return {keys};
2462
3636
  }
2463
3637
 
3638
+ /* What changes the elements of an array in place */
3639
+ const ARRAY_MUTATORS = new Set(["push", "unshift", "splice", "fill", "copyWithin"]);
3640
+ const OBJECT_MUTATORS = new Set(["assign", "defineProperty", "defineProperties"]);
3641
+
3642
+ /***************************************************************
3643
+ * The places a file MUTATES a published list: one of its own
3644
+ * (`lists`, published_lists()) or one it imports (an import whose
3645
+ * name ends in _I18N_KEYS). A list is read as it is WRITTEN, so a
3646
+ * key it is given later -- `.push()`, `.splice()`, `.unshift()`,
3647
+ * `.fill()`, `X[i] = k`, `X.length = n`, `Object.assign(X, ...)`,
3648
+ * `delete X[i]`, a reassignment -- is asked for by nobody.
3649
+ *
3650
+ * -> [{line, why}]
3651
+ ***************************************************************/
3652
+ function list_mutations(ast, lists = [])
3653
+ {
3654
+ let names = new Set();
3655
+ for(let node of ast.body) {
3656
+ let decl = (node.type === "ExportNamedDeclaration")? node.declaration : node;
3657
+ if(decl && decl.type === "VariableDeclaration") {
3658
+ for(let d of decl.declarations) {
3659
+ if(d.id.type === "Identifier" && PUBLISHED_LIST_RE.test(d.id.name)) {
3660
+ names.add(d.id.name);
3661
+ }
3662
+ }
3663
+ }
3664
+ if(node.type === "ExportNamedDeclaration" && !node.source) {
3665
+ for(let spec of node.specifiers) {
3666
+ if(lists.some((l) => l.name === (spec.exported.name || spec.exported.value))) {
3667
+ names.add(spec.local.name);
3668
+ }
3669
+ }
3670
+ }
3671
+ if(node.type === "ImportDeclaration") {
3672
+ for(let spec of node.specifiers) {
3673
+ if(spec.type === "ImportSpecifier" && PUBLISHED_LIST_RE.test(spec.imported.name || spec.imported.value)) {
3674
+ names.add(spec.local.name);
3675
+ }
3676
+ }
3677
+ }
3678
+ }
3679
+ if(names.size === 0) {
3680
+ return [];
3681
+ }
3682
+ /* A use of one of them: the name, bound at the top of the module */
3683
+ let is_list = (n, chain) => {
3684
+ if(!n || n.type !== "Identifier" || !names.has(n.name)) {
3685
+ return false;
3686
+ }
3687
+ let b = binding_of(n.name, chain);
3688
+ return !!b && !!b.scope && b.scope.length === 1;
3689
+ };
3690
+ let out = [];
3691
+ let say = (n, name) => {
3692
+ out.push({line: line_of(ast, n.start),
3693
+ why: `the published list ${name} is MUTATED (\`${ast.code.slice(n.start, n.end).slice(0, 80)}\`): ` +
3694
+ `it is read as it is written, so what it is given here is asked for by nobody -- ` +
3695
+ `write every key in its array literal`});
3696
+ };
3697
+ walk(ast, (n, ancestors) => {
3698
+ let chain = [...ancestors, n];
3699
+ if(n.type === "CallExpression") {
3700
+ let c = n.callee;
3701
+ if(c.type === "MemberExpression" && !c.computed && ARRAY_MUTATORS.has(c.property.name)
3702
+ && is_list(c.object, chain)) {
3703
+ say(n, c.object.name);
3704
+ } else if(c.type === "MemberExpression" && !c.computed && c.object.type === "Identifier"
3705
+ && (c.object.name === "Object" || c.object.name === "Reflect")
3706
+ && (OBJECT_MUTATORS.has(c.property.name) || c.property.name === "set")
3707
+ && is_list(n.arguments[0], chain)) {
3708
+ say(n, n.arguments[0].name);
3709
+ }
3710
+ } else if(n.type === "AssignmentExpression") {
3711
+ if(is_list(n.left, chain)) {
3712
+ say(n, n.left.name);
3713
+ } else if(n.left.type === "MemberExpression" && is_list(n.left.object, chain)) {
3714
+ say(n, n.left.object.name);
3715
+ }
3716
+ } else if(n.type === "UpdateExpression" && n.argument.type === "MemberExpression"
3717
+ && is_list(n.argument.object, chain)) {
3718
+ say(n, n.argument.object.name);
3719
+ } else if(n.type === "UnaryExpression" && n.operator === "delete"
3720
+ && n.argument.type === "MemberExpression" && is_list(n.argument.object, chain)) {
3721
+ say(n, n.argument.object.name);
3722
+ }
3723
+ });
3724
+ return out;
3725
+ }
3726
+
2464
3727
 
2465
3728
  /***************************
2466
3729
  * The scan
@@ -2475,13 +3738,20 @@ function read_string_array(init, base_of = () => null, seen = new Set())
2475
3738
  *
2476
3739
  * `what` says which place it is:
2477
3740
  *
2478
- * "t" the first argument of t() / tr()
3741
+ * "t" the key of a TRANSLATOR call (call_kind(): t(),
3742
+ * tr(), `i18next.t()`, an alias, `t?.()`,
3743
+ * `t.call(x, key)`); an array there is the list of
3744
+ * keys i18next tries in turn, each one a key
2479
3745
  * "code" the code of a schema check, add("error", <code>)
2480
- * "attribute" setAttribute("data-i18n*", <key>)
3746
+ * "attribute" setAttribute("data-i18n*", <key>), the name a
3747
+ * literal or a const the reader follows to one
2481
3748
  * "property" an `i18n`, `code` or `data-i18n*` property of an
2482
- * object literal, or one of ctx.attr_keys
3749
+ * object literal (`{[ATTR]: k}` with ATTR a const
3750
+ * bound to one), or one of ctx.attr_keys
2483
3751
  * "array" an element of one of ctx.array_keys
2484
- * "assignment" `x.i18n = <key>`, `x["data-i18n-title"] = <key>`
3752
+ * "assignment" `x.i18n = <key>`, `x["data-i18n-title"] = <key>`,
3753
+ * `x[ATTR] = <key>`, `el.dataset.i18nTitle = <key>`
3754
+ * (`dataset.i18nX` IS the attribute data-i18n-x)
2485
3755
  * "call" a key position of a key function: `entry` and
2486
3756
  * `path` say which
2487
3757
  *
@@ -2494,42 +3764,78 @@ function key_places(ast, ctx = {})
2494
3764
  {
2495
3765
  let attr_keys = ctx.attr_keys || new Set();
2496
3766
  let array_keys = ctx.array_keys || new Set();
2497
- let calls = key_calls(ast, ctx.lib_module || null, ctx.table);
2498
- let of_the_table = new Set(calls.map((c) => c.call));
3767
+ let table = ctx.table || I18N_KEY_FUNCTIONS;
3768
+ program_url(ast, ctx.lib_module || null);
3769
+ /* One walk: the key calls are found on the way (key_calls() gets them) */
3770
+ let known_calls = key_calls_cached(ast, table);
3771
+ let calls = known_calls || [];
2499
3772
  let out = [];
2500
3773
  let read = (what, node, scope, root) => {
2501
3774
  if(node) {
2502
3775
  out.push({what, node: root, leaves: leaves_of(node, scope, root)});
2503
3776
  }
2504
3777
  };
3778
+ let read_t = (node, scope, root) => {
3779
+ if(node) {
3780
+ out.push({what: "t", node: root, leaves: key_list_leaves(node, scope, root)});
3781
+ }
3782
+ };
3783
+ /* The names a property or an attribute is given: a literal, or a
3784
+ * const (a branch of them) the reader follows */
3785
+ let names_of = (node, scope) => {
3786
+ let s = static_string(node);
3787
+ return (s !== null)? [s] : static_keys(node, scope).keys;
3788
+ };
3789
+ let is_i18n_name = (name) => name === "i18n" || DATA_I18N_ATTR_RE.test(name);
3790
+ let is_property_key = (x) => is_i18n_name(x) || x === "code" || attr_keys.has(x);
2505
3791
  walk(ast, (n, ancestors) => {
2506
3792
  switch(n.type) {
2507
3793
  case "CallExpression": {
2508
- let name = callee_name(n.callee);
2509
- let scope = [...ancestors, n];
2510
- if(of_the_table.has(n)) {
2511
- break; // a key function (a `tr` of the library too): read where its table says
3794
+ ancestors.push(n);
3795
+ let c = call_kind(n, ancestors, table);
3796
+ ancestors.pop();
3797
+ if(c && c.kind === "key") {
3798
+ /* A key function (a `tr` of the library too): read where its table says */
3799
+ if(!known_calls) {
3800
+ calls.push({entry: c.entry, call: n, scope: ancestors.slice(), args: c.args});
3801
+ }
3802
+ break;
2512
3803
  }
2513
- if(name === "t" || name === "tr") {
2514
- read("t", n.arguments[0], scope, n);
3804
+ let name = callee_name(n.callee);
3805
+ if(c && c.kind === "t") {
3806
+ read_t(c.args? c.args[0] : null, [...ancestors, n], n);
2515
3807
  } else if(name === "add" && is_string_literal(n.arguments[0], /^(error|warning)$/)) {
2516
3808
  /* The codes the schema checks hand to `t(finding.code)` */
2517
- read("code", n.arguments[1], scope, n);
2518
- } else if(name === "setAttribute" && is_string_literal(n.arguments[0], DATA_I18N_ATTR_RE)) {
2519
- read("attribute", n.arguments[1], scope, n);
3809
+ read("code", n.arguments[1], [...ancestors, n], n);
3810
+ } else if(name === "setAttribute" && n.arguments.length >= 2) {
3811
+ let scope = [...ancestors, n];
3812
+ if(names_of(n.arguments[0], scope).some((x) => DATA_I18N_ATTR_RE.test(x))) {
3813
+ read("attribute", n.arguments[1], scope, n);
3814
+ }
2520
3815
  }
2521
3816
  break;
2522
3817
  }
2523
3818
  case "Property": {
3819
+ if(ancestors[ancestors.length - 1].type !== "ObjectExpression") {
3820
+ break; // a destructuring pattern
3821
+ }
2524
3822
  let name = prop_name(n);
2525
- if(name === null || ancestors[ancestors.length - 1].type !== "ObjectExpression") {
2526
- break; // computed, or a destructuring pattern
3823
+ let names = null;
3824
+ if(name !== null) {
3825
+ if(!is_property_key(name) && !array_keys.has(name)) {
3826
+ break;
3827
+ }
3828
+ names = [name];
3829
+ }
3830
+ if(!names && !n.computed) {
3831
+ break;
2527
3832
  }
2528
3833
  let scope = [...ancestors, n];
2529
- if(name === "i18n" || name === "code" || DATA_I18N_ATTR_RE.test(name) || attr_keys.has(name)) {
3834
+ names = names || names_of(n.key, scope);
3835
+ if(names.some(is_property_key)) {
2530
3836
  read("property", n.value, scope, n);
2531
3837
  }
2532
- if(array_keys.has(name) && n.value.type === "ArrayExpression") {
3838
+ if(names.some((x) => array_keys.has(x)) && n.value.type === "ArrayExpression") {
2533
3839
  for(let el of n.value.elements) {
2534
3840
  read("array", el, [...scope, n.value], n);
2535
3841
  }
@@ -2541,8 +3847,18 @@ function key_places(ast, ctx = {})
2541
3847
  break;
2542
3848
  }
2543
3849
  let name = member_step(n.left).prop;
2544
- if(name === "i18n" || (name !== null && DATA_I18N_ATTR_RE.test(name))) {
2545
- read("assignment", n.right, [...ancestors, n], n);
3850
+ let o = n.left.object;
3851
+ let dataset = (o.type === "MemberExpression" && member_step(o).prop === "dataset");
3852
+ let fits = (x) => is_i18n_name(x) || (dataset && DATASET_I18N_RE.test(x));
3853
+ if(name !== null && !fits(name)) {
3854
+ break;
3855
+ }
3856
+ if(name === null && !n.left.computed) {
3857
+ break;
3858
+ }
3859
+ let scope = [...ancestors, n];
3860
+ if(name !== null || names_of(n.left.property, scope).some(fits)) {
3861
+ read("assignment", n.right, scope, n);
2546
3862
  }
2547
3863
  break;
2548
3864
  }
@@ -2550,9 +3866,30 @@ function key_places(ast, ctx = {})
2550
3866
  break;
2551
3867
  }
2552
3868
  });
2553
- for(let {entry, call, scope} of calls) {
3869
+ if(!known_calls) {
3870
+ set_key_calls(ast, table, calls);
3871
+ }
3872
+ for(let {entry, call, scope, args} of calls) {
2554
3873
  for(let path of entry.keys) {
2555
- out.push({what: "call", node: call, entry, path, leaves: values_at(call, path, scope)});
3874
+ out.push({what: "call", node: call, entry, path, leaves: values_at(call, path, scope, args)});
3875
+ }
3876
+ }
3877
+ return out;
3878
+ }
3879
+
3880
+ /***************************************************************
3881
+ * The leaves of the key of a translator call: leaves_of(), but an
3882
+ * array of keys is read as the keys it holds -- `t([k, "fallback"])`,
3883
+ * i18next's list of fallback keys, asks for each
3884
+ ***************************************************************/
3885
+ function key_list_leaves(node, scope, root)
3886
+ {
3887
+ let out = [];
3888
+ for(let v of expand(node, {scope, via: [root], off: false})) {
3889
+ if(v.kind === "value" && v.node.type === "ArrayExpression") {
3890
+ out.push(...element_values(v, 0).map(classify));
3891
+ } else {
3892
+ out.push(classify(v));
2556
3893
  }
2557
3894
  }
2558
3895
  return out;
@@ -2566,24 +3903,61 @@ function key_places(ast, ctx = {})
2566
3903
  * -> {keys: Set, read_keys: Set (those READ, not published by a
2567
3904
  * list), declared_seen: Set,
2568
3905
  * lists: [{name, keys}], list_errors: [why],
2569
- * marker_refs: [{lists, keys, line}], marker_errors: [{line, why}]}
3906
+ * list_mutations: [{line, why}] (list_mutations()),
3907
+ * marker_refs: [{lists, keys, line, shapes}],
3908
+ * marker_errors: [{line, why}]}
2570
3909
  *
2571
3910
  * Every place a key is read (key_places()) goes through leaves_of(),
2572
3911
  * so a marker standing there is found: one that marks no such place,
2573
- * or that is not a marker at all, is an error. So is a DATA marker
2574
- * on what the reader follows to keys and nothing else (a literal, a
2575
- * const that is one, a `?:` branch that is one, a call of a function
2576
- * of the module that returns literals): it claims data where the
2577
- * reader reads a key.
3912
+ * or that is not a marker at all, is an error, which says what it
3913
+ * stands before. So is a DATA marker on what the reader follows to
3914
+ * keys and nothing else (a literal, a const that is one, a `?:`
3915
+ * branch that is one, a call of a function -- of the module, or
3916
+ * imported -- that returns literals): it claims data where the
3917
+ * reader reads a key; and one that would HIDE keys the reader does
3918
+ * not read: a call whose returns mix keys and data, an array of
3919
+ * string literals. Under a LIST marker those keys are asked for and
3920
+ * must be in the lists, and a template with `${}` must have the
3921
+ * shape of a key of them (`shapes`, checked where the lists are
3922
+ * known).
2578
3923
  ***************************************************************/
2579
3924
  function scan_ast(ast, ctx)
2580
3925
  {
3926
+ program_url(ast, ctx.lib_module || null);
2581
3927
  let keys = new Set();
2582
3928
  let declared_seen = new Set();
2583
3929
  let declared = ctx.declared || new Set();
2584
3930
  let markers = markers_of(ast);
2585
3931
  let var_errors = [];
2586
3932
  let vars_checked = new Set();
3933
+ let text_of = (n) => ast.code.slice(n.start, n.end).replace(/\s+/g, " ").slice(0, 80);
3934
+
3935
+ /*
3936
+ * What a leaf holds that the reader does not READ, and a marker
3937
+ * standing on it must answer for: the keys of a call whose
3938
+ * returns mix keys and data (`partial`), the strings of an array
3939
+ * of string literals (`strings`), the shape of a template with
3940
+ * `${}`. Under a list marker the keys are asked for and must be
3941
+ * in the lists, and a template must have the shape of some key of
3942
+ * them; under a data marker, keys the marker would hide fail.
3943
+ */
3944
+ let answer = (leaf, m) => {
3945
+ let held = leaf.partial || leaf.strings || null;
3946
+ if(held && held.length > 0) {
3947
+ if(m.kind === "lists") {
3948
+ for(let k of held) {
3949
+ keys.add(k);
3950
+ m.keys.add(k);
3951
+ }
3952
+ } else {
3953
+ m.hidden.push({leaf, keys: held});
3954
+ }
3955
+ }
3956
+ if(m.kind === "lists" && leaf.kind === "unknown" && leaf.node && leaf.node.type === "TemplateLiteral"
3957
+ && leaf.node.expressions.length > 0) {
3958
+ m.shapes.push({text: text_of(leaf.node), source: template_shape(leaf.node)});
3959
+ }
3960
+ };
2587
3961
 
2588
3962
  /*
2589
3963
  * A marker says where the keys of what it marks come from, and a
@@ -2598,7 +3972,7 @@ function scan_ast(ast, ctx)
2598
3972
  let check_marked_var = (leaf, m) => {
2599
3973
  let root = leaf.var_root;
2600
3974
  let b = root.binding;
2601
- let id = `${m.line}:${b.declarators[0].start}:${JSON.stringify(root.steps)}`;
3975
+ let id = `${m.line}:${b.declarators[0].start}:${b.name}:${JSON.stringify(root.steps)}`;
2602
3976
  if(vars_checked.has(id)) {
2603
3977
  return;
2604
3978
  }
@@ -2630,6 +4004,9 @@ function scan_ast(ast, ctx)
2630
4004
  m.keys.add(k);
2631
4005
  }
2632
4006
  }
4007
+ for(let l of r.leaves) {
4008
+ answer(l, m);
4009
+ }
2633
4010
  };
2634
4011
 
2635
4012
  let take = (leaves) => {
@@ -2642,9 +4019,9 @@ function scan_ast(ast, ctx)
2642
4019
  m.used = true;
2643
4020
  if(!m.keys) {
2644
4021
  m.keys = new Set();
2645
- }
2646
- if(!m.leaves) {
2647
4022
  m.leaves = [];
4023
+ m.hidden = [];
4024
+ m.shapes = [];
2648
4025
  }
2649
4026
  m.leaves.push(leaf);
2650
4027
  if(leaf.kind === "key") {
@@ -2653,6 +4030,7 @@ function scan_ast(ast, ctx)
2653
4030
  if(leaf.kind === "unknown" && leaf.var_root) {
2654
4031
  check_marked_var(leaf, m);
2655
4032
  }
4033
+ answer(leaf, m);
2656
4034
  }
2657
4035
  }
2658
4036
  };
@@ -2699,14 +4077,30 @@ function scan_ast(ast, ctx)
2699
4077
  let marker_refs = [];
2700
4078
  for(let m of markers.all) {
2701
4079
  if(!m.used) {
2702
- marker_errors.push({line: m.line, why: `the i18n marker stands before no place a key is read ` +
2703
- `(a key position of a key function, t(), an i18n or data-i18n* value)`});
2704
- } else if(m.kind === "lists") {
2705
- marker_refs.push({lists: m.lists, keys: [...(m.keys || [])], line: m.line});
2706
- } else if(m.kind === "data" && m.leaves.every((l) => KNOWN.has(l.kind)) && m.keys.size > 0) {
4080
+ marker_errors.push({line: m.line, why: marker_misplaced(ast, m)});
4081
+ continue;
4082
+ }
4083
+ if(m.kind === "lists") {
4084
+ marker_refs.push({lists: m.lists, keys: [...m.keys], line: m.line, shapes: m.shapes});
4085
+ continue;
4086
+ }
4087
+ if(m.leaves.every((l) => KNOWN.has(l.kind)) && m.keys.size > 0) {
2707
4088
  let held = [...m.keys].map((k) => JSON.stringify(k)).join(", ");
2708
4089
  marker_errors.push({line: m.line, why: `marked as data, but what it marks is the key ` +
2709
4090
  `${held}, which the reader follows: drop the marker (it is no data)`});
4091
+ continue;
4092
+ }
4093
+ for(let h of m.hidden) {
4094
+ let held = h.keys.map((k) => JSON.stringify(k)).join(", ");
4095
+ let what = text_of(h.leaf.node);
4096
+ marker_errors.push({line: m.line, why: h.leaf.strings?
4097
+ `marked as data, but what it marks is an array of string literals (${held}), which is no ` +
4098
+ `DOM -- a createElement2 spec carries its attrs, an object, in its second slot --: the marker ` +
4099
+ `would hide those strings. Hand each key where it is read, or build the DOM` :
4100
+ `marked as data, but \`${what}\` can also return the key ${held}: the reader does not read ` +
4101
+ `a call whose returns mix keys with what it cannot follow, and the marker would hide that ` +
4102
+ `key. Split the branches: hand the key where it is read (a literal, a const) and mark ` +
4103
+ `only the data`});
2710
4104
  }
2711
4105
  }
2712
4106
 
@@ -2716,11 +4110,22 @@ function scan_ast(ast, ctx)
2716
4110
  declared_seen,
2717
4111
  lists: published.lists,
2718
4112
  list_errors: published.errors,
4113
+ list_mutations: list_mutations(ast, published.lists),
2719
4114
  marker_refs,
2720
4115
  marker_errors
2721
4116
  };
2722
4117
  }
2723
4118
 
4119
+ /***************************************************************
4120
+ * The shape of a template with `${}`: a regex source every string
4121
+ * it can build matches (`monitor ${control}` -> ^monitor [\s\S]*$)
4122
+ ***************************************************************/
4123
+ function template_shape(node)
4124
+ {
4125
+ let esc = (q) => q.value.cooked.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4126
+ return "^" + node.quasis.map(esc).join("[\\s\\S]*") + "$";
4127
+ }
4128
+
2724
4129
  /***************************************************************
2725
4130
  * The i18n keys the source asks for.
2726
4131
  *
@@ -2757,12 +4162,18 @@ function collect_used_keys(opts)
2757
4162
 
2758
4163
  let scan = (url, lib_module) => {
2759
4164
  let label = lib_module? `gobj-ui/src/${lib_module}` : relative(process.cwd(), fileURLToPath(url));
2760
- let ast;
2761
- try {
2762
- ast = parse_source(readFileSync(url, "utf8"));
2763
- } catch(e) {
2764
- fail(`${label} cannot be parsed (${e.message}): the keys it asks for are NOT checked`);
2765
- return [];
4165
+ /* Parsed once: the reader that follows an import into a module
4166
+ * reads the same tree the scan reads */
4167
+ let ast = AST_CACHE.get(url.href) || null;
4168
+ if(!ast) {
4169
+ try {
4170
+ ast = parse_source(readFileSync(url, "utf8"), url);
4171
+ } catch(e) {
4172
+ AST_CACHE.set(url.href, null);
4173
+ fail(`${label} cannot be parsed (${e.message}): the keys it asks for are NOT checked`);
4174
+ return [];
4175
+ }
4176
+ AST_CACHE.set(url.href, ast);
2766
4177
  }
2767
4178
  let r = scan_ast(ast, {...ctx, lib_module});
2768
4179
  for(let k of r.keys) {
@@ -2777,6 +4188,9 @@ function collect_used_keys(opts)
2777
4188
  for(let why of r.list_errors) {
2778
4189
  fail(`${label}: the published list ${why}: its keys are NOT asked for`);
2779
4190
  }
4191
+ for(let e of r.list_mutations) {
4192
+ fail(`${label}:${e.line}: ${e.why}`);
4193
+ }
2780
4194
  for(let list of r.lists) {
2781
4195
  if(!published.has(list.name)) {
2782
4196
  published.set(list.name, new Set());
@@ -2872,6 +4286,14 @@ function collect_used_keys(opts)
2872
4286
  `${ref.lists.join(", ")}, and is not in it`);
2873
4287
  }
2874
4288
  }
4289
+ for(let shape of ref.shapes || []) {
4290
+ let re = new RegExp(shape.source);
4291
+ if(!names.some((name) => [...published.get(name)].some((k) => re.test(k)))) {
4292
+ fail(`${ref.label}:${ref.line}: the i18n marker names ${ref.lists.join(", ")} for ` +
4293
+ `${shape.text}, and no key of ${ref.lists.length > 1? "them" : "it"} has that shape: ` +
4294
+ `publish the keys the template builds`);
4295
+ }
4296
+ }
2875
4297
  }
2876
4298
 
2877
4299
  return {keys, lib_keys};
@@ -3083,10 +4505,15 @@ export {
3083
4505
  marker_of,
3084
4506
  key_calls,
3085
4507
  key_places,
4508
+ call_kind,
4509
+ is_translator_call,
4510
+ is_translation,
3086
4511
  is_forwarded,
3087
4512
  call_rule_violations,
3088
4513
  call_rule_message,
4514
+ param_flows,
3089
4515
  published_lists,
4516
+ list_mutations,
3090
4517
  scan_ast,
3091
4518
  collect_used_keys,
3092
4519
  keys_from_shell_config,