@yuneta/gobj-ui 7.26.28 → 7.26.30

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.
@@ -42,33 +42,45 @@
42
42
  * fine and simply never changes language -- invisible by
43
43
  * construction, and it shipped: a Settings `label` column).
44
44
  *
45
- * Rule 5 PARSES (acorn, never executes) the app's own source
46
- * (recursively, .js and .mjs; not tests, not a locales/ directory),
47
- * the shell declaration (`src/app_config.json`) and the gobj-ui
48
- * modules the app MOUNTS, transitively -- deep, through the barrel,
49
- * or by a dynamic import(): the library translates through the
50
- * APP's i18next, so a key it asks for and the app does not define
45
+ * Rule 5 PARSES the app's own source -- acorn reads a source file
46
+ * as a syntax tree, it never RUNS one --, recursively, .js and
47
+ * .mjs (not tests, not a locales/ directory), the shell
48
+ * declaration (`src/app_config.json`) and the gobj-ui modules the
49
+ * app MOUNTS, transitively -- deep, through the barrel, or by a
50
+ * dynamic import(): the library translates through the APP's
51
+ * i18next, so a key it asks for and the app does not define
51
52
  * renders raw (that is how the treedb editor shipped reading
52
53
  * "edit", "new", "delete" in every language). The library scanned
53
54
  * is THIS one -- the package the app installed and builds with,
54
- * the one this script ships in. In each file it reads as keys:
55
+ * the one this script ships in. (A LOCALE is not a source: it is
56
+ * parsed and then imported, and its top-level code runs; see
57
+ * below.) In each file it reads as keys:
55
58
  *
56
59
  * - the string given to t() / tr(), to an `i18n` or `code`
57
- * property, to a `data-i18n*` attribute (as a property, by
58
- * setAttribute(), or written in markup), and the codes of the
59
- * schema checks (`add("error", "<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
63
+ * the schema checks (`add("error", "<code>")`);
60
64
  * - the strings given to a gobj-ui function that RECEIVES a key
61
65
  * as an argument (`yui_shell_show_error(shell, "<key>")`,
62
66
  * `icon_button(..., "<key>")`), at the positions its table
63
- * names: I18N_KEY_FUNCTIONS, src/i18n_key_functions.js;
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,
71
+ * document.createElement() and the like -- known by where
72
+ * they come from, never by the name of a variable;
64
73
  * - the keys a module PUBLISHES as data: an exported
65
74
  * `<NAME>_I18N_KEYS` array of string literals (the keys it
66
75
  * hands to t() as variables, yui_dev.js's 51). Read as data
67
76
  * -- `export const`, `let` or `var`, `export {X}`, wrapped in
68
- * Object.freeze() --; anything so named that is not an
69
- * exported array of string literals is a violation, never
70
- * skipped (a list not exported, declared in a function, by
71
- * destructuring, or exported under another name included).
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`).
72
84
  *
73
85
  * A string is read where it is written, and FOLLOWED where the
74
86
  * source writes it out: a literal, a template with no `${}`,
@@ -77,27 +89,48 @@
77
89
  * "save"); a property of a const object literal (`DEFAULTS.ok`;
78
90
  * every value of it for `NOTES[verdict]`); the element of a const
79
91
  * array literal its `.map()`, `.forEach()`, ... callback or a
80
- * for...of iterates. A key built by concatenation, held in a `let`,
81
- * or arriving as data cannot be known: its module publishes it in
82
- * a `<NAME>_I18N_KEYS` list.
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.
83
99
  *
84
100
  * A MARKER at the place a key is read says where the keys of what
85
101
  * follows it come from: `/\* i18n: <NAME>_I18N_KEYS *\/` (that
86
102
  * published list; several by commas) or `/\* i18n: data *\/` (data,
87
- * no key a list can hold). The library marks every key position
88
- * it cannot write as a literal or a const (its own test fails
89
- * otherwise); this script checks the markers it reads: each one
90
- * must be a marker and stand before a place a key is read, the
91
- * lists it names must be published by a module the app mounts,
92
- * and the keys it marks that can be read must be in them. A
93
- * marked `let` / `var` is READ, and so is one a marked member
94
- * or element is read from (`LABELS[k]`, `picked = LIST[0]`, the
95
- * `x` of `LIST.forEach((x) => ...)`): everything it is given in
96
- * its scope (its value, every `=` to it; for a member, also what
97
- * is written into it: `x.a = v`, `x.push(v)`, Object.assign())
98
- * is followed, each literal is a key asked for and must be in
99
- * the lists named, and one given nothing but literals is a
100
- * violation: it is a const.
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.
119
+ *
120
+ * THE RULE PER CALL holds the app's own code, as a test holds the
121
+ * library (i18n_key_functions.test.js): every value a key position
122
+ * of a key function can take is a literal (a branch of them,
123
+ * nothing, DOM), a const the reader follows to one, a `let` that
124
+ * is only ever given DOM, or MARKED where it is handed. Anything
125
+ * else -- a `let` given literals, an options variable, a
126
+ * concatenation, a parameter of a wrapper of the app's own -- is a
127
+ * 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.
101
134
  *
102
135
  * Options (paths relative to the app's directory):
103
136
  *
@@ -122,20 +155,21 @@
122
155
  *
123
156
  * A locale is a file <code>.js of the locales directory (en.js,
124
157
  * es.js, pt-br.js; flags.js and locales.js are not) exporting
125
- * `{<code>}` with a `translation` dict. It is imported directly,
126
- * not through locales.js, which drags in gobj-js and i18next and
127
- * expects a browser. A file that LOOKS like a locale -- it holds a
128
- * `translation` dict, it exports its own name (es.js exporting
129
- * `es`), or it has a default export -- and fails to load, or that
130
- * cannot even be parsed, is a violation: skipping it would
131
- * validate one language less and say OK. Any other file with a
132
- * locale's name (app.js, a helper needing a browser) is said and
133
- * skipped.
158
+ * `{<code>}` with a `translation` dict. It is parsed first, and
159
+ * then IMPORTED, so its top-level code runs -- the app's own code,
160
+ * but code. It is imported directly, not through locales.js, which
161
+ * drags in gobj-js and i18next and expects a browser. A file that
162
+ * LOOKS like a locale -- it holds a `translation` dict, it exports
163
+ * its own name (es.js exporting `es`), or it has a default export
164
+ * -- and fails to load, or that cannot even be parsed, is a
165
+ * violation: skipping it would validate one language less and say
166
+ * OK. Any other file with a locale's name (app.js, a helper
167
+ * needing a browser) is said and skipped.
134
168
  *
135
169
  * Exits 1 on any violation, so the build fails loudly.
136
170
  *
137
- * Needs `acorn`, which every app declares already for
138
- * check-markup-strings.mjs.
171
+ * Needs `acorn`, a dependency of gobj-ui since 7.26.29: it is
172
+ * installed with the package, and an app need not declare it.
139
173
  *
140
174
  * Its readers are exported: an app whose catalog has another shape
141
175
  * (gobj-ui's own test-app, English as the source) loads the catalog
@@ -206,15 +240,15 @@ function note(msg)
206
240
  }
207
241
 
208
242
  /*
209
- * The parser. Resolved from this script up, which in an app reaches the
210
- * app's own node_modules -- where check-markup-strings.mjs finds it too.
243
+ * The parser: a dependency of this package, resolved from this script up
244
+ * (the package's own node_modules, or the app's where npm hoists it).
211
245
  */
212
246
  let acorn_parse;
213
247
  try {
214
248
  ({parse: acorn_parse} = await import("acorn"));
215
249
  } catch(e) {
216
- abort(`cannot load acorn (${e.message}): add "acorn" to the app's devDependencies ` +
217
- `(check-markup-strings.mjs needs it too)`);
250
+ abort(`cannot load acorn (${e.message}): it is a dependency of @yuneta/gobj-ui, ` +
251
+ `so the install is incomplete: run npm install`);
218
252
  }
219
253
 
220
254
  /***************************************************************
@@ -878,9 +912,9 @@ function scope_names(a)
878
912
  case "ForInStatement":
879
913
  if(a.left.type === "VariableDeclaration") {
880
914
  for(let d of a.left.declarations) {
881
- let each = (a.type === "ForOfStatement" && d.id.type === "Identifier");
882
915
  for(let name of pattern_names(d.id)) {
883
- declare(name, each? {kind: "each", of: a.right} : {kind: "other"});
916
+ 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"});
884
918
  }
885
919
  }
886
920
  }
@@ -920,11 +954,15 @@ function scope_names(a)
920
954
  declare(name, what);
921
955
  }
922
956
  }
923
- } else if((d.type === "FunctionDeclaration" || d.type === "ClassDeclaration") && d.id) {
957
+ } else if(d.type === "FunctionDeclaration" && d.id) {
958
+ declare(d.id.name, {kind: "function", fn: d});
959
+ } else if(d.type === "ClassDeclaration" && d.id) {
924
960
  declare(d.id.name, {kind: "other"});
925
961
  } else if(d.type === "ImportDeclaration") {
926
962
  for(let sp of d.specifiers) {
927
- declare(sp.local.name, {kind: "other"});
963
+ let imported = (sp.type === "ImportSpecifier")? (sp.imported.name || sp.imported.value)
964
+ : (sp.type === "ImportDefaultSpecifier")? "default" : "*";
965
+ declare(sp.local.name, {kind: "import", source: d.source.value, imported});
928
966
  }
929
967
  }
930
968
  }
@@ -932,6 +970,51 @@ function scope_names(a)
932
970
  return names;
933
971
  }
934
972
 
973
+ /***************************************************************
974
+ * Where a binding pattern puts `name`, when properties alone reach
975
+ * it: {steps, path, default}. `steps` are the properties read from
976
+ * the value bound to reach it (none for a plain name; member_step()
977
+ * 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.
980
+ *
981
+ * function f(msg = "fallback") msg: {path: "", default: "fallback"}
982
+ * function f({title, opts: {label}}) title: ".title", label: ".opts.label"
983
+ ***************************************************************/
984
+ function param_path(p, name, steps = [], dflt = null)
985
+ {
986
+ if(!p) {
987
+ return null;
988
+ }
989
+ switch(p.type) {
990
+ case "Identifier":
991
+ if(p.name !== name) {
992
+ return null;
993
+ }
994
+ return {steps, path: steps.map((s) => "." + s.prop).join(""), default: dflt};
995
+ case "AssignmentPattern":
996
+ return param_path(p.left, name, steps, p.right);
997
+ case "ObjectPattern":
998
+ 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;
1001
+ if(key === null) {
1002
+ if(pattern_names(target).includes(name)) {
1003
+ return null;
1004
+ }
1005
+ continue;
1006
+ }
1007
+ let r = param_path(target, name, [...steps, {prop: key, computed: false}], null);
1008
+ if(r) {
1009
+ return r;
1010
+ }
1011
+ }
1012
+ return null;
1013
+ default:
1014
+ return null;
1015
+ }
1016
+ }
1017
+
935
1018
  /***************************************************************
936
1019
  * What a name used in `scope` (the chain of ancestors of the use,
937
1020
  * outermost first) is bound to:
@@ -940,13 +1023,21 @@ function scope_names(a)
940
1023
  * is the chain down to the block
941
1024
  * that declares it, where its
942
1025
  * value is read
943
- * {kind: "each", of, scope} an ELEMENT of `of`: the first
1026
+ * {kind: "each", of, steps, scope}
1027
+ * an ELEMENT of `of`: the first
944
1028
  * parameter of a callback of
945
1029
  * `of.map()`, `.forEach()`, ...,
946
- * or the variable of a for...of
947
- * {kind: "param", fn, name, index}
1030
+ * or the variable of a for...of;
1031
+ * `steps`, the properties read
1032
+ * from it by a destructuring
1033
+ * {kind: "param", fn, name, index, path, default, scope}
948
1034
  * parameter `index` of the
949
- * function `fn`, called `name`
1035
+ * function `fn`, called `name`;
1036
+ * `path` what a destructuring
1037
+ * reads from it ("" or
1038
+ * ".title"), `default` the
1039
+ * value written for it when the
1040
+ * caller gives none
950
1041
  * {kind: "var", decl, name, declarators, scope}
951
1042
  * a plain `let` or `var` (`decl`
952
1043
  * says which): what it
@@ -955,9 +1046,14 @@ function scope_names(a)
955
1046
  * holds (var_leaves()); `scope`
956
1047
  * is the chain down to the node
957
1048
  * that declares it
958
- * {kind: "other"} anything else: a function, a
959
- * class, an import, a destructured
960
- * name
1049
+ * {kind: "function", fn, scope} a function declaration
1050
+ * {kind: "import", source, imported}
1051
+ * an import: from module
1052
+ * `source`, its name there
1053
+ * (or "default", "*")
1054
+ * {kind: "other"} anything else: a class, a
1055
+ * name an array pattern or a
1056
+ * rest element declares
961
1057
  * null declared nowhere: a global
962
1058
  ***************************************************************/
963
1059
  function binding_of(name, scope)
@@ -970,16 +1066,18 @@ function binding_of(name, scope)
970
1066
  if(!pattern_names(a.params[j]).includes(name)) {
971
1067
  continue;
972
1068
  }
973
- let p = (a.params[j].type === "AssignmentPattern")? a.params[j].left : a.params[j];
974
- if(p.type !== "Identifier") {
1069
+ let at = param_path(a.params[j], name);
1070
+ if(!at) {
975
1071
  return {kind: "other"};
976
1072
  }
977
1073
  if(j === 0 && parent && parent.type === "CallExpression" && parent.arguments[0] === a
978
1074
  && parent.callee.type === "MemberExpression" && !parent.callee.computed
979
1075
  && ITERATION_METHODS.has(parent.callee.property.name)) {
980
- return {kind: "each", of: parent.callee.object, scope: scope.slice(0, i - 1)};
1076
+ return {kind: "each", of: parent.callee.object, steps: at.steps,
1077
+ scope: scope.slice(0, i - 1)};
981
1078
  }
982
- return {kind: "param", fn: a, name: function_name(a, parent), index: j};
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)};
983
1081
  }
984
1082
  let hoisted = function_scope_names(a);
985
1083
  if(hoisted.has(name)) {
@@ -997,11 +1095,14 @@ function binding_of(name, scope)
997
1095
  return {kind: "const", init: d.init, scope: scope.slice(0, i + 1)};
998
1096
  }
999
1097
  if(d.kind === "each") {
1000
- return {kind: "each", of: d.of, scope: scope.slice(0, i)};
1098
+ return {kind: "each", of: d.of, steps: d.steps || [], scope: scope.slice(0, i)};
1001
1099
  }
1002
1100
  if(d.kind === "var") {
1003
1101
  return {kind: "var", decl: d.decl, name, declarators: d.declarators, scope: scope.slice(0, i + 1)};
1004
1102
  }
1103
+ if(d.kind === "function") {
1104
+ return {kind: "function", fn: d.fn, scope: scope.slice(0, i + 1)};
1105
+ }
1005
1106
  return d;
1006
1107
  }
1007
1108
  return null;
@@ -1027,8 +1128,9 @@ const MAX_DEPTH = 24;
1027
1128
  * an object that has none
1028
1129
  * {kind: "param", node, binding, path}
1029
1130
  * a parameter of the enclosing
1030
- * function, `path` the rest read
1031
- * from it (".title", "[].label")
1131
+ * function, `path` what is read
1132
+ * from it (".title", "[].label",
1133
+ * a destructuring included)
1032
1134
  * {kind: "unknown", node} a `let`, a `var` (with its
1033
1135
  * `binding`), an import, a
1034
1136
  * property of something unknown
@@ -1051,8 +1153,14 @@ const MAX_DEPTH = 24;
1051
1153
  * element of an array literal (`LIST[i]`);
1052
1154
  * - an ELEMENT of an array literal it reached: the parameter of a
1053
1155
  * `.map()`, `.forEach()`, ... callback over it, the variable of a
1054
- * for...of over it;
1055
- * - Object.freeze(), which changes nothing it reads.
1156
+ * for...of over it (and what a destructuring there reads from
1157
+ * 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());
1162
+ * - Object.freeze(), String(x) and `${x}` (a template that is only
1163
+ * that), which change nothing it reads.
1056
1164
  *
1057
1165
  * ctx: {scope, via, off}. `off` is set once it left the place it is
1058
1166
  * read for the declaration of a const: `via` stops growing there.
@@ -1090,10 +1198,18 @@ function expand(node, ctx, depth = 0)
1090
1198
  return expand(b.init, {scope: b.scope, via: via, off: true}, depth + 1);
1091
1199
  }
1092
1200
  if(b.kind === "each") {
1093
- return elements_of(b.of, {scope: b.scope, via: via, off: true}, depth + 1);
1201
+ let out = elements_of(b.of, {scope: b.scope, via: via, off: true}, depth + 1);
1202
+ for(let step of b.steps) {
1203
+ out = out.flatMap((o) => step_values(o, step, node, depth + 1));
1204
+ }
1205
+ return out;
1094
1206
  }
1095
1207
  if(b.kind === "param") {
1096
- return [item("param", {binding: b, path: ""})];
1208
+ let out = [item("param", {binding: b, path: b.path})];
1209
+ if(b.default) {
1210
+ out.push(...expand(b.default, {scope: b.scope, via: via, off: true}, depth + 1));
1211
+ }
1212
+ return out;
1097
1213
  }
1098
1214
  if(b.kind === "var") {
1099
1215
  return [item("unknown", {binding: b, var_root: {binding: b, steps: []}})];
@@ -1114,6 +1230,17 @@ function expand(node, ctx, depth = 0)
1114
1230
  if(is_object_call(node, "freeze") && node.arguments.length === 1) {
1115
1231
  return expand(node.arguments[0], here, depth + 1);
1116
1232
  }
1233
+ if(node.callee.type === "Identifier" && node.callee.name === "String"
1234
+ && node.arguments.length === 1 && binding_of("String", ctx.scope) === null) {
1235
+ return expand(node.arguments[0], here, depth + 1);
1236
+ }
1237
+ return local_call_values(node, ctx, via, depth) || [item("value")];
1238
+
1239
+ case "TemplateLiteral":
1240
+ /* `${x}` and nothing else: x as a string */
1241
+ if(node.expressions.length === 1 && node.quasis.every((q) => q.value.cooked === "")) {
1242
+ return expand(node.expressions[0], here, depth + 1);
1243
+ }
1117
1244
  return [item("value")];
1118
1245
 
1119
1246
  default:
@@ -1121,6 +1248,44 @@ function expand(node, ctx, depth = 0)
1121
1248
  }
1122
1249
  }
1123
1250
 
1251
+ /* The leaves the reader KNOWS: no key hides behind them */
1252
+ const KNOWN = new Set(["key", "nothing", "dom"]);
1253
+
1254
+ /***************************************************************
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.
1263
+ ***************************************************************/
1264
+ function local_call_values(call, ctx, via, depth)
1265
+ {
1266
+ if(call.callee.type !== "Identifier") {
1267
+ return null;
1268
+ }
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;
1275
+ }
1276
+ if(!fn || fn.async || fn.generator) {
1277
+ return null;
1278
+ }
1279
+ 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;
1285
+ }
1286
+ return out;
1287
+ }
1288
+
1124
1289
  /***************************************************************
1125
1290
  * The step a member expression takes from its object:
1126
1291
  *
@@ -1335,6 +1500,9 @@ function returned_values(fn)
1335
1500
  * template with no `${}`
1336
1501
  * {kind: "nothing"} null, undefined, "", a number, a
1337
1502
  * boolean: no key
1503
+ * {kind: "dom"} DOM, or what builds it (is_dom()): no
1504
+ * key either -- a key position takes DOM
1505
+ * as it is, only a string there is a key
1338
1506
  * {kind: "param", ...} a parameter (of an enclosing function),
1339
1507
  * see expand()
1340
1508
  * {kind: "unknown"} anything else: a variable it cannot
@@ -1369,13 +1537,60 @@ function classify(v)
1369
1537
  let s = n.quasis[0].value.cooked;
1370
1538
  return Object.assign({}, v, {kind: s? "key" : "nothing", value: s});
1371
1539
  }
1540
+ if(is_dom(n, v.scope)) {
1541
+ return Object.assign({}, v, {kind: "dom"});
1542
+ }
1372
1543
  return Object.assign({}, v, {kind: "unknown"});
1373
1544
  }
1374
1545
 
1546
+ /*
1547
+ * What BUILDS DOM, known by where it comes from, never by a name the
1548
+ * code chose (`$box` says nothing): the builders of gobj-js and
1549
+ * gobj-ui by the module they are imported from, and the document's.
1550
+ */
1551
+ const DOM_BUILDERS = [
1552
+ {name: "createElement2", from: /^@yuneta\/gobj-js$/},
1553
+ {name: "createOneHtml", from: /^@yuneta\/gobj-js$/},
1554
+ {name: "html_markup", from: /^@yuneta\/gobj-js$/},
1555
+ {name: "text_node", from: /^(?:@yuneta\/gobj-ui(?:\/index\.js|\/src\/yui_text\.js)?|\.\/yui_text\.js)$/}
1556
+ ];
1557
+ const DOCUMENT_BUILDERS = new Set([
1558
+ "createElement", "createElementNS", "createTextNode", "createDocumentFragment"
1559
+ ]);
1560
+
1561
+ /***************************************************************
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,
1565
+ * document.createElement() and the like. `scope` is the chain of
1566
+ * ancestors of the node, where its names are resolved.
1567
+ ***************************************************************/
1568
+ function is_dom(n, scope = [])
1569
+ {
1570
+ if(n.type === "ArrayExpression") {
1571
+ return true;
1572
+ }
1573
+ if(n.type !== "CallExpression") {
1574
+ return false;
1575
+ }
1576
+ let c = n.callee;
1577
+ if(c.type === "MemberExpression" && !c.computed && c.object.type === "Identifier"
1578
+ && c.object.name === "document" && c.property.type === "Identifier"
1579
+ && DOCUMENT_BUILDERS.has(c.property.name)) {
1580
+ return binding_of("document", scope) === null; // the global, not a name of the module
1581
+ }
1582
+ if(c.type !== "Identifier") {
1583
+ return false;
1584
+ }
1585
+ let b = binding_of(c.name, scope);
1586
+ return !!b && b.kind === "import"
1587
+ && DOM_BUILDERS.some((x) => x.name === b.imported && x.from.test(b.source));
1588
+ }
1589
+
1375
1590
  /***************************************************************
1376
1591
  * The strings an expression can be, as far as they are WRITTEN:
1377
1592
  * the keys of leaves_of(). `complete` says every value it can take
1378
- * is known (keys and nothing). `scope` is the chain of ancestors of
1593
+ * is known (keys, nothing, DOM). `scope` is the chain of ancestors of
1379
1594
  * `node`: without it, no name is followed.
1380
1595
  ***************************************************************/
1381
1596
  function static_keys(node, scope = [])
@@ -1385,7 +1600,7 @@ function static_keys(node, scope = [])
1385
1600
  for(let leaf of leaves_of(node, scope)) {
1386
1601
  if(leaf.kind === "key") {
1387
1602
  keys.push(leaf.value);
1388
- } else if(leaf.kind !== "nothing") {
1603
+ } else if(!KNOWN.has(leaf.kind)) {
1389
1604
  complete = false;
1390
1605
  }
1391
1606
  }
@@ -1657,7 +1872,7 @@ function assigned_values(binding, members = false)
1657
1872
  * -> {leaves: [leaf], complete, written, unread: [node], lets: [binding]}
1658
1873
  *
1659
1874
  * `complete`: every leaf is known (a literal, a const that is one,
1660
- * nothing) and every write was read. `written`: some leaf was
1875
+ * nothing, DOM) and every write was read. `written`: some leaf was
1661
1876
  * written INTO the variable (`x.a = "k"`, `x.push("k")`), which a
1662
1877
  * const would not carry to the reader. `unread`: the node of each
1663
1878
  * value it could not read. `lets`: the variables it went through,
@@ -1715,7 +1930,7 @@ function var_leaves(root, seen = new Set(), depth = 0)
1715
1930
  continue;
1716
1931
  }
1717
1932
  out.leaves.push(leaf);
1718
- if(leaf.kind !== "key" && leaf.kind !== "nothing") {
1933
+ if(!KNOWN.has(leaf.kind)) {
1719
1934
  out.complete = false;
1720
1935
  out.unread.push(leaf.node);
1721
1936
  }
@@ -1747,6 +1962,8 @@ function source_keys(node, scope = [])
1747
1962
  for(let l of r.leaves) {
1748
1963
  if(l.kind === "key") {
1749
1964
  keys.push(l.value);
1965
+ } else if(l.kind === "dom") {
1966
+ unread.push({at: leaf.node, node: l.node}); // DOM is no key of a list
1750
1967
  }
1751
1968
  }
1752
1969
  for(let n of r.unread) {
@@ -1852,17 +2069,32 @@ function marker_of(leaf, markers)
1852
2069
  ***************************/
1853
2070
 
1854
2071
 
1855
- /* The exported ones by name, every one by its module */
1856
- const KEY_FN_EXPORTED = new Map();
1857
- const KEY_FN_BY_MODULE = new Map();
1858
- for(let entry of I18N_KEY_FUNCTIONS) {
1859
- if(entry.exported) {
1860
- KEY_FN_EXPORTED.set(entry.name, entry);
2072
+ /***************************************************************
2073
+ * A table of key functions (I18N_KEY_FUNCTIONS, or one a test
2074
+ * builds) indexed: {exported: Map(name -> entry), by_module:
2075
+ * Map(module -> Map(name -> entry))}. Cached per table.
2076
+ ***************************************************************/
2077
+ const KEY_FN_INDEX = new WeakMap();
2078
+
2079
+ function key_fn_index(table = I18N_KEY_FUNCTIONS)
2080
+ {
2081
+ if(KEY_FN_INDEX.has(table)) {
2082
+ return KEY_FN_INDEX.get(table);
1861
2083
  }
1862
- if(!KEY_FN_BY_MODULE.has(entry.module)) {
1863
- KEY_FN_BY_MODULE.set(entry.module, new Map());
2084
+ let exported = new Map();
2085
+ let by_module = new Map();
2086
+ for(let entry of table) {
2087
+ if(entry.exported) {
2088
+ exported.set(entry.name, entry);
2089
+ }
2090
+ if(!by_module.has(entry.module)) {
2091
+ by_module.set(entry.module, new Map());
2092
+ }
2093
+ by_module.get(entry.module).set(entry.name, entry);
1864
2094
  }
1865
- KEY_FN_BY_MODULE.get(entry.module).set(entry.name, entry);
2095
+ let index = {exported, by_module};
2096
+ KEY_FN_INDEX.set(table, index);
2097
+ return index;
1866
2098
  }
1867
2099
 
1868
2100
  /***************************************************************
@@ -1881,12 +2113,13 @@ function is_gobj_ui_source(src, lib_module)
1881
2113
  * The calls of a file to a key function, each with what the file
1882
2114
  * bound the callee to: an import of a gobj-ui function (by its
1883
2115
  * exported name), or, in the library module that defines it, the
1884
- * function itself.
2116
+ * function itself. `table` is I18N_KEY_FUNCTIONS by default.
1885
2117
  *
1886
2118
  * -> [{entry, call, scope}], `scope` the ancestors of the call
1887
2119
  ***************************************************************/
1888
- function key_calls(ast, lib_module)
2120
+ function key_calls(ast, lib_module, table)
1889
2121
  {
2122
+ let index = key_fn_index(table);
1890
2123
  let direct = new Map(); // local name -> entry
1891
2124
  let namespaces = new Set(); // `import * as ns` of a gobj-ui module
1892
2125
  for(let node of ast.body) {
@@ -1896,7 +2129,7 @@ function key_calls(ast, lib_module)
1896
2129
  for(let spec of node.specifiers) {
1897
2130
  if(spec.type === "ImportSpecifier") {
1898
2131
  let name = spec.imported.name || spec.imported.value;
1899
- let entry = KEY_FN_EXPORTED.get(name);
2132
+ let entry = index.exported.get(name);
1900
2133
  if(entry) {
1901
2134
  direct.set(spec.local.name, entry);
1902
2135
  }
@@ -1905,8 +2138,8 @@ function key_calls(ast, lib_module)
1905
2138
  }
1906
2139
  }
1907
2140
  }
1908
- if(lib_module && KEY_FN_BY_MODULE.has(lib_module)) {
1909
- for(let [name, entry] of KEY_FN_BY_MODULE.get(lib_module)) {
2141
+ if(lib_module && index.by_module.has(lib_module)) {
2142
+ for(let [name, entry] of index.by_module.get(lib_module)) {
1910
2143
  direct.set(name, entry);
1911
2144
  }
1912
2145
  }
@@ -1925,7 +2158,7 @@ function key_calls(ast, lib_module)
1925
2158
  entry = direct.get(c.name) || null;
1926
2159
  } else if(c.type === "MemberExpression" && !c.computed
1927
2160
  && c.object.type === "Identifier" && namespaces.has(c.object.name)) {
1928
- entry = KEY_FN_EXPORTED.get(c.property.name) || null;
2161
+ entry = index.exported.get(c.property.name) || null;
1929
2162
  }
1930
2163
  if(entry) {
1931
2164
  out.push({entry, call: n, scope: ancestors.slice()});
@@ -1937,15 +2170,17 @@ function key_calls(ast, lib_module)
1937
2170
  /***************************************************************
1938
2171
  * Is a leaf a key position of the function it is in, handed on?
1939
2172
  * `message` where `message` is a key parameter of that function,
1940
- * `opts.title` where it reads `opts.title`. Only a function of the
1941
- * table forwards: its own callers are read where they call it.
2173
+ * `opts.title` where it reads `opts.title` (or destructures it).
2174
+ * Only a function of the table forwards: its own callers are read
2175
+ * where they call it.
1942
2176
  ***************************************************************/
1943
- function is_forwarded(leaf, lib_module)
2177
+ function is_forwarded(leaf, lib_module, table)
1944
2178
  {
1945
- if(leaf.kind !== "param" || !lib_module || !KEY_FN_BY_MODULE.has(lib_module)) {
2179
+ let index = key_fn_index(table);
2180
+ if(leaf.kind !== "param" || !lib_module || !index.by_module.has(lib_module)) {
1946
2181
  return false;
1947
2182
  }
1948
- let entry = KEY_FN_BY_MODULE.get(lib_module).get(leaf.binding.name);
2183
+ let entry = index.by_module.get(lib_module).get(leaf.binding.name);
1949
2184
  if(!entry) {
1950
2185
  return false;
1951
2186
  }
@@ -1953,29 +2188,67 @@ function is_forwarded(leaf, lib_module)
1953
2188
  }
1954
2189
 
1955
2190
  /***************************************************************
1956
- * The rule a gobj-ui module keeps at every key position of a key
1957
- * function it calls (i18n_key_functions.test.js holds it): every
1958
- * value the position can take is
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.
2195
+ ***************************************************************/
2196
+ function is_translation(leaf)
2197
+ {
2198
+ return !!leaf.node && leaf.node.type === "CallExpression" && callee_name(leaf.node.callee) === "t";
2199
+ }
2200
+
2201
+ /***************************************************************
2202
+ * Is a leaf a plain `let` / `var` (itself, not a member of it) that
2203
+ * only ever holds DOM, or nothing: `let $msg = createElement2(...)`?
2204
+ * Everything it is given is followed (var_leaves()).
2205
+ ***************************************************************/
2206
+ function holds_only_dom(leaf)
2207
+ {
2208
+ if(leaf.kind !== "unknown" || !leaf.var_root || leaf.var_root.steps.length > 0) {
2209
+ return false;
2210
+ }
2211
+ let r = var_leaves(leaf.var_root);
2212
+ return r.complete && !r.written && r.leaves.length > 0
2213
+ && r.leaves.every((l) => l.kind === "dom" || l.kind === "nothing");
2214
+ }
2215
+
2216
+ /***************************************************************
2217
+ * The rule a file keeps at every key position of a key function it
2218
+ * calls -- a gobj-ui module (i18n_key_functions.test.js holds it)
2219
+ * and an app's own source (validate-locales fails it): every value
2220
+ * the position can take is
1959
2221
  *
1960
- * a) a string literal, or a `?:` / `||` / `??` of them, or nothing;
2222
+ * a) a string literal, or a `?:` / `||` / `??` of them, nothing,
2223
+ * or DOM (an array spec, createElement2(), ...: is_dom());
1961
2224
  * b) a `const` the reader follows to one of those (also through a
1962
- * const object or array, and the element of a const array its
1963
- * `.map()` / for...of iterates);
1964
- * c) a key position of the enclosing table function, handed on;
2225
+ * 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;
2229
+ * c) a key position of the enclosing table function, handed on
2230
+ * (only in the library: an app's functions are in no table);
1965
2231
  * d) MARKED where it is handed: `/\* i18n: <NAME>_I18N_KEYS *\/`
1966
- * (the module publishes those keys) or `/\* i18n: data *\/`.
2232
+ * (a module publishes those keys) or `/\* i18n: data *\/`.
2233
+ *
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.
1967
2237
  *
1968
- * -> [{line, call_line, name, path, text}]
2238
+ * -> [{line, call_line, name, path, text, why}]
1969
2239
  ***************************************************************/
1970
- function call_rule_violations(ast, lib_module)
2240
+ function call_rule_violations(ast, lib_module, table)
1971
2241
  {
1972
2242
  let markers = markers_of(ast);
1973
2243
  let out = [];
1974
- for(let {entry, call, scope} of key_calls(ast, lib_module)) {
2244
+ for(let {entry, call, scope} of key_calls(ast, lib_module, table)) {
1975
2245
  for(let path of entry.keys) {
1976
2246
  for(let leaf of values_at(call, path, scope)) {
1977
- if(leaf.kind === "key" || leaf.kind === "nothing"
1978
- || is_forwarded(leaf, lib_module) || marker_of(leaf, markers)) {
2247
+ let why = null;
2248
+ if(is_translation(leaf)) {
2249
+ why = "translated";
2250
+ } else if(KNOWN.has(leaf.kind) || is_forwarded(leaf, lib_module, table)
2251
+ || marker_of(leaf, markers) || holds_only_dom(leaf)) {
1979
2252
  continue;
1980
2253
  }
1981
2254
  out.push({
@@ -1983,7 +2256,8 @@ function call_rule_violations(ast, lib_module)
1983
2256
  call_line: line_of(ast, call.start),
1984
2257
  name: entry.name,
1985
2258
  path: path,
1986
- text: ast.code.slice(leaf.node.start, leaf.node.end).slice(0, 120)
2259
+ text: ast.code.slice(leaf.node.start, leaf.node.end).slice(0, 120),
2260
+ why: why
1987
2261
  });
1988
2262
  }
1989
2263
  }
@@ -1991,6 +2265,22 @@ function call_rule_violations(ast, lib_module)
1991
2265
  return out;
1992
2266
  }
1993
2267
 
2268
+ /***************************************************************
2269
+ * A violation of call_rule_violations() said to whoever has to fix
2270
+ * it, from the file `label`
2271
+ ***************************************************************/
2272
+ function call_rule_message(label, v)
2273
+ {
2274
+ let where = `${label}:${v.line}${v.call_line !== v.line? ` (call at line ${v.call_line})` : ""}`;
2275
+ let what = `${v.name}() [${v.path}] is given ${v.text.replace(/\s+/g, " ")}`;
2276
+ if(v.why === "translated") {
2277
+ 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`;
2279
+ }
2280
+ return `${where}: ${what}, which the reader cannot follow: hand it a literal or a const, ` +
2281
+ `or mark it /* i18n: data */ or /* i18n: <NAME>_I18N_KEYS */`;
2282
+ }
2283
+
1994
2284
 
1995
2285
  /***************************
1996
2286
  * Published lists
@@ -2000,14 +2290,17 @@ function call_rule_violations(ast, lib_module)
2000
2290
  /***************************************************************
2001
2291
  * The `<NAME>_I18N_KEYS` arrays a module exports, READ, never run:
2002
2292
  * `export const|let|var X = [...]`, `const X = [...]; export {X}`,
2003
- * either wrapped in Object.freeze(). Each element is a string literal
2004
- * or a template with no `${}`.
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
2295
+ * module read the same way -- a top-level `const`, or a list it
2296
+ * publishes: `[...BASE_I18N_KEYS, "k"]`.
2005
2297
  *
2006
2298
  * -> {lists: [{name, keys}], errors: [why]}
2007
2299
  *
2008
2300
  * Anything else so named is an ERROR, never skipped: the keys it
2009
2301
  * holds would go unasked, and the build say OK. That covers an
2010
- * export that is not an array of literals, and a list nobody can
2302
+ * 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
2011
2304
  * read: one declared and not exported, one declared inside a
2012
2305
  * function or a block, one declared by destructuring, one exported
2013
2306
  * under a name that does not end in _I18N_KEYS. A re-export
@@ -2024,7 +2317,7 @@ function published_lists(ast)
2024
2317
  if(decl.type === "VariableDeclaration") {
2025
2318
  for(let d of decl.declarations) {
2026
2319
  if(d.id.type === "Identifier") {
2027
- top.set(d.id.name, {init: d.init});
2320
+ top.set(d.id.name, {init: d.init, kind: decl.kind});
2028
2321
  }
2029
2322
  }
2030
2323
  } else if(decl.id && (decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration")) {
@@ -2032,6 +2325,15 @@ function published_lists(ast)
2032
2325
  }
2033
2326
  }
2034
2327
 
2328
+ /* What a list may spread: a const of the module, or a list it publishes */
2329
+ let base_of = (name) => {
2330
+ let d = top.get(name);
2331
+ if(!d || !d.init || !d.kind) {
2332
+ return null;
2333
+ }
2334
+ return (d.kind === "const" || PUBLISHED_LIST_RE.test(name))? d.init : null;
2335
+ };
2336
+
2035
2337
  let lists = [];
2036
2338
  let errors = [];
2037
2339
  let read_as = new Set(); // local names exported under a list's name
@@ -2046,7 +2348,7 @@ function published_lists(ast)
2046
2348
  errors.push(`${exported}: ${d.what? "a " + d.what : "declared with no value"}, not an array`);
2047
2349
  return;
2048
2350
  }
2049
- let r = read_string_array(d.init);
2351
+ let r = read_string_array(d.init, base_of, new Set([local]));
2050
2352
  if(r.error) {
2051
2353
  errors.push(`${exported}: ${r.error}`);
2052
2354
  return;
@@ -2113,9 +2415,12 @@ function published_lists(ast)
2113
2415
  }
2114
2416
 
2115
2417
  /***************************************************************
2116
- * An array of string literals, through any Object.freeze()
2418
+ * An array of string literals, through any Object.freeze(). A
2419
+ * spread `...NAME` is read where `base_of(NAME)` says it is declared
2420
+ * (null: it cannot be read); `seen` holds the names on the way, so a
2421
+ * cycle is an error, not a loop.
2117
2422
  ***************************************************************/
2118
- function read_string_array(init)
2423
+ function read_string_array(init, base_of = () => null, seen = new Set())
2119
2424
  {
2120
2425
  let n = init;
2121
2426
  while(is_object_call(n, "freeze") && n.arguments.length === 1) {
@@ -2127,6 +2432,20 @@ function read_string_array(init)
2127
2432
  let keys = [];
2128
2433
  for(let i = 0; i < n.elements.length; i++) {
2129
2434
  let el = n.elements[i];
2435
+ if(el && el.type === "SpreadElement") {
2436
+ let name = (el.argument.type === "Identifier")? el.argument.name : null;
2437
+ let base = (name && !seen.has(name))? base_of(name) : null;
2438
+ if(!base) {
2439
+ return {error: `element ${i} spreads ${name || "a " + el.argument.type}, which is neither a ` +
2440
+ `const of the module nor a list it publishes`};
2441
+ }
2442
+ let r = read_string_array(base, base_of, new Set([...seen, name]));
2443
+ if(r.error) {
2444
+ return {error: `element ${i} spreads ${name}, ${r.error}`};
2445
+ }
2446
+ keys.push(...r.keys);
2447
+ continue;
2448
+ }
2130
2449
  let s = null;
2131
2450
  if(el && el.type === "Literal" && typeof el.value === "string") {
2132
2451
  s = el.value;
@@ -2148,6 +2467,97 @@ function read_string_array(init)
2148
2467
  ***************************/
2149
2468
 
2150
2469
 
2470
+ /***************************************************************
2471
+ * Every place a parsed file READS a key, with the leaves of what
2472
+ * stands there (leaves_of(), values_at()):
2473
+ *
2474
+ * -> [{what, node, leaves, entry, path}]
2475
+ *
2476
+ * `what` says which place it is:
2477
+ *
2478
+ * "t" the first argument of t() / tr()
2479
+ * "code" the code of a schema check, add("error", <code>)
2480
+ * "attribute" setAttribute("data-i18n*", <key>)
2481
+ * "property" an `i18n`, `code` or `data-i18n*` property of an
2482
+ * object literal, or one of ctx.attr_keys
2483
+ * "array" an element of one of ctx.array_keys
2484
+ * "assignment" `x.i18n = <key>`, `x["data-i18n-title"] = <key>`
2485
+ * "call" a key position of a key function: `entry` and
2486
+ * `path` say which
2487
+ *
2488
+ * `node` is the node of the place (the call, the property, the
2489
+ * assignment), where a marker of the whole place stands.
2490
+ *
2491
+ * ctx: {lib_module, attr_keys, array_keys (Sets), table}
2492
+ ***************************************************************/
2493
+ function key_places(ast, ctx = {})
2494
+ {
2495
+ let attr_keys = ctx.attr_keys || new Set();
2496
+ 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));
2499
+ let out = [];
2500
+ let read = (what, node, scope, root) => {
2501
+ if(node) {
2502
+ out.push({what, node: root, leaves: leaves_of(node, scope, root)});
2503
+ }
2504
+ };
2505
+ walk(ast, (n, ancestors) => {
2506
+ switch(n.type) {
2507
+ 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
2512
+ }
2513
+ if(name === "t" || name === "tr") {
2514
+ read("t", n.arguments[0], scope, n);
2515
+ } else if(name === "add" && is_string_literal(n.arguments[0], /^(error|warning)$/)) {
2516
+ /* 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);
2520
+ }
2521
+ break;
2522
+ }
2523
+ case "Property": {
2524
+ let name = prop_name(n);
2525
+ if(name === null || ancestors[ancestors.length - 1].type !== "ObjectExpression") {
2526
+ break; // computed, or a destructuring pattern
2527
+ }
2528
+ let scope = [...ancestors, n];
2529
+ if(name === "i18n" || name === "code" || DATA_I18N_ATTR_RE.test(name) || attr_keys.has(name)) {
2530
+ read("property", n.value, scope, n);
2531
+ }
2532
+ if(array_keys.has(name) && n.value.type === "ArrayExpression") {
2533
+ for(let el of n.value.elements) {
2534
+ read("array", el, [...scope, n.value], n);
2535
+ }
2536
+ }
2537
+ break;
2538
+ }
2539
+ case "AssignmentExpression": {
2540
+ if(n.operator !== "=" || n.left.type !== "MemberExpression") {
2541
+ break;
2542
+ }
2543
+ 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);
2546
+ }
2547
+ break;
2548
+ }
2549
+ default:
2550
+ break;
2551
+ }
2552
+ });
2553
+ for(let {entry, call, scope} of calls) {
2554
+ for(let path of entry.keys) {
2555
+ out.push({what: "call", node: call, entry, path, leaves: values_at(call, path, scope)});
2556
+ }
2557
+ }
2558
+ return out;
2559
+ }
2560
+
2151
2561
  /***************************************************************
2152
2562
  * The keys one parsed file asks for.
2153
2563
  *
@@ -2158,16 +2568,18 @@ function read_string_array(init)
2158
2568
  * lists: [{name, keys}], list_errors: [why],
2159
2569
  * marker_refs: [{lists, keys, line}], marker_errors: [{line, why}]}
2160
2570
  *
2161
- * Every place a key is read goes through leaves_of(), so a marker
2162
- * standing there is found: one that marks no such place, or that is
2163
- * not a marker at all, is an error.
2571
+ * Every place a key is read (key_places()) goes through leaves_of(),
2572
+ * 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.
2164
2578
  ***************************************************************/
2165
2579
  function scan_ast(ast, ctx)
2166
2580
  {
2167
2581
  let keys = new Set();
2168
2582
  let declared_seen = new Set();
2169
- let attr_keys = ctx.attr_keys || new Set();
2170
- let array_keys = ctx.array_keys || new Set();
2171
2583
  let declared = ctx.declared || new Set();
2172
2584
  let markers = markers_of(ast);
2173
2585
  let var_errors = [];
@@ -2231,6 +2643,10 @@ function scan_ast(ast, ctx)
2231
2643
  if(!m.keys) {
2232
2644
  m.keys = new Set();
2233
2645
  }
2646
+ if(!m.leaves) {
2647
+ m.leaves = [];
2648
+ }
2649
+ m.leaves.push(leaf);
2234
2650
  if(leaf.kind === "key") {
2235
2651
  m.keys.add(leaf.value);
2236
2652
  }
@@ -2240,11 +2656,6 @@ function scan_ast(ast, ctx)
2240
2656
  }
2241
2657
  }
2242
2658
  };
2243
- let read = (node, scope, root) => {
2244
- if(node) {
2245
- take(leaves_of(node, scope, root));
2246
- }
2247
- };
2248
2659
  let add_markup = (text) => {
2249
2660
  if(typeof text !== "string" || !text.includes("data-i18n")) {
2250
2661
  return;
@@ -2258,61 +2669,22 @@ function scan_ast(ast, ctx)
2258
2669
  }
2259
2670
  };
2260
2671
 
2261
- walk(ast, (n, ancestors) => {
2262
- switch(n.type) {
2263
- case "CallExpression": {
2264
- let name = callee_name(n.callee);
2265
- let scope = [...ancestors, n];
2266
- if(name === "t" || name === "tr") {
2267
- read(n.arguments[0], scope, n);
2268
- } else if(name === "add" && is_string_literal(n.arguments[0], /^(error|warning)$/)) {
2269
- /* The codes the schema checks hand to `t(finding.code)` */
2270
- read(n.arguments[1], scope, n);
2271
- } else if(name === "setAttribute" && is_string_literal(n.arguments[0], DATA_I18N_ATTR_RE)) {
2272
- read(n.arguments[1], scope, n);
2273
- }
2274
- break;
2275
- }
2276
- case "Property": {
2277
- let name = prop_name(n);
2278
- if(name === null) {
2279
- break;
2280
- }
2281
- if(declared.has(name)) {
2282
- declared_seen.add(name);
2283
- }
2284
- if(ancestors[ancestors.length - 1].type !== "ObjectExpression") {
2285
- break; // a destructuring pattern
2286
- }
2287
- let scope = [...ancestors, n];
2288
- if(name === "i18n" || name === "code" || DATA_I18N_ATTR_RE.test(name) || attr_keys.has(name)) {
2289
- read(n.value, scope, n);
2290
- }
2291
- if(array_keys.has(name) && n.value.type === "ArrayExpression") {
2292
- for(let el of n.value.elements) {
2293
- read(el, [...scope, n.value], n);
2294
- }
2295
- }
2296
- break;
2672
+ for(let place of key_places(ast, ctx)) {
2673
+ take(place.leaves);
2674
+ }
2675
+ walk(ast, (n) => {
2676
+ if(n.type === "Property") {
2677
+ let name = prop_name(n);
2678
+ if(name !== null && declared.has(name)) {
2679
+ declared_seen.add(name);
2297
2680
  }
2298
- case "Literal":
2299
- add_markup(n.value);
2300
- break;
2301
- case "TemplateElement":
2302
- add_markup(n.value.cooked);
2303
- break;
2304
- default:
2305
- break;
2681
+ } else if(n.type === "Literal") {
2682
+ add_markup(n.value);
2683
+ } else if(n.type === "TemplateElement") {
2684
+ add_markup(n.value.cooked);
2306
2685
  }
2307
2686
  });
2308
2687
 
2309
- /* The keys handed to a key function */
2310
- for(let {entry, call, scope} of key_calls(ast, ctx.lib_module || null)) {
2311
- for(let path of entry.keys) {
2312
- take(values_at(call, path, scope));
2313
- }
2314
- }
2315
-
2316
2688
  /* The keys a module publishes as data */
2317
2689
  let read_keys = new Set(keys);
2318
2690
  let published = published_lists(ast);
@@ -2331,6 +2703,10 @@ function scan_ast(ast, ctx)
2331
2703
  `(a key position of a key function, t(), an i18n or data-i18n* value)`});
2332
2704
  } else if(m.kind === "lists") {
2333
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) {
2707
+ let held = [...m.keys].map((k) => JSON.stringify(k)).join(", ");
2708
+ marker_errors.push({line: m.line, why: `marked as data, but what it marks is the key ` +
2709
+ `${held}, which the reader follows: drop the marker (it is no data)`});
2334
2710
  }
2335
2711
  }
2336
2712
 
@@ -2358,8 +2734,10 @@ function scan_ast(ast, ctx)
2358
2734
  * modules the app mounts)}
2359
2735
  *
2360
2736
  * A violation found on the way (a file that cannot be parsed, a list
2361
- * that cannot be read, a marker that is wrong) is reported with
2362
- * fail(): see violation_count().
2737
+ * that cannot be read, a marker that is wrong, a key position of a
2738
+ * key function in the app's own code that breaks the per-call rule,
2739
+ * call_rule_violations()) is reported with fail(): see
2740
+ * violation_count().
2363
2741
  ***************************************************************/
2364
2742
  function collect_used_keys(opts)
2365
2743
  {
@@ -2413,6 +2791,12 @@ function collect_used_keys(opts)
2413
2791
  for(let ref of r.marker_refs) {
2414
2792
  refs.push({label, ...ref});
2415
2793
  }
2794
+ if(!lib_module) {
2795
+ /* The library keeps the rule by its own test; the app's code here */
2796
+ for(let v of call_rule_violations(ast, null)) {
2797
+ fail(call_rule_message(label, v));
2798
+ }
2799
+ }
2416
2800
  return lib_imports(ast, barrel, lib_module);
2417
2801
  };
2418
2802
 
@@ -2692,12 +3076,16 @@ export {
2692
3076
  leaves_of,
2693
3077
  static_keys,
2694
3078
  source_keys,
3079
+ var_leaves,
2695
3080
  values_at,
3081
+ is_dom,
2696
3082
  markers_of,
2697
3083
  marker_of,
2698
3084
  key_calls,
3085
+ key_places,
2699
3086
  is_forwarded,
2700
3087
  call_rule_violations,
3088
+ call_rule_message,
2701
3089
  published_lists,
2702
3090
  scan_ast,
2703
3091
  collect_used_keys,