@yuneta/gobj-ui 7.26.28 → 7.26.29
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.
- package/README.md +169 -40
- package/dist/gobj-ui.cjs.js +17 -13
- package/dist/gobj-ui.es.js +17 -13
- package/package.json +4 -2
- package/scripts/validate-locales.mjs +565 -177
- package/src/c_yui_form.js +5 -3
- package/src/c_yui_gobj_tree_js.js +1 -1
- package/src/c_yui_nav.js +12 -10
- package/src/c_yui_period.js +3 -3
- package/src/c_yui_schema_editor.js +22 -18
- package/src/c_yui_shell.js +4 -3
- package/src/c_yui_treedb_graph.js +3 -4
- package/src/c_yui_treedb_topic_with_form.js +15 -15
- package/src/c_yui_treedb_topics.js +8 -8
- package/src/c_yui_window_manager.js +5 -2
- package/src/i18n_key_functions.js +112 -23
- package/src/i18n_key_functions.test.js +329 -8
- package/src/shell_route_map.js +2 -2
- package/src/yui_gclass_view.js +1 -1
- package/src/yui_time.js +1 -1
|
@@ -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
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
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.
|
|
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,
|
|
58
|
-
*
|
|
59
|
-
*
|
|
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()
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
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
|
|
81
|
-
*
|
|
82
|
-
* 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.
|
|
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).
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
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
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
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`,
|
|
138
|
-
*
|
|
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
|
|
210
|
-
*
|
|
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}):
|
|
217
|
-
`
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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}
|
|
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
|
-
*
|
|
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: "
|
|
959
|
-
*
|
|
960
|
-
*
|
|
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
|
|
974
|
-
if(
|
|
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,
|
|
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`
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
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
|
-
|
|
1863
|
-
|
|
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
|
-
|
|
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 =
|
|
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 &&
|
|
1909
|
-
for(let [name, entry] of
|
|
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 =
|
|
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
|
|
1941
|
-
* table forwards: its own callers are read
|
|
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
|
-
|
|
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 =
|
|
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
|
-
*
|
|
1957
|
-
*
|
|
1958
|
-
*
|
|
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,
|
|
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,
|
|
1963
|
-
* `.map()` / for...of iterates
|
|
1964
|
-
*
|
|
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
|
-
* (
|
|
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
|
-
|
|
1978
|
-
|
|
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
|
-
*
|
|
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
|
|
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(),
|
|
2162
|
-
* standing there is found: one that marks no such place,
|
|
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
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
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
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
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
|
|
2362
|
-
*
|
|
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,
|