@yuneta/gobj-ui 7.26.27 → 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 +590 -25
- package/dist/gobj-ui.cjs.js +1080 -229
- package/dist/gobj-ui.es.js +1081 -230
- package/package.json +4 -2
- package/scripts/validate-locales.mjs +2656 -315
- package/src/c_g6_nodes_tree.js +1086 -253
- package/src/c_yui_form.js +16 -4
- package/src/c_yui_fsm_graph.js +5 -0
- package/src/c_yui_gclass.js +11 -0
- package/src/c_yui_gobj_tree_js.js +18 -1
- package/src/c_yui_json_graph.js +5 -0
- package/src/c_yui_nav.js +21 -16
- package/src/c_yui_pager.js +1 -1
- package/src/c_yui_period.js +10 -3
- package/src/c_yui_schema_editor.js +34 -18
- package/src/c_yui_shell.js +11 -10
- package/src/c_yui_treedb_graph.js +17 -9
- package/src/c_yui_treedb_schema.js +5 -0
- package/src/c_yui_treedb_topic_with_form.js +9 -9
- package/src/c_yui_treedb_topics.js +13 -13
- package/src/c_yui_window.js +2 -2
- package/src/c_yui_window_manager.js +5 -2
- package/src/c_yui_wizard.js +8 -1
- package/src/form_json_field.js +7 -0
- package/src/g6_canvas_tab_order.js +84 -0
- package/src/g6_canvas_tab_order.wiring.test.js +232 -0
- package/src/g6_nodes_tree_controls.test.js +547 -0
- package/src/g6_nodes_tree_focus.wiring.test.js +1360 -0
- package/src/g6_nodes_tree_i18n_keys.test.js +241 -5
- package/src/i18n_key_functions.js +235 -0
- package/src/i18n_key_functions.test.js +1525 -0
- package/src/lib_graph.js +38 -1
- package/src/nav_backbar_name.wiring.test.js +82 -0
- package/src/schema_flags.js +47 -0
- package/src/schema_import.js +7 -0
- package/src/shell_modals.js +20 -16
- package/src/shell_route_map.js +13 -3
- package/src/wizard_helpers.js +8 -0
- package/src/yui_dev.js +16 -3
- package/src/yui_dev_controls.test.js +174 -0
- package/src/yui_gclass_view.js +1 -1
- package/src/yui_graph_camera.js +1 -1
- package/src/yui_install.js +10 -8
- package/src/yui_table_select.js +2 -2
- package/src/yui_time.js +17 -2
|
@@ -24,12 +24,16 @@
|
|
|
24
24
|
* language never uses is a violation; `_zero` is always
|
|
25
25
|
* allowed (i18next uses it for a count of 0). i18next itself
|
|
26
26
|
* falls back to the bare key for ANY missing form: asking for
|
|
27
|
-
* `one`/`few` is a policy of grammar, not of i18next. A
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
27
|
+
* `one`/`few` is a policy of grammar, not of i18next. A group
|
|
28
|
+
* is a plural only when some locale has its bare base
|
|
29
|
+
* (`songs`) or its `_other` form: keys that only END like
|
|
30
|
+
* forms are plain keys (`pick_one` alone; `step_one`,
|
|
31
|
+
* `step_two`, `step_three`, an enumeration). A form asked for
|
|
32
|
+
* directly, `t("pick_one")` next to a bare `pick`, is a key
|
|
33
|
+
* the source uses like any other. Ordinals
|
|
31
34
|
* (`place_ordinal_one`) are checked with the ordinal
|
|
32
|
-
* categories of each language
|
|
35
|
+
* categories of each language; the `_ordinal_` infix says
|
|
36
|
+
* they are plural forms.
|
|
33
37
|
* 4. No duplicate key in a locale file (an object literal keeps
|
|
34
38
|
* the LAST one and says nothing -- a stale entry silently
|
|
35
39
|
* overrides a new one; it happened to "last" and "loading").
|
|
@@ -38,10 +42,9 @@
|
|
|
38
42
|
* fine and simply never changes language -- invisible by
|
|
39
43
|
* construction, and it shipped: a Settings `label` column).
|
|
40
44
|
*
|
|
41
|
-
* Rule 5
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* keys it hands to t() as variables, yui_dev.js's 51), the shell
|
|
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
|
|
45
48
|
* declaration (`src/app_config.json`) and the gobj-ui modules the
|
|
46
49
|
* app MOUNTS, transitively -- deep, through the barrel, or by a
|
|
47
50
|
* dynamic import(): the library translates through the APP's
|
|
@@ -49,7 +52,85 @@
|
|
|
49
52
|
* renders raw (that is how the treedb editor shipped reading
|
|
50
53
|
* "edit", "new", "delete" in every language). The library scanned
|
|
51
54
|
* is THIS one -- the package the app installed and builds with,
|
|
52
|
-
* 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:
|
|
58
|
+
*
|
|
59
|
+
* - the string given to t() / tr(), to an `i18n` or `code`
|
|
60
|
+
* property, to a `data-i18n*` attribute (as a property, as a
|
|
61
|
+
* member written, `x.i18n = k` / `x["data-i18n-title"] = k`,
|
|
62
|
+
* by setAttribute(), or written in markup), and the codes of
|
|
63
|
+
* the schema checks (`add("error", "<code>")`);
|
|
64
|
+
* - the strings given to a gobj-ui function that RECEIVES a key
|
|
65
|
+
* as an argument (`yui_shell_show_error(shell, "<key>")`,
|
|
66
|
+
* `icon_button(..., "<key>")`), at the positions its table
|
|
67
|
+
* names: I18N_KEY_FUNCTIONS, src/i18n_key_functions.js. A
|
|
68
|
+
* position may hold DOM as well, which is no key: an array
|
|
69
|
+
* (a createElement2 spec), createElement2(), createOneHtml()
|
|
70
|
+
* or html_markup() of gobj-js, text_node() of gobj-ui,
|
|
71
|
+
* document.createElement() and the like -- known by where
|
|
72
|
+
* they come from, never by the name of a variable;
|
|
73
|
+
* - the keys a module PUBLISHES as data: an exported
|
|
74
|
+
* `<NAME>_I18N_KEYS` array of string literals (the keys it
|
|
75
|
+
* hands to t() as variables, yui_dev.js's 51). Read as data
|
|
76
|
+
* -- `export const`, `let` or `var`, `export {X}`, wrapped in
|
|
77
|
+
* Object.freeze(), spreading another array of the module read
|
|
78
|
+
* the same way (`[...BASE_I18N_KEYS, "k"]`: a top-level const,
|
|
79
|
+
* or a list it publishes) --; anything so named that cannot be
|
|
80
|
+
* read so is a violation, never skipped (a list not exported,
|
|
81
|
+
* declared in a function, by destructuring, or exported under
|
|
82
|
+
* another name; an element that is a variable, a call, a spread
|
|
83
|
+
* of an import or of a `let`).
|
|
84
|
+
*
|
|
85
|
+
* A string is read where it is written, and FOLLOWED where the
|
|
86
|
+
* source writes it out: a literal, a template with no `${}`,
|
|
87
|
+
* every branch of `?:`, `||` and `??`, the right one of `&&`; a
|
|
88
|
+
* `const` to its value (`const KEY = "save"; t(KEY)` asks for
|
|
89
|
+
* "save"); a property of a const object literal (`DEFAULTS.ok`;
|
|
90
|
+
* every value of it for `NOTES[verdict]`); the element of a const
|
|
91
|
+
* array literal its `.map()`, `.forEach()`, ... callback or a
|
|
92
|
+
* for...of iterates, and what a destructuring reads from it;
|
|
93
|
+
* `String(x)`, and a template that is only `${x}`, as x; the
|
|
94
|
+
* default of a parameter; a call of a function of the same module
|
|
95
|
+
* whose every `return` is one of these (`t(pick())`, `pick`
|
|
96
|
+
* returning "a" or "b", asks for both). A key built by
|
|
97
|
+
* concatenation, held in a `let`, or arriving as data cannot be
|
|
98
|
+
* known: its module publishes it in a `<NAME>_I18N_KEYS` list.
|
|
99
|
+
*
|
|
100
|
+
* A MARKER at the place a key is read says where the keys of what
|
|
101
|
+
* follows it come from: `/\* i18n: <NAME>_I18N_KEYS *\/` (that
|
|
102
|
+
* published list; several by commas) or `/\* i18n: data *\/` (data,
|
|
103
|
+
* no key a list can hold). This script checks the markers it
|
|
104
|
+
* reads: each one must be a marker and stand before a place a key
|
|
105
|
+
* is read, the lists it names must be published by a module the
|
|
106
|
+
* app mounts (the app's own included), and the keys it marks that
|
|
107
|
+
* can be read must be in them. A `data` marker on what the reader
|
|
108
|
+
* follows to keys and nothing else -- a literal, a const that is
|
|
109
|
+
* one, a `?:` branch that is one, a call of a function of the
|
|
110
|
+
* module that returns literals -- is a violation: it claims data
|
|
111
|
+
* where the reader reads a key. A marked `let` / `var` is READ,
|
|
112
|
+
* and so is one a marked member or element is read from (`LABELS[k]`,
|
|
113
|
+
* `picked = LIST[0]`, the `x` of `LIST.forEach((x) => ...)`):
|
|
114
|
+
* everything it is given in its scope (its value, every `=` to
|
|
115
|
+
* it; for a member, also what is written into it: `x.a = v`,
|
|
116
|
+
* `x.push(v)`, Object.assign()) is followed, each literal is a key
|
|
117
|
+
* asked for and must be in the lists named, and one given nothing
|
|
118
|
+
* but literals is a violation: it is a const.
|
|
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.
|
|
53
134
|
*
|
|
54
135
|
* Options (paths relative to the app's directory):
|
|
55
136
|
*
|
|
@@ -74,13 +155,26 @@
|
|
|
74
155
|
*
|
|
75
156
|
* A locale is a file <code>.js of the locales directory (en.js,
|
|
76
157
|
* es.js, pt-br.js; flags.js and locales.js are not) exporting
|
|
77
|
-
* `{<code>}` with a `translation` dict. It is
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
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.
|
|
81
168
|
*
|
|
82
169
|
* Exits 1 on any violation, so the build fails loudly.
|
|
83
170
|
*
|
|
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.
|
|
173
|
+
*
|
|
174
|
+
* Its readers are exported: an app whose catalog has another shape
|
|
175
|
+
* (gobj-ui's own test-app, English as the source) loads the catalog
|
|
176
|
+
* itself and reuses collect_used_keys() for everything else.
|
|
177
|
+
*
|
|
84
178
|
* Up to gobj-ui 7.26.24 every app carried its own copy, and the
|
|
85
179
|
* copies drifted into three families with different rules
|
|
86
180
|
* (REV-CLOUD V17.3.4). This one is their union, and an app keeps
|
|
@@ -89,13 +183,17 @@
|
|
|
89
183
|
* Copyright (c) 2025-2026, ArtGins.
|
|
90
184
|
* All Rights Reserved.
|
|
91
185
|
***********************************************************************/
|
|
92
|
-
import {readdirSync, readFileSync, existsSync, statSync} from "fs";
|
|
93
|
-
import {resolve} from "path";
|
|
94
|
-
import {pathToFileURL} from "url";
|
|
186
|
+
import {readdirSync, readFileSync, existsSync, statSync, realpathSync} from "fs";
|
|
187
|
+
import {resolve, relative} from "path";
|
|
188
|
+
import {pathToFileURL, fileURLToPath} from "url";
|
|
189
|
+
import {I18N_KEY_FUNCTIONS} from "../src/i18n_key_functions.js";
|
|
95
190
|
|
|
96
191
|
const ASCII_RE = /^[\x20-\x7e]+$/;
|
|
97
192
|
const LOCALE_FILE_RE = /^([a-z]{2,3}(?:[-_][a-zA-Z]{2,4})?)\.js$/;
|
|
98
193
|
const PLURAL_RE = /^(.+?)(_ordinal)?_(zero|one|two|few|many|other)$/;
|
|
194
|
+
const PUBLISHED_LIST_RE = /_I18N_KEYS$/i;
|
|
195
|
+
const DATA_I18N_ATTR_RE = /^data-i18n(?:-title|-aria-label|-placeholder)?$/;
|
|
196
|
+
const DATA_I18N_MARKUP_RE = /data-i18n(?:-title|-aria-label|-placeholder)?=["']([^"']+)["']/g;
|
|
99
197
|
|
|
100
198
|
/* This package: the library the app builds with. */
|
|
101
199
|
const LIB = new URL("../", import.meta.url);
|
|
@@ -116,6 +214,14 @@ function fail(msg)
|
|
|
116
214
|
errors++;
|
|
117
215
|
}
|
|
118
216
|
|
|
217
|
+
/***************************************************************
|
|
218
|
+
* The violations reported so far (by fail())
|
|
219
|
+
***************************************************************/
|
|
220
|
+
function violation_count()
|
|
221
|
+
{
|
|
222
|
+
return errors;
|
|
223
|
+
}
|
|
224
|
+
|
|
119
225
|
/***************************************************************
|
|
120
226
|
* A fatal problem of the setup, not a violation: said, exit 1
|
|
121
227
|
***************************************************************/
|
|
@@ -133,6 +239,18 @@ function note(msg)
|
|
|
133
239
|
process.stdout.write(`validate-locales: ${msg}\n`);
|
|
134
240
|
}
|
|
135
241
|
|
|
242
|
+
/*
|
|
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).
|
|
245
|
+
*/
|
|
246
|
+
let acorn_parse;
|
|
247
|
+
try {
|
|
248
|
+
({parse: acorn_parse} = await import("acorn"));
|
|
249
|
+
} catch(e) {
|
|
250
|
+
abort(`cannot load acorn (${e.message}): it is a dependency of @yuneta/gobj-ui, ` +
|
|
251
|
+
`so the install is incomplete: run npm install`);
|
|
252
|
+
}
|
|
253
|
+
|
|
136
254
|
/***************************************************************
|
|
137
255
|
* The directory a path names, as a URL ending in '/'
|
|
138
256
|
***************************************************************/
|
|
@@ -195,6 +313,17 @@ function parse_args(argv)
|
|
|
195
313
|
return opts;
|
|
196
314
|
}
|
|
197
315
|
|
|
316
|
+
/***************************************************************
|
|
317
|
+
* The names a locale file <code>.js may export its locale under:
|
|
318
|
+
* pt-br -> pt-br, pt_br, ptBr, ptBR
|
|
319
|
+
***************************************************************/
|
|
320
|
+
function locale_export_names(code)
|
|
321
|
+
{
|
|
322
|
+
let camel = code.replace(/[-_]([a-z])/gi, (m, c) => c.toUpperCase()); // pt-br -> ptBr
|
|
323
|
+
let upper = code.replace(/[-_]([a-z]+)/gi, (m, c) => c.toUpperCase()); // pt-br -> ptBR
|
|
324
|
+
return [...new Set([code, code.replace(/-/g, "_"), camel, upper])];
|
|
325
|
+
}
|
|
326
|
+
|
|
198
327
|
/***************************************************************
|
|
199
328
|
* The locales of the app: {code: translation}
|
|
200
329
|
***************************************************************/
|
|
@@ -214,20 +343,49 @@ async function load_locales(locales_dir)
|
|
|
214
343
|
let locales = {};
|
|
215
344
|
let names = new Set();
|
|
216
345
|
for(let code of codes) {
|
|
346
|
+
let url = new URL(`${code}.js`, locales_dir);
|
|
347
|
+
let export_names = locale_export_names(code);
|
|
348
|
+
|
|
349
|
+
/* Parsed first, never trusted to the import: a syntax error is
|
|
350
|
+
* a broken file, not "a helper that needs a browser". */
|
|
351
|
+
let ast;
|
|
352
|
+
try {
|
|
353
|
+
ast = parse_source(readFileSync(url, "utf8"));
|
|
354
|
+
} catch(e) {
|
|
355
|
+
fail(`${code}.js cannot be parsed (${e.message}): its language is NOT validated`);
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
let looks = looks_like_a_locale(ast, export_names);
|
|
359
|
+
|
|
217
360
|
let mod;
|
|
218
361
|
try {
|
|
219
|
-
mod = await import(
|
|
362
|
+
mod = await import(url.href);
|
|
220
363
|
} catch(e) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
364
|
+
if(looks) {
|
|
365
|
+
fail(`${code}.js ${looks} and cannot be loaded (${e.message}): ` +
|
|
366
|
+
`its language is NOT validated`);
|
|
367
|
+
} else {
|
|
368
|
+
/* A helper that happens to have a locale's name (app.js),
|
|
369
|
+
* and needs a browser to load: said and skipped */
|
|
370
|
+
note(`${code}.js cannot be loaded here (${e.message}): not a locale, skipped`);
|
|
371
|
+
}
|
|
224
372
|
continue;
|
|
225
373
|
}
|
|
226
|
-
let
|
|
227
|
-
let
|
|
228
|
-
|
|
374
|
+
let locale = null;
|
|
375
|
+
for(let name of export_names) {
|
|
376
|
+
if(mod[name]) {
|
|
377
|
+
locale = mod[name];
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
locale = locale || mod.default;
|
|
229
382
|
if(!locale || !locale.translation || typeof locale.translation !== "object") {
|
|
230
|
-
|
|
383
|
+
if(looks) {
|
|
384
|
+
fail(`${code}.js ${looks} but exports no {${code}} with a translation dict: ` +
|
|
385
|
+
`its language is NOT validated`);
|
|
386
|
+
} else {
|
|
387
|
+
note(`${code}.js exports no {${code}} with a translation dict: not a locale, skipped`);
|
|
388
|
+
}
|
|
231
389
|
continue;
|
|
232
390
|
}
|
|
233
391
|
locales[code] = locale.translation;
|
|
@@ -241,6 +399,51 @@ async function load_locales(locales_dir)
|
|
|
241
399
|
return {locales, names};
|
|
242
400
|
}
|
|
243
401
|
|
|
402
|
+
/***************************************************************
|
|
403
|
+
* Does a parsed file look like a locale? It does when it holds an
|
|
404
|
+
* object literal with a `translation` dict, when it exports one of
|
|
405
|
+
* the names of its locale (es.js exporting `es`), or when it has a
|
|
406
|
+
* default export. Returns what it saw (for the message), or "".
|
|
407
|
+
***************************************************************/
|
|
408
|
+
function looks_like_a_locale(ast, export_names)
|
|
409
|
+
{
|
|
410
|
+
let saw = "";
|
|
411
|
+
for(let node of ast.body) {
|
|
412
|
+
if(node.type === "ExportDefaultDeclaration") {
|
|
413
|
+
return "has a default export";
|
|
414
|
+
}
|
|
415
|
+
if(node.type !== "ExportNamedDeclaration") {
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
let exported = [];
|
|
419
|
+
if(node.declaration && node.declaration.type === "VariableDeclaration") {
|
|
420
|
+
for(let d of node.declaration.declarations) {
|
|
421
|
+
exported.push(...pattern_names(d.id));
|
|
422
|
+
}
|
|
423
|
+
} else if(node.declaration && node.declaration.id) {
|
|
424
|
+
exported.push(node.declaration.id.name);
|
|
425
|
+
}
|
|
426
|
+
for(let spec of node.specifiers || []) {
|
|
427
|
+
exported.push(spec.exported.name || spec.exported.value);
|
|
428
|
+
}
|
|
429
|
+
let own = exported.find((name) => export_names.includes(name));
|
|
430
|
+
if(own) {
|
|
431
|
+
return `exports ${own}`;
|
|
432
|
+
}
|
|
433
|
+
if(exported.includes("default")) {
|
|
434
|
+
return "has a default export";
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
walk(ast, (n, ancestors) => {
|
|
438
|
+
if(!saw && n.type === "Property" && prop_name(n) === "translation"
|
|
439
|
+
&& n.value.type === "ObjectExpression"
|
|
440
|
+
&& ancestors[ancestors.length - 1].type === "ObjectExpression") {
|
|
441
|
+
saw = "holds a translation dict";
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
return saw;
|
|
445
|
+
}
|
|
446
|
+
|
|
244
447
|
/***************************************************************
|
|
245
448
|
* The plural categories of a language (Intl.PluralRules)
|
|
246
449
|
***************************************************************/
|
|
@@ -279,43 +482,32 @@ function split_plurals(keys, plural_groups)
|
|
|
279
482
|
|
|
280
483
|
/***************************************************************
|
|
281
484
|
* Which `<x>_<category>` keys ARE plurals: a key that only ends like one
|
|
282
|
-
* (`pick_one
|
|
283
|
-
*
|
|
485
|
+
* (`pick_one`; `step_one` / `step_two` / `step_three`, an enumeration)
|
|
486
|
+
* is a key. A cardinal group is a plural when some locale has its bare
|
|
487
|
+
* base or its `_other` form -- every language has `other`, so a real
|
|
488
|
+
* plural always carries one of the two. An ordinal says it with its
|
|
489
|
+
* `_ordinal_` infix.
|
|
284
490
|
***************************************************************/
|
|
285
491
|
function find_plural_groups(locales)
|
|
286
492
|
{
|
|
287
|
-
let forms = new Map(); // group -> Set of
|
|
493
|
+
let forms = new Map(); // group -> Set of categories, in any locale
|
|
288
494
|
let bare = new Set();
|
|
289
|
-
for(let
|
|
495
|
+
for(let translation of Object.values(locales)) {
|
|
290
496
|
for(let k of Object.keys(translation)) {
|
|
291
497
|
bare.add(k);
|
|
292
498
|
let m = k.match(PLURAL_RE);
|
|
293
499
|
if(m) {
|
|
294
500
|
let group = m[1] + (m[2]? "#ordinal" : "");
|
|
295
501
|
if(!forms.has(group)) {
|
|
296
|
-
forms.set(group, new
|
|
297
|
-
}
|
|
298
|
-
let per = forms.get(group);
|
|
299
|
-
if(!per.has(code)) {
|
|
300
|
-
per.set(code, new Set());
|
|
502
|
+
forms.set(group, new Set());
|
|
301
503
|
}
|
|
302
|
-
|
|
504
|
+
forms.get(group).add(m[3]);
|
|
303
505
|
}
|
|
304
506
|
}
|
|
305
507
|
}
|
|
306
508
|
let groups = new Set();
|
|
307
|
-
for(let [group,
|
|
308
|
-
|
|
309
|
-
let is_plural = bare.has(base) && !group.endsWith("#ordinal");
|
|
310
|
-
for(let cats of per.values()) {
|
|
311
|
-
if(cats.has("other") || cats.size >= 2) {
|
|
312
|
-
is_plural = true;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
if(group.endsWith("#ordinal")) {
|
|
316
|
-
is_plural = true; // `_ordinal_` says it
|
|
317
|
-
}
|
|
318
|
-
if(is_plural) {
|
|
509
|
+
for(let [group, cats] of forms) {
|
|
510
|
+
if(group.endsWith("#ordinal") || bare.has(group) || cats.has("other")) {
|
|
319
511
|
groups.add(group);
|
|
320
512
|
}
|
|
321
513
|
}
|
|
@@ -420,10 +612,26 @@ async function main()
|
|
|
420
612
|
}
|
|
421
613
|
}
|
|
422
614
|
|
|
615
|
+
/*
|
|
616
|
+
* A FORM asked for by its own name (`t("pick_one")`, a bare `pick`
|
|
617
|
+
* beside it) is defined when a locale carries that form.
|
|
618
|
+
*/
|
|
619
|
+
let is_defined_form = (k) => {
|
|
620
|
+
let m = k.match(PLURAL_RE);
|
|
621
|
+
if(!m) {
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
let group = m[1] + (m[2]? "#ordinal" : "");
|
|
625
|
+
if(!plural_groups.has(group)) {
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
return codes.some((code) => (split[code].plurals.get(group) || new Set()).has(m[3]));
|
|
629
|
+
};
|
|
630
|
+
|
|
423
631
|
/* Every key the source asks for exists: a missing one is a violation, not a fallback */
|
|
424
|
-
let used = collect_used_keys(opts);
|
|
632
|
+
let {keys: used} = collect_used_keys(opts);
|
|
425
633
|
for(let k of used) {
|
|
426
|
-
if(!ref_plain.has(k) && !bases.has(k)) {
|
|
634
|
+
if(!ref_plain.has(k) && !bases.has(k) && !is_defined_form(k)) {
|
|
427
635
|
fail(`key used in the source but defined in NO locale: ${JSON.stringify(k)}`);
|
|
428
636
|
}
|
|
429
637
|
}
|
|
@@ -440,334 +648,2467 @@ async function main()
|
|
|
440
648
|
`, ${used.size} used in the source)\n`);
|
|
441
649
|
}
|
|
442
650
|
|
|
443
|
-
/***************************************************************
|
|
444
|
-
* The i18n keys the source asks for
|
|
445
|
-
*
|
|
446
|
-
* A string literal is matched by its own quote: an apostrophe inside
|
|
447
|
-
* double quotes ("don't") is part of the key.
|
|
448
|
-
*
|
|
449
|
-
* The (?!\s*\+) guards skip a key BUILT BY CONCATENATION
|
|
450
|
-
* (`i18n: "kind_" + t.kind`): the literal is only a prefix, the real key
|
|
451
|
-
* cannot be known statically, and demanding "kind_" is a false positive.
|
|
452
|
-
* Such keys are not validated: keep them in sync by hand. The guard is
|
|
453
|
-
* right after the literal, INSIDE the call: `t('topic') + ':'`
|
|
454
|
-
* concatenates the result, and its key is asked for.
|
|
455
|
-
***************************************************************/
|
|
456
|
-
const STR = String.raw`(?:"([^"\n]+)"|'([^'\n]+)')`;
|
|
457
|
-
const STR_OR_TEMPLATE = String.raw`(?:"([^"\n]+)"|'([^'\n]+)'|` + "`" + String.raw`([^` + "`" + String.raw`$\n]+)` + "`" + ")";
|
|
458
|
-
const NOT_CONCAT = String.raw`(?!\s*\+)`;
|
|
459
651
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
}
|
|
652
|
+
/***************************
|
|
653
|
+
* The parsed source
|
|
654
|
+
***************************/
|
|
464
655
|
|
|
465
|
-
const PATTERNS = [
|
|
466
|
-
re(String.raw`\bt\(\s*` + STR_OR_TEMPLATE + NOT_CONCAT),
|
|
467
|
-
/* A local ALIAS of `t` is a blind spot: `yui_tabulator_i18n.js` asks
|
|
468
|
-
* for its whole chrome through `tr(key, default)`. */
|
|
469
|
-
re(String.raw`\btr\(\s*` + STR_OR_TEMPLATE + NOT_CONCAT),
|
|
470
|
-
/* The codes the schema checks hand to `t(finding.code)` /
|
|
471
|
-
* `t(conflict.code)`: an i18n key each, written where it is born. */
|
|
472
|
-
re(String.raw`\badd\(\s*['"](?:error|warning)['"]\s*,\s*` + STR),
|
|
473
|
-
re(String.raw`\bcode:\s*` + STR + NOT_CONCAT),
|
|
474
|
-
/* What refresh_language() re-translates */
|
|
475
|
-
re(String.raw`\bi18n:\s*` + STR + NOT_CONCAT),
|
|
476
|
-
re(String.raw`["']data-i18n(?:-title|-aria-label|-placeholder)?["']:\s*` + STR + NOT_CONCAT),
|
|
477
|
-
re(String.raw`\bsetAttribute\(\s*["']data-i18n(?:-title|-aria-label|-placeholder)?["']\s*,\s*` + STR + NOT_CONCAT),
|
|
478
|
-
/data-i18n(?:-title|-aria-label|-placeholder)?=["']([^"']+)["']/g
|
|
479
|
-
];
|
|
480
656
|
|
|
481
|
-
|
|
657
|
+
/***************************************************************
|
|
658
|
+
* A source file as an ESTree (acorn). Throws on a syntax error.
|
|
659
|
+
* Its comments (the i18n markers live there) and its text ride
|
|
660
|
+
* along as `ast.comments` and `ast.code`, not enumerable, so
|
|
661
|
+
* walk() never visits them.
|
|
662
|
+
***************************************************************/
|
|
663
|
+
function parse_source(code)
|
|
482
664
|
{
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
665
|
+
let comments = [];
|
|
666
|
+
let ast = acorn_parse(code, {
|
|
667
|
+
ecmaVersion: "latest",
|
|
668
|
+
sourceType: "module",
|
|
669
|
+
allowHashBang: true,
|
|
670
|
+
onComment: comments
|
|
671
|
+
});
|
|
672
|
+
Object.defineProperty(ast, "comments", {value: comments, enumerable: false});
|
|
673
|
+
Object.defineProperty(ast, "code", {value: code, enumerable: false});
|
|
674
|
+
return ast;
|
|
489
675
|
}
|
|
490
676
|
|
|
491
|
-
|
|
677
|
+
/***************************************************************
|
|
678
|
+
* Visit every node, with the chain of its ancestors (the last one
|
|
679
|
+
* is its parent). The chain is the walker's own: copy it to keep it.
|
|
680
|
+
***************************************************************/
|
|
681
|
+
function walk(node, visit, ancestors = [])
|
|
492
682
|
{
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
let
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
patterns.push(re(String.raw`\b(?:` + opts.attr_keys.join("|") + String.raw`)\s*:\s*` + STR));
|
|
499
|
-
}
|
|
500
|
-
let array_re = opts.array_keys.length > 0?
|
|
501
|
-
re(String.raw`\b(?:` + opts.array_keys.join("|") + String.raw`)\s*:\s*\[([^\]]*)\]`) : null;
|
|
502
|
-
/* A module publishes the keys it hands to t() as DATA (yui_dev.js:
|
|
503
|
-
* YUI_DEV_I18N_KEYS): every key of such an array is asked for */
|
|
504
|
-
let published_re = /\bexport\s+const\s+[A-Z0-9_]+_I18N_KEYS\s*=\s*\[([^\]]*)\]/g;
|
|
505
|
-
let declared = new Set([...opts.attr_keys, ...opts.array_keys]);
|
|
506
|
-
let declared_seen = new Set();
|
|
507
|
-
|
|
508
|
-
let scan = (url) => {
|
|
509
|
-
/* Comments are not code: the library documents the contract with
|
|
510
|
-
* examples like `data-i18n="<key>"`. */
|
|
511
|
-
let code = strip_comments(readFileSync(url, "utf8"));
|
|
512
|
-
for(let r of patterns) {
|
|
513
|
-
r.lastIndex = 0;
|
|
514
|
-
let m;
|
|
515
|
-
while((m = r.exec(code)) !== null) {
|
|
516
|
-
let k = literal_of(m);
|
|
517
|
-
/* A key INTERPOLATED in a template (`data-i18n="${key}"`)
|
|
518
|
-
* cannot be known statically either */
|
|
519
|
-
if(!k || k.includes("${")) {
|
|
520
|
-
continue;
|
|
521
|
-
}
|
|
522
|
-
keys.add(k);
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
let lists = [];
|
|
526
|
-
if(array_re) {
|
|
527
|
-
array_re.lastIndex = 0;
|
|
528
|
-
let f;
|
|
529
|
-
while((f = array_re.exec(code)) !== null) {
|
|
530
|
-
lists.push(f[1]);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
published_re.lastIndex = 0;
|
|
534
|
-
let pm;
|
|
535
|
-
while((pm = published_re.exec(code)) !== null) {
|
|
536
|
-
lists.push(pm[1]);
|
|
537
|
-
}
|
|
538
|
-
for(let list of lists) {
|
|
539
|
-
let lit_re = re(STR);
|
|
540
|
-
let l;
|
|
541
|
-
while((l = lit_re.exec(list)) !== null) {
|
|
542
|
-
keys.add(literal_of(l));
|
|
543
|
-
}
|
|
683
|
+
visit(node, ancestors);
|
|
684
|
+
ancestors.push(node);
|
|
685
|
+
for(let key of Object.keys(node)) {
|
|
686
|
+
if(key === "type" || key === "start" || key === "end" || key === "loc") {
|
|
687
|
+
continue;
|
|
544
688
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
689
|
+
let v = node[key];
|
|
690
|
+
if(Array.isArray(v)) {
|
|
691
|
+
for(let c of v) {
|
|
692
|
+
if(c && typeof c.type === "string") {
|
|
693
|
+
walk(c, visit, ancestors);
|
|
694
|
+
}
|
|
548
695
|
}
|
|
696
|
+
} else if(v && typeof v.type === "string") {
|
|
697
|
+
walk(v, visit, ancestors);
|
|
549
698
|
}
|
|
550
|
-
|
|
551
|
-
|
|
699
|
+
}
|
|
700
|
+
ancestors.pop();
|
|
701
|
+
}
|
|
552
702
|
|
|
553
|
-
|
|
703
|
+
/***************************************************************
|
|
704
|
+
* The line of a position of a parsed file
|
|
705
|
+
***************************************************************/
|
|
706
|
+
function line_of(ast, pos)
|
|
707
|
+
{
|
|
708
|
+
return ast.code.slice(0, pos).split("\n").length;
|
|
709
|
+
}
|
|
554
710
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
pending.push(...lib_imports(src, barrel, false));
|
|
564
|
-
}
|
|
711
|
+
/***************************************************************
|
|
712
|
+
* The name of a property (`a:`, `"a-b":`, `["a"]:`), null when it is
|
|
713
|
+
* computed from a value
|
|
714
|
+
***************************************************************/
|
|
715
|
+
function prop_name(p)
|
|
716
|
+
{
|
|
717
|
+
if(!p.computed && p.key.type === "Identifier") {
|
|
718
|
+
return p.key.name;
|
|
565
719
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
for(let name of declared) {
|
|
569
|
-
if(!declared_seen.has(name)) {
|
|
570
|
-
fail(`package.json "validate-locales" names ${JSON.stringify(name)}, which no source uses: a typo turns its check off`);
|
|
571
|
-
}
|
|
720
|
+
if(p.key.type === "Literal") {
|
|
721
|
+
return String(p.key.value);
|
|
572
722
|
}
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
573
725
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
if(!existsSync(LIB_SRC)) {
|
|
582
|
-
abort(`no gobj-ui source at ${LIB_SRC.pathname}`);
|
|
583
|
-
}
|
|
584
|
-
let missing = [];
|
|
585
|
-
let seen = new Set();
|
|
586
|
-
while(pending.length > 0) {
|
|
587
|
-
let name = pending.pop();
|
|
588
|
-
if(seen.has(name)) {
|
|
589
|
-
continue;
|
|
590
|
-
}
|
|
591
|
-
seen.add(name);
|
|
592
|
-
let url = new URL(name, LIB_SRC);
|
|
593
|
-
if(!existsSync(url)) {
|
|
594
|
-
missing.push(name);
|
|
595
|
-
continue;
|
|
596
|
-
}
|
|
597
|
-
let src = scan(url);
|
|
598
|
-
pending.push(...lib_imports(src, barrel, true));
|
|
726
|
+
/***************************************************************
|
|
727
|
+
* The name a call is made by: `f(...)` and `x.f(...)` are both `f`
|
|
728
|
+
***************************************************************/
|
|
729
|
+
function callee_name(callee)
|
|
730
|
+
{
|
|
731
|
+
if(callee.type === "Identifier") {
|
|
732
|
+
return callee.name;
|
|
599
733
|
}
|
|
600
|
-
if(
|
|
601
|
-
|
|
734
|
+
if(callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier") {
|
|
735
|
+
return callee.property.name;
|
|
602
736
|
}
|
|
737
|
+
return null;
|
|
738
|
+
}
|
|
603
739
|
|
|
604
|
-
|
|
740
|
+
function is_string_literal(n, re)
|
|
741
|
+
{
|
|
742
|
+
return !!n && n.type === "Literal" && typeof n.value === "string" && (!re || re.test(n.value));
|
|
605
743
|
}
|
|
606
744
|
|
|
607
745
|
/***************************************************************
|
|
608
|
-
*
|
|
609
|
-
* (*.test.js, *.spec.js, __tests__/, test/, tests/), not a locales/
|
|
610
|
-
* directory (its keys are definitions, not uses), not node_modules or a
|
|
611
|
-
* build
|
|
746
|
+
* `Object.<name>(...)`?
|
|
612
747
|
***************************************************************/
|
|
613
|
-
|
|
748
|
+
function is_object_call(n, name)
|
|
749
|
+
{
|
|
750
|
+
return n.type === "CallExpression" && n.callee.type === "MemberExpression"
|
|
751
|
+
&& !n.callee.computed && n.callee.object.type === "Identifier"
|
|
752
|
+
&& n.callee.object.name === "Object" && n.callee.property.name === name;
|
|
753
|
+
}
|
|
614
754
|
|
|
615
|
-
|
|
755
|
+
|
|
756
|
+
/***************************
|
|
757
|
+
* Scopes
|
|
758
|
+
***************************/
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
const FUNCTION_RE = /^(FunctionDeclaration|FunctionExpression|ArrowFunctionExpression)$/;
|
|
762
|
+
|
|
763
|
+
/* The array methods whose callback receives each ELEMENT first */
|
|
764
|
+
const ITERATION_METHODS = new Set([
|
|
765
|
+
"map", "flatMap", "forEach", "filter", "find", "findLast", "some", "every"
|
|
766
|
+
]);
|
|
767
|
+
|
|
768
|
+
/***************************************************************
|
|
769
|
+
* The names a binding pattern declares
|
|
770
|
+
***************************************************************/
|
|
771
|
+
function pattern_names(p, out = [])
|
|
616
772
|
{
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
773
|
+
if(!p) {
|
|
774
|
+
return out;
|
|
775
|
+
}
|
|
776
|
+
switch(p.type) {
|
|
777
|
+
case "Identifier":
|
|
778
|
+
out.push(p.name);
|
|
779
|
+
break;
|
|
780
|
+
case "AssignmentPattern":
|
|
781
|
+
pattern_names(p.left, out);
|
|
782
|
+
break;
|
|
783
|
+
case "RestElement":
|
|
784
|
+
pattern_names(p.argument, out);
|
|
785
|
+
break;
|
|
786
|
+
case "ArrayPattern":
|
|
787
|
+
for(let e of p.elements) {
|
|
788
|
+
pattern_names(e, out);
|
|
632
789
|
}
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
790
|
+
break;
|
|
791
|
+
case "ObjectPattern":
|
|
792
|
+
for(let q of p.properties) {
|
|
793
|
+
pattern_names(q.type === "RestElement"? q : q.value, out);
|
|
794
|
+
}
|
|
795
|
+
break;
|
|
796
|
+
default:
|
|
797
|
+
break;
|
|
798
|
+
}
|
|
636
799
|
return out;
|
|
637
800
|
}
|
|
638
801
|
|
|
639
802
|
/***************************************************************
|
|
640
|
-
* The
|
|
641
|
-
*
|
|
642
|
-
*
|
|
643
|
-
*
|
|
644
|
-
*
|
|
645
|
-
*
|
|
803
|
+
* The `var`s a function body declares, hoisted to it (not those
|
|
804
|
+
* of a function nested in it), and its own name for a function
|
|
805
|
+
* expression: the names it binds besides its parameters, each
|
|
806
|
+
* with the declarators of a `var` (null when it is not a plain
|
|
807
|
+
* `var <name>`). Cached: a module asks for them once per name it
|
|
808
|
+
* reads.
|
|
646
809
|
*
|
|
647
|
-
*
|
|
648
|
-
* purpose ("ES/EN" on a language button); so is the endonym of one of the
|
|
649
|
-
* app's locales (its `name`: "中文", "español") in a language menu. Any
|
|
650
|
-
* other value is a key -- "configuración" is a Spanish literal that forgot
|
|
651
|
-
* its key, and it is reported, as an ASCII one would be. `wordmark` and `alt` are
|
|
652
|
-
* deliberately out: a brand name is not translated. A key starting with
|
|
653
|
-
* `_` is a comment.
|
|
810
|
+
* -> Map(name -> [VariableDeclarator] | null)
|
|
654
811
|
***************************************************************/
|
|
655
|
-
const
|
|
812
|
+
const FN_NAMES = new WeakMap();
|
|
656
813
|
|
|
657
|
-
function
|
|
814
|
+
function function_scope_names(fn)
|
|
658
815
|
{
|
|
659
|
-
if(
|
|
660
|
-
|
|
661
|
-
return;
|
|
816
|
+
if(FN_NAMES.has(fn)) {
|
|
817
|
+
return FN_NAMES.get(fn);
|
|
662
818
|
}
|
|
663
|
-
let
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
} catch(e) {
|
|
667
|
-
abort(`${url.pathname} is not valid json: ${e.message}`);
|
|
819
|
+
let names = new Map();
|
|
820
|
+
if(fn.type === "FunctionExpression" && fn.id) {
|
|
821
|
+
names.set(fn.id.name, null);
|
|
668
822
|
}
|
|
669
|
-
let
|
|
670
|
-
if(
|
|
671
|
-
for(let item of node) {
|
|
672
|
-
walk(item);
|
|
673
|
-
}
|
|
674
|
-
return;
|
|
675
|
-
}
|
|
676
|
-
if(!node || typeof node !== "object") {
|
|
823
|
+
let visit = (n) => {
|
|
824
|
+
if(!n || typeof n.type !== "string" || FUNCTION_RE.test(n.type)) {
|
|
677
825
|
return;
|
|
678
826
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
827
|
+
if(n.type === "VariableDeclaration" && n.kind === "var") {
|
|
828
|
+
for(let d of n.declarations) {
|
|
829
|
+
for(let name of pattern_names(d.id)) {
|
|
830
|
+
let plain = (d.id.type === "Identifier");
|
|
831
|
+
let had = names.has(name)? names.get(name) : [];
|
|
832
|
+
names.set(name, (plain && had)? [...had, d] : null);
|
|
833
|
+
}
|
|
682
834
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
835
|
+
}
|
|
836
|
+
for(let key of Object.keys(n)) {
|
|
837
|
+
let v = n[key];
|
|
838
|
+
if(Array.isArray(v)) {
|
|
839
|
+
for(let c of v) {
|
|
840
|
+
visit(c);
|
|
841
|
+
}
|
|
842
|
+
} else if(v && typeof v.type === "string") {
|
|
843
|
+
visit(v);
|
|
686
844
|
}
|
|
687
|
-
walk(v);
|
|
688
845
|
}
|
|
689
846
|
};
|
|
690
|
-
|
|
847
|
+
if(fn.body && fn.body.type === "BlockStatement") {
|
|
848
|
+
for(let s of fn.body.body) {
|
|
849
|
+
visit(s);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
FN_NAMES.set(fn, names);
|
|
853
|
+
return names;
|
|
691
854
|
}
|
|
692
855
|
|
|
693
856
|
/***************************************************************
|
|
694
|
-
*
|
|
695
|
-
* or
|
|
857
|
+
* The name a function is known by: its own, or that of the variable
|
|
858
|
+
* or the property it is assigned to
|
|
696
859
|
***************************************************************/
|
|
697
|
-
function
|
|
860
|
+
function function_name(fn, parent)
|
|
698
861
|
{
|
|
699
|
-
|
|
700
|
-
.
|
|
701
|
-
|
|
862
|
+
if(fn.id) {
|
|
863
|
+
return fn.id.name;
|
|
864
|
+
}
|
|
865
|
+
if(parent && parent.type === "VariableDeclarator" && parent.id.type === "Identifier") {
|
|
866
|
+
return parent.id.name;
|
|
867
|
+
}
|
|
868
|
+
if(parent && (parent.type === "Property" || parent.type === "MethodDefinition")) {
|
|
869
|
+
return prop_name(parent);
|
|
870
|
+
}
|
|
871
|
+
return null;
|
|
702
872
|
}
|
|
703
873
|
|
|
704
874
|
/***************************************************************
|
|
705
|
-
* The
|
|
706
|
-
*
|
|
707
|
-
*
|
|
708
|
-
* to
|
|
875
|
+
* The names a node that is not a function binds for what is inside
|
|
876
|
+
* it: name -> {kind: "const", init} | {kind: "each", of} |
|
|
877
|
+
* {kind: "var", declarators} (a plain `let <name>`; a `var` belongs
|
|
878
|
+
* to its function, or here to the module) | {kind: "other"}.
|
|
879
|
+
* Cached, like function_scope_names().
|
|
709
880
|
***************************************************************/
|
|
710
|
-
|
|
881
|
+
const SCOPE_NAMES = new WeakMap();
|
|
882
|
+
|
|
883
|
+
function scope_names(a)
|
|
711
884
|
{
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
try {
|
|
715
|
-
src = strip_comments(readFileSync(LIB_INDEX, "utf8"));
|
|
716
|
-
} catch(e) {
|
|
717
|
-
abort(`cannot read the gobj-ui barrel at ${LIB_INDEX.pathname}: ${e.message}`);
|
|
885
|
+
if(SCOPE_NAMES.has(a)) {
|
|
886
|
+
return SCOPE_NAMES.get(a);
|
|
718
887
|
}
|
|
719
|
-
let
|
|
720
|
-
let
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
888
|
+
let names = new Map();
|
|
889
|
+
let declare = (name, what) => {
|
|
890
|
+
let had = names.get(name);
|
|
891
|
+
if(!had) {
|
|
892
|
+
names.set(name, what);
|
|
893
|
+
} else if(had.kind === "var" && what.kind === "var") {
|
|
894
|
+
/* `var x = 1; ... var x = 2;`: one binding, two values */
|
|
895
|
+
names.set(name, {kind: "var", decl: "var", declarators: [...had.declarators, ...what.declarators]});
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
let statements = [];
|
|
899
|
+
switch(a.type) {
|
|
900
|
+
case "Program":
|
|
901
|
+
case "BlockStatement":
|
|
902
|
+
case "StaticBlock":
|
|
903
|
+
statements = a.body;
|
|
904
|
+
break;
|
|
905
|
+
case "SwitchStatement":
|
|
906
|
+
statements = a.cases.flatMap((c) => c.consequent);
|
|
907
|
+
break;
|
|
908
|
+
case "ForStatement":
|
|
909
|
+
statements = (a.init && a.init.type === "VariableDeclaration")? [a.init] : [];
|
|
910
|
+
break;
|
|
911
|
+
case "ForOfStatement":
|
|
912
|
+
case "ForInStatement":
|
|
913
|
+
if(a.left.type === "VariableDeclaration") {
|
|
914
|
+
for(let d of a.left.declarations) {
|
|
915
|
+
for(let name of pattern_names(d.id)) {
|
|
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"});
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
break;
|
|
922
|
+
case "CatchClause":
|
|
923
|
+
for(let name of pattern_names(a.param)) {
|
|
924
|
+
declare(name, {kind: "other"});
|
|
925
|
+
}
|
|
926
|
+
break;
|
|
927
|
+
case "ClassExpression":
|
|
928
|
+
if(a.id) {
|
|
929
|
+
declare(a.id.name, {kind: "other"});
|
|
930
|
+
}
|
|
931
|
+
break;
|
|
932
|
+
default:
|
|
933
|
+
break;
|
|
934
|
+
}
|
|
935
|
+
for(let s of statements) {
|
|
936
|
+
let d = (s.type === "ExportNamedDeclaration" || s.type === "ExportDefaultDeclaration")?
|
|
937
|
+
s.declaration : s;
|
|
938
|
+
if(!d) {
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
if(d.type === "VariableDeclaration") {
|
|
942
|
+
if(d.kind === "var" && a.type !== "Program") {
|
|
943
|
+
continue; // hoisted: function_scope_names() holds it
|
|
944
|
+
}
|
|
945
|
+
for(let v of d.declarations) {
|
|
946
|
+
let plain = (v.id.type === "Identifier");
|
|
947
|
+
let what = {kind: "other"};
|
|
948
|
+
if(plain && d.kind === "const" && v.init) {
|
|
949
|
+
what = {kind: "const", init: v.init};
|
|
950
|
+
} else if(plain && d.kind !== "const") {
|
|
951
|
+
what = {kind: "var", decl: d.kind, declarators: [v]};
|
|
952
|
+
}
|
|
953
|
+
for(let name of pattern_names(v.id)) {
|
|
954
|
+
declare(name, what);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
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) {
|
|
960
|
+
declare(d.id.name, {kind: "other"});
|
|
961
|
+
} else if(d.type === "ImportDeclaration") {
|
|
962
|
+
for(let sp of d.specifiers) {
|
|
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});
|
|
727
966
|
}
|
|
728
|
-
/* `a as b` is published as `b` */
|
|
729
|
-
let parts = sym.split(/\s+as\s+/);
|
|
730
|
-
map.set(parts[parts.length - 1].trim(), mod);
|
|
731
967
|
}
|
|
732
968
|
}
|
|
733
|
-
|
|
969
|
+
SCOPE_NAMES.set(a, names);
|
|
970
|
+
return names;
|
|
734
971
|
}
|
|
735
972
|
|
|
736
973
|
/***************************************************************
|
|
737
|
-
*
|
|
738
|
-
*
|
|
739
|
-
*
|
|
740
|
-
*
|
|
741
|
-
*
|
|
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"
|
|
742
983
|
***************************************************************/
|
|
743
|
-
function
|
|
984
|
+
function param_path(p, name, steps = [], dflt = null)
|
|
744
985
|
{
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
let from_app = /(?:from\s+|import\(\s*)["']@yuneta\/gobj-ui\/src\/([\w.-]+\.js)["']/g;
|
|
749
|
-
while((m = from_app.exec(src)) !== null) {
|
|
750
|
-
out.push(m[1]);
|
|
986
|
+
if(!p) {
|
|
987
|
+
return null;
|
|
751
988
|
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
sym = sym.trim().split(/\s+as\s+/)[0].trim();
|
|
757
|
-
let mod = barrel.get(sym);
|
|
758
|
-
if(mod) {
|
|
759
|
-
out.push(mod);
|
|
989
|
+
switch(p.type) {
|
|
990
|
+
case "Identifier":
|
|
991
|
+
if(p.name !== name) {
|
|
992
|
+
return null;
|
|
760
993
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
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;
|
|
769
1015
|
}
|
|
770
|
-
return out;
|
|
771
1016
|
}
|
|
772
1017
|
|
|
773
|
-
|
|
1018
|
+
/***************************************************************
|
|
1019
|
+
* What a name used in `scope` (the chain of ancestors of the use,
|
|
1020
|
+
* outermost first) is bound to:
|
|
1021
|
+
*
|
|
1022
|
+
* {kind: "const", init, scope} a `const` with a value; `scope`
|
|
1023
|
+
* is the chain down to the block
|
|
1024
|
+
* that declares it, where its
|
|
1025
|
+
* value is read
|
|
1026
|
+
* {kind: "each", of, steps, scope}
|
|
1027
|
+
* an ELEMENT of `of`: the first
|
|
1028
|
+
* parameter of a callback of
|
|
1029
|
+
* `of.map()`, `.forEach()`, ...,
|
|
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}
|
|
1034
|
+
* parameter `index` of the
|
|
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
|
|
1041
|
+
* {kind: "var", decl, name, declarators, scope}
|
|
1042
|
+
* a plain `let` or `var` (`decl`
|
|
1043
|
+
* says which): what it
|
|
1044
|
+
* holds is never followed, except
|
|
1045
|
+
* where a marker says what it
|
|
1046
|
+
* holds (var_leaves()); `scope`
|
|
1047
|
+
* is the chain down to the node
|
|
1048
|
+
* that declares it
|
|
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
|
|
1057
|
+
* null declared nowhere: a global
|
|
1058
|
+
***************************************************************/
|
|
1059
|
+
function binding_of(name, scope)
|
|
1060
|
+
{
|
|
1061
|
+
for(let i = scope.length - 1; i >= 0; i--) {
|
|
1062
|
+
let a = scope[i];
|
|
1063
|
+
if(FUNCTION_RE.test(a.type)) {
|
|
1064
|
+
let parent = scope[i - 1];
|
|
1065
|
+
for(let j = 0; j < a.params.length; j++) {
|
|
1066
|
+
if(!pattern_names(a.params[j]).includes(name)) {
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
let at = param_path(a.params[j], name);
|
|
1070
|
+
if(!at) {
|
|
1071
|
+
return {kind: "other"};
|
|
1072
|
+
}
|
|
1073
|
+
if(j === 0 && parent && parent.type === "CallExpression" && parent.arguments[0] === a
|
|
1074
|
+
&& parent.callee.type === "MemberExpression" && !parent.callee.computed
|
|
1075
|
+
&& ITERATION_METHODS.has(parent.callee.property.name)) {
|
|
1076
|
+
return {kind: "each", of: parent.callee.object, steps: at.steps,
|
|
1077
|
+
scope: scope.slice(0, i - 1)};
|
|
1078
|
+
}
|
|
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)};
|
|
1081
|
+
}
|
|
1082
|
+
let hoisted = function_scope_names(a);
|
|
1083
|
+
if(hoisted.has(name)) {
|
|
1084
|
+
let declarators = hoisted.get(name);
|
|
1085
|
+
return declarators? {kind: "var", decl: "var", name, declarators, scope: scope.slice(0, i + 1)}
|
|
1086
|
+
: {kind: "other"};
|
|
1087
|
+
}
|
|
1088
|
+
continue;
|
|
1089
|
+
}
|
|
1090
|
+
let d = scope_names(a).get(name);
|
|
1091
|
+
if(!d) {
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
if(d.kind === "const") {
|
|
1095
|
+
return {kind: "const", init: d.init, scope: scope.slice(0, i + 1)};
|
|
1096
|
+
}
|
|
1097
|
+
if(d.kind === "each") {
|
|
1098
|
+
return {kind: "each", of: d.of, steps: d.steps || [], scope: scope.slice(0, i)};
|
|
1099
|
+
}
|
|
1100
|
+
if(d.kind === "var") {
|
|
1101
|
+
return {kind: "var", decl: d.decl, name, declarators: d.declarators, scope: scope.slice(0, i + 1)};
|
|
1102
|
+
}
|
|
1103
|
+
if(d.kind === "function") {
|
|
1104
|
+
return {kind: "function", fn: d.fn, scope: scope.slice(0, i + 1)};
|
|
1105
|
+
}
|
|
1106
|
+
return d;
|
|
1107
|
+
}
|
|
1108
|
+
return null;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
|
|
1112
|
+
/***************************
|
|
1113
|
+
* What a value can be
|
|
1114
|
+
***************************/
|
|
1115
|
+
|
|
1116
|
+
|
|
1117
|
+
/* How far a chain of consts is followed: a cycle stops here */
|
|
1118
|
+
const MAX_DEPTH = 24;
|
|
1119
|
+
|
|
1120
|
+
/***************************************************************
|
|
1121
|
+
* The values an expression can take, as far as the source writes
|
|
1122
|
+
* them out. Each is one of
|
|
1123
|
+
*
|
|
1124
|
+
* {kind: "value", node} an expression it cannot follow
|
|
1125
|
+
* further: a literal, an object
|
|
1126
|
+
* or array literal, a call
|
|
1127
|
+
* {kind: "nothing", node} `undefined`, or the property of
|
|
1128
|
+
* an object that has none
|
|
1129
|
+
* {kind: "param", node, binding, path}
|
|
1130
|
+
* a parameter of the enclosing
|
|
1131
|
+
* function, `path` what is read
|
|
1132
|
+
* from it (".title", "[].label",
|
|
1133
|
+
* a destructuring included)
|
|
1134
|
+
* {kind: "unknown", node} a `let`, a `var` (with its
|
|
1135
|
+
* `binding`), an import, a
|
|
1136
|
+
* property of something unknown
|
|
1137
|
+
*
|
|
1138
|
+
* with `scope` (the ancestors of `node`, to read it further) and
|
|
1139
|
+
* `via`, the nodes of the place it is READ that it went through,
|
|
1140
|
+
* outermost first: where a marker may stand (markers_of()). An
|
|
1141
|
+
* unknown read from a plain `let` / `var` -- the variable itself, a
|
|
1142
|
+
* property of it, an element of it -- carries `var_root`: {binding,
|
|
1143
|
+
* steps}, the variable and the steps from it to what is read
|
|
1144
|
+
* (extend_root()), so a marker standing there can have it followed
|
|
1145
|
+
* (var_leaves()). What it follows:
|
|
1146
|
+
*
|
|
1147
|
+
* - every branch of `?:`, of `||` and `??`, the right one of `&&`
|
|
1148
|
+
* (the left one is a condition: when it is the value, it is
|
|
1149
|
+
* falsy, not a key);
|
|
1150
|
+
* - a `const`, to its value, read in the scope that declares it;
|
|
1151
|
+
* - a property of an object literal it reached (`DEFAULTS.ok`; for
|
|
1152
|
+
* a computed one, `NOTES[verdict]`, every value it holds), and an
|
|
1153
|
+
* element of an array literal (`LIST[i]`);
|
|
1154
|
+
* - an ELEMENT of an array literal it reached: the parameter of a
|
|
1155
|
+
* `.map()`, `.forEach()`, ... callback over it, the variable of a
|
|
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.
|
|
1164
|
+
*
|
|
1165
|
+
* ctx: {scope, via, off}. `off` is set once it left the place it is
|
|
1166
|
+
* read for the declaration of a const: `via` stops growing there.
|
|
1167
|
+
***************************************************************/
|
|
1168
|
+
function expand(node, ctx, depth = 0)
|
|
1169
|
+
{
|
|
1170
|
+
if(!node) {
|
|
1171
|
+
return [];
|
|
1172
|
+
}
|
|
1173
|
+
let via = ctx.off? ctx.via : [...ctx.via, node];
|
|
1174
|
+
let here = {scope: ctx.scope, via: via, off: ctx.off};
|
|
1175
|
+
let item = (kind, extra) => Object.assign({kind, node, scope: ctx.scope, via, off: ctx.off}, extra);
|
|
1176
|
+
if(depth > MAX_DEPTH) {
|
|
1177
|
+
return [item("unknown")];
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
switch(node.type) {
|
|
1181
|
+
case "ConditionalExpression":
|
|
1182
|
+
return [...expand(node.consequent, here, depth + 1),
|
|
1183
|
+
...expand(node.alternate, here, depth + 1)];
|
|
1184
|
+
|
|
1185
|
+
case "LogicalExpression":
|
|
1186
|
+
return [...(node.operator !== "&&"? expand(node.left, here, depth + 1) : []),
|
|
1187
|
+
...expand(node.right, here, depth + 1)];
|
|
1188
|
+
|
|
1189
|
+
case "Identifier": {
|
|
1190
|
+
if(node.name === "undefined") {
|
|
1191
|
+
return [item("nothing")];
|
|
1192
|
+
}
|
|
1193
|
+
let b = binding_of(node.name, ctx.scope);
|
|
1194
|
+
if(!b) {
|
|
1195
|
+
return [item("unknown")];
|
|
1196
|
+
}
|
|
1197
|
+
if(b.kind === "const") {
|
|
1198
|
+
return expand(b.init, {scope: b.scope, via: via, off: true}, depth + 1);
|
|
1199
|
+
}
|
|
1200
|
+
if(b.kind === "each") {
|
|
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;
|
|
1206
|
+
}
|
|
1207
|
+
if(b.kind === "param") {
|
|
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;
|
|
1213
|
+
}
|
|
1214
|
+
if(b.kind === "var") {
|
|
1215
|
+
return [item("unknown", {binding: b, var_root: {binding: b, steps: []}})];
|
|
1216
|
+
}
|
|
1217
|
+
return [item("unknown")];
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
case "MemberExpression": {
|
|
1221
|
+
let step = member_step(node);
|
|
1222
|
+
let out = [];
|
|
1223
|
+
for(let o of expand(node.object, here, depth + 1)) {
|
|
1224
|
+
out.push(...step_values(o, step, node, depth + 1));
|
|
1225
|
+
}
|
|
1226
|
+
return out;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
case "CallExpression":
|
|
1230
|
+
if(is_object_call(node, "freeze") && node.arguments.length === 1) {
|
|
1231
|
+
return expand(node.arguments[0], here, depth + 1);
|
|
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
|
+
}
|
|
1244
|
+
return [item("value")];
|
|
1245
|
+
|
|
1246
|
+
default:
|
|
1247
|
+
return [item("value")];
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
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
|
+
|
|
1289
|
+
/***************************************************************
|
|
1290
|
+
* The step a member expression takes from its object:
|
|
1291
|
+
*
|
|
1292
|
+
* {prop: name, computed} a property: `x.label`, `x["a"]`,
|
|
1293
|
+
* `x[0]` (`computed` for brackets:
|
|
1294
|
+
* on an array, an index reads an
|
|
1295
|
+
* element)
|
|
1296
|
+
* {prop: null, computed} any property: `x[k]`
|
|
1297
|
+
* {each: true} an element: what a `.map()` /
|
|
1298
|
+
* `.forEach()` callback, a for...of
|
|
1299
|
+
* or a `[]` key position reads
|
|
1300
|
+
***************************************************************/
|
|
1301
|
+
function member_step(node)
|
|
1302
|
+
{
|
|
1303
|
+
let name = null;
|
|
1304
|
+
if(!node.computed && node.property.type === "Identifier") {
|
|
1305
|
+
name = node.property.name;
|
|
1306
|
+
} else if(node.computed && node.property.type === "Literal") {
|
|
1307
|
+
name = String(node.property.value);
|
|
1308
|
+
}
|
|
1309
|
+
return {prop: name, computed: !!node.computed};
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/***************************************************************
|
|
1313
|
+
* The var_root of what is read one step further from something read
|
|
1314
|
+
* from a `let` / `var` (null when it was not)
|
|
1315
|
+
***************************************************************/
|
|
1316
|
+
function extend_root(root, step)
|
|
1317
|
+
{
|
|
1318
|
+
return root? {binding: root.binding, steps: [...root.steps, step]} : null;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
/***************************************************************
|
|
1322
|
+
* The values one step (member_step()) reads from a value of expand():
|
|
1323
|
+
* a property, or an element. `node` is the node that reads it (null:
|
|
1324
|
+
* the value's own)
|
|
1325
|
+
***************************************************************/
|
|
1326
|
+
function step_values(o, step, node, depth)
|
|
1327
|
+
{
|
|
1328
|
+
let at = node || o.node;
|
|
1329
|
+
if(step.each) {
|
|
1330
|
+
return element_values(o, depth);
|
|
1331
|
+
}
|
|
1332
|
+
if(o.kind === "param") {
|
|
1333
|
+
return [step.prop !== null? Object.assign({}, o, {path: o.path + "." + step.prop, node: at}) :
|
|
1334
|
+
Object.assign({}, o, {kind: "unknown", binding: null, var_root: null, node: at})];
|
|
1335
|
+
}
|
|
1336
|
+
if(o.kind === "value" && o.node.type === "ObjectExpression") {
|
|
1337
|
+
return property_values(o, step.prop, depth + 1);
|
|
1338
|
+
}
|
|
1339
|
+
if(o.kind === "value" && o.node.type === "ArrayExpression" && step.computed) {
|
|
1340
|
+
return element_values(o, depth + 1);
|
|
1341
|
+
}
|
|
1342
|
+
if(o.kind === "nothing" || (o.kind === "value" && o.node.type === "Literal")) {
|
|
1343
|
+
return [Object.assign({}, o, {kind: "nothing", node: at})];
|
|
1344
|
+
}
|
|
1345
|
+
return [Object.assign({}, o, {kind: "unknown", binding: null, node: at,
|
|
1346
|
+
var_root: extend_root(o.var_root, step)})];
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
/***************************************************************
|
|
1350
|
+
* The values of property `name` of an object literal (`name` null:
|
|
1351
|
+
* of every property), each followed with expand()
|
|
1352
|
+
***************************************************************/
|
|
1353
|
+
function property_values(o, name, depth)
|
|
1354
|
+
{
|
|
1355
|
+
let out = [];
|
|
1356
|
+
let scope = [...o.scope, o.node];
|
|
1357
|
+
let found = false;
|
|
1358
|
+
for(let p of o.node.properties) {
|
|
1359
|
+
if(p.type === "SpreadElement") {
|
|
1360
|
+
for(let s of expand(p.argument, {scope, via: o.via, off: true}, depth + 1)) {
|
|
1361
|
+
if(s.kind === "value" && s.node.type === "ObjectExpression") {
|
|
1362
|
+
let r = property_values(s, name, depth + 1).filter((x) => x.kind !== "nothing");
|
|
1363
|
+
found = found || r.length > 0;
|
|
1364
|
+
out.push(...r);
|
|
1365
|
+
} else if(s.kind !== "nothing") {
|
|
1366
|
+
found = true;
|
|
1367
|
+
out.push(Object.assign({}, s, {kind: "unknown", binding: null,
|
|
1368
|
+
var_root: extend_root(s.var_root, {prop: name, computed: false})}));
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
continue;
|
|
1372
|
+
}
|
|
1373
|
+
let pn = prop_name(p);
|
|
1374
|
+
if(pn === null) {
|
|
1375
|
+
found = true;
|
|
1376
|
+
out.push({kind: "unknown", node: p, scope, via: o.via, off: o.off});
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
if(name === null || pn === name) {
|
|
1380
|
+
found = true;
|
|
1381
|
+
let v = (p.kind === "init" && !p.method)? p.value : null;
|
|
1382
|
+
if(v) {
|
|
1383
|
+
out.push(...expand(v, {scope: [...scope, p], via: o.via, off: o.off}, depth + 1));
|
|
1384
|
+
} else {
|
|
1385
|
+
out.push({kind: "unknown", node: p, scope, via: o.via, off: o.off});
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
if(!found) {
|
|
1390
|
+
out.push({kind: "nothing", node: o.node, scope: o.scope, via: o.via, off: o.off});
|
|
1391
|
+
}
|
|
1392
|
+
return out;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
/***************************************************************
|
|
1396
|
+
* The elements of whatever `node` is, when it is an array literal
|
|
1397
|
+
* (after expand()): each element followed with expand(). A `.map()`
|
|
1398
|
+
* of one is the values its callback returns, each element bound to
|
|
1399
|
+
* the callback's parameter. Anything else is unknown, or a param.
|
|
1400
|
+
***************************************************************/
|
|
1401
|
+
function elements_of(node, ctx, depth)
|
|
1402
|
+
{
|
|
1403
|
+
let out = [];
|
|
1404
|
+
for(let v of expand(node, ctx, depth + 1)) {
|
|
1405
|
+
out.push(...element_values(v, depth));
|
|
1406
|
+
}
|
|
1407
|
+
return out;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
/***************************************************************
|
|
1411
|
+
* The elements of one value of expand(): see elements_of()
|
|
1412
|
+
***************************************************************/
|
|
1413
|
+
function element_values(v, depth)
|
|
1414
|
+
{
|
|
1415
|
+
let out = [];
|
|
1416
|
+
if(v.kind === "param") {
|
|
1417
|
+
out.push(Object.assign({}, v, {path: v.path + "[]"}));
|
|
1418
|
+
} else if(v.kind === "nothing" || (v.kind === "value" && v.node.type === "Literal")) {
|
|
1419
|
+
return out;
|
|
1420
|
+
} else if(v.kind === "value" && v.node.type === "ArrayExpression") {
|
|
1421
|
+
let scope = [...v.scope, v.node];
|
|
1422
|
+
for(let el of v.node.elements) {
|
|
1423
|
+
if(!el) {
|
|
1424
|
+
continue;
|
|
1425
|
+
}
|
|
1426
|
+
if(el.type === "SpreadElement") {
|
|
1427
|
+
out.push(...elements_of(el.argument, {scope, via: v.via, off: v.off}, depth + 1));
|
|
1428
|
+
continue;
|
|
1429
|
+
}
|
|
1430
|
+
out.push(...expand(el, {scope, via: v.via, off: v.off}, depth + 1));
|
|
1431
|
+
}
|
|
1432
|
+
} else if(v.kind === "value" && is_mapping(v.node)) {
|
|
1433
|
+
let cb = v.node.arguments[0];
|
|
1434
|
+
let scope = [...v.scope, v.node, cb];
|
|
1435
|
+
for(let r of returned_values(cb)) {
|
|
1436
|
+
out.push(...expand(r.node, {scope: [...scope, ...r.path], via: v.via, off: v.off}, depth + 1));
|
|
1437
|
+
}
|
|
1438
|
+
} else {
|
|
1439
|
+
out.push(Object.assign({}, v, {kind: "unknown", binding: null,
|
|
1440
|
+
var_root: extend_root(v.var_root, {each: true})}));
|
|
1441
|
+
}
|
|
1442
|
+
return out;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
/***************************************************************
|
|
1446
|
+
* `<array>.map(callback)` (or flatMap), the callback a function
|
|
1447
|
+
* literal with a plain first parameter?
|
|
1448
|
+
***************************************************************/
|
|
1449
|
+
function is_mapping(n)
|
|
1450
|
+
{
|
|
1451
|
+
if(n.type !== "CallExpression" || n.callee.type !== "MemberExpression" || n.callee.computed
|
|
1452
|
+
|| !/^(map|flatMap)$/.test(n.callee.property.name)) {
|
|
1453
|
+
return false;
|
|
1454
|
+
}
|
|
1455
|
+
let cb = n.arguments[0];
|
|
1456
|
+
return !!cb && FUNCTION_RE.test(cb.type) && cb.params.length > 0
|
|
1457
|
+
&& cb.params[0].type === "Identifier";
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
/***************************************************************
|
|
1461
|
+
* What a function returns: its expression body, or the argument of
|
|
1462
|
+
* every `return` of its block (not of a function nested in it), each
|
|
1463
|
+
* with the nodes between the function and it
|
|
1464
|
+
***************************************************************/
|
|
1465
|
+
function returned_values(fn)
|
|
1466
|
+
{
|
|
1467
|
+
if(fn.body.type !== "BlockStatement") {
|
|
1468
|
+
return [{node: fn.body, path: []}];
|
|
1469
|
+
}
|
|
1470
|
+
let out = [];
|
|
1471
|
+
let visit = (n, path) => {
|
|
1472
|
+
if(!n || typeof n.type !== "string" || FUNCTION_RE.test(n.type)) {
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
if(n.type === "ReturnStatement") {
|
|
1476
|
+
if(n.argument) {
|
|
1477
|
+
out.push({node: n.argument, path: [...path, n]});
|
|
1478
|
+
}
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
for(let key of Object.keys(n)) {
|
|
1482
|
+
let v = n[key];
|
|
1483
|
+
if(Array.isArray(v)) {
|
|
1484
|
+
for(let c of v) {
|
|
1485
|
+
visit(c, [...path, n]);
|
|
1486
|
+
}
|
|
1487
|
+
} else if(v && typeof v.type === "string") {
|
|
1488
|
+
visit(v, [...path, n]);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
};
|
|
1492
|
+
visit(fn.body, []);
|
|
1493
|
+
return out;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
/***************************************************************
|
|
1497
|
+
* The leaves of what an expression can be, each one of
|
|
1498
|
+
*
|
|
1499
|
+
* {kind: "key", value} a non-empty string: a literal, a
|
|
1500
|
+
* template with no `${}`
|
|
1501
|
+
* {kind: "nothing"} null, undefined, "", a number, a
|
|
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
|
|
1506
|
+
* {kind: "param", ...} a parameter (of an enclosing function),
|
|
1507
|
+
* see expand()
|
|
1508
|
+
* {kind: "unknown"} anything else: a variable it cannot
|
|
1509
|
+
* follow, a call, a concatenation, an
|
|
1510
|
+
* object
|
|
1511
|
+
*
|
|
1512
|
+
* each with its `node` and `via`. `scope` is the ancestors of `node`
|
|
1513
|
+
* (outermost first); `root` the node of the place it is read (a call,
|
|
1514
|
+
* a property), where a marker of the whole place stands.
|
|
1515
|
+
***************************************************************/
|
|
1516
|
+
function leaves_of(node, scope = [], root = null)
|
|
1517
|
+
{
|
|
1518
|
+
let out = [];
|
|
1519
|
+
let ctx = {scope, via: root? [root] : [], off: false};
|
|
1520
|
+
for(let v of expand(node, ctx)) {
|
|
1521
|
+
out.push(classify(v));
|
|
1522
|
+
}
|
|
1523
|
+
return out;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
function classify(v)
|
|
1527
|
+
{
|
|
1528
|
+
if(v.kind !== "value") {
|
|
1529
|
+
return v;
|
|
1530
|
+
}
|
|
1531
|
+
let n = v.node;
|
|
1532
|
+
if(n.type === "Literal") {
|
|
1533
|
+
let kind = (typeof n.value === "string" && n.value)? "key" : "nothing";
|
|
1534
|
+
return Object.assign({}, v, {kind, value: n.value});
|
|
1535
|
+
}
|
|
1536
|
+
if(n.type === "TemplateLiteral" && n.expressions.length === 0) {
|
|
1537
|
+
let s = n.quasis[0].value.cooked;
|
|
1538
|
+
return Object.assign({}, v, {kind: s? "key" : "nothing", value: s});
|
|
1539
|
+
}
|
|
1540
|
+
if(is_dom(n, v.scope)) {
|
|
1541
|
+
return Object.assign({}, v, {kind: "dom"});
|
|
1542
|
+
}
|
|
1543
|
+
return Object.assign({}, v, {kind: "unknown"});
|
|
1544
|
+
}
|
|
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
|
+
|
|
1590
|
+
/***************************************************************
|
|
1591
|
+
* The strings an expression can be, as far as they are WRITTEN:
|
|
1592
|
+
* the keys of leaves_of(). `complete` says every value it can take
|
|
1593
|
+
* is known (keys, nothing, DOM). `scope` is the chain of ancestors of
|
|
1594
|
+
* `node`: without it, no name is followed.
|
|
1595
|
+
***************************************************************/
|
|
1596
|
+
function static_keys(node, scope = [])
|
|
1597
|
+
{
|
|
1598
|
+
let keys = [];
|
|
1599
|
+
let complete = true;
|
|
1600
|
+
for(let leaf of leaves_of(node, scope)) {
|
|
1601
|
+
if(leaf.kind === "key") {
|
|
1602
|
+
keys.push(leaf.value);
|
|
1603
|
+
} else if(!KNOWN.has(leaf.kind)) {
|
|
1604
|
+
complete = false;
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
return {keys, complete};
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
/***************************************************************
|
|
1611
|
+
* The leaves at a key position of I18N_KEY_FUNCTIONS in one call:
|
|
1612
|
+
* "1", "2.title", "1.actions[].label", "2[].label". `scope` is the
|
|
1613
|
+
* chain of ancestors of the call. Each leaf is one of leaves_of(),
|
|
1614
|
+
* its `via` starting at the call.
|
|
1615
|
+
*
|
|
1616
|
+
* A position the call does not reach (an argument not given) has no
|
|
1617
|
+
* leaf: its default applies. An options object held in a variable,
|
|
1618
|
+
* a spread, a parameter: followed like any value.
|
|
1619
|
+
***************************************************************/
|
|
1620
|
+
function values_at(call, path, scope = [])
|
|
1621
|
+
{
|
|
1622
|
+
let segs = String(path).split(".");
|
|
1623
|
+
let first = segs.shift().match(/^(\d+)(\[\])?$/);
|
|
1624
|
+
let at_call = {kind: "unknown", node: call, scope, via: [call], off: false};
|
|
1625
|
+
if(!first) {
|
|
1626
|
+
return [at_call];
|
|
1627
|
+
}
|
|
1628
|
+
let idx = Number(first[1]);
|
|
1629
|
+
let call_scope = [...scope, call];
|
|
1630
|
+
for(let i = 0; i <= idx && i < call.arguments.length; i++) {
|
|
1631
|
+
if(call.arguments[i].type === "SpreadElement") {
|
|
1632
|
+
return [Object.assign({}, at_call, {node: call.arguments[i]})];
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
let arg = call.arguments[idx];
|
|
1636
|
+
if(!arg) {
|
|
1637
|
+
return [];
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
let ctx = {scope: call_scope, via: [call], off: false};
|
|
1641
|
+
let cur = first[2]? elements_of(arg, ctx, 0) : expand(arg, ctx);
|
|
1642
|
+
for(let seg of segs) {
|
|
1643
|
+
let m = seg.match(/^([\w$]+)(\[\])?$/);
|
|
1644
|
+
if(!m) {
|
|
1645
|
+
return [at_call];
|
|
1646
|
+
}
|
|
1647
|
+
let next = [];
|
|
1648
|
+
for(let v of cur) {
|
|
1649
|
+
if(v.kind === "param") {
|
|
1650
|
+
next.push(Object.assign({}, v, {path: v.path + "." + m[1]}));
|
|
1651
|
+
} else if(v.kind === "value" && v.node.type === "ObjectExpression") {
|
|
1652
|
+
next.push(...property_values(v, m[1], 0));
|
|
1653
|
+
} else if(v.kind === "value" && is_object_call(v.node, "assign")) {
|
|
1654
|
+
let assigned = v.node.arguments.flatMap((a) =>
|
|
1655
|
+
expand(a, {scope: [...v.scope, v.node], via: v.via, off: v.off}));
|
|
1656
|
+
for(let a of assigned) {
|
|
1657
|
+
if(a.kind === "value" && a.node.type === "ObjectExpression") {
|
|
1658
|
+
next.push(...property_values(a, m[1], 0)
|
|
1659
|
+
.filter((x) => x.kind !== "nothing"));
|
|
1660
|
+
} else if(a.kind === "param") {
|
|
1661
|
+
next.push(Object.assign({}, a, {path: a.path + "." + m[1]}));
|
|
1662
|
+
} else if(a.kind !== "nothing") {
|
|
1663
|
+
next.push(Object.assign({}, a, {kind: "unknown", binding: null,
|
|
1664
|
+
var_root: extend_root(a.var_root, {prop: m[1], computed: false})}));
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
} else if(v.kind === "nothing" || (v.kind === "value" && v.node.type === "Literal")) {
|
|
1668
|
+
continue;
|
|
1669
|
+
} else {
|
|
1670
|
+
next.push(Object.assign({}, v, {kind: "unknown", binding: null,
|
|
1671
|
+
var_root: extend_root(v.var_root, {prop: m[1], computed: false})}));
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
if(m[2]) {
|
|
1675
|
+
let els = [];
|
|
1676
|
+
for(let v of next) {
|
|
1677
|
+
if(v.kind === "param") {
|
|
1678
|
+
els.push(Object.assign({}, v, {path: v.path + "[]"}));
|
|
1679
|
+
} else if(v.kind === "value") {
|
|
1680
|
+
els.push(...elements_of(v.node, v, 0));
|
|
1681
|
+
} else if(v.kind !== "nothing") {
|
|
1682
|
+
els.push(Object.assign({}, v, {var_root: extend_root(v.var_root, {each: true})}));
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
next = els;
|
|
1686
|
+
}
|
|
1687
|
+
cur = next;
|
|
1688
|
+
}
|
|
1689
|
+
return cur.map(classify);
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
/***************************************************************
|
|
1693
|
+
* The steps from a `let` / `var` to the member a node names, when
|
|
1694
|
+
* the node is the variable itself (no step) or a member of it
|
|
1695
|
+
* (`x.a`, `x[k]`, `x.a.b`): [step] (member_step()), or null when it
|
|
1696
|
+
* is not read from the variable. `is_it(id)` says whether an
|
|
1697
|
+
* identifier is the variable.
|
|
1698
|
+
***************************************************************/
|
|
1699
|
+
function steps_to(node, is_it)
|
|
1700
|
+
{
|
|
1701
|
+
let steps = [];
|
|
1702
|
+
let n = node;
|
|
1703
|
+
while(n.type === "MemberExpression") {
|
|
1704
|
+
steps.unshift(member_step(n));
|
|
1705
|
+
n = n.object;
|
|
1706
|
+
}
|
|
1707
|
+
return is_it(n)? steps : null;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
/***************************************************************
|
|
1711
|
+
* Can two steps (member_step()) reach the same value? An element
|
|
1712
|
+
* meets an index (`x[0]`) and any property (`x[k]`); a property
|
|
1713
|
+
* meets the same name, and any property.
|
|
1714
|
+
***************************************************************/
|
|
1715
|
+
function steps_meet(a, b)
|
|
1716
|
+
{
|
|
1717
|
+
if(a.each || b.each) {
|
|
1718
|
+
let other = a.each? b : a;
|
|
1719
|
+
return !!other.each || other.prop === null || (other.computed && /^\d+$/.test(other.prop));
|
|
1720
|
+
}
|
|
1721
|
+
return a.prop === null || b.prop === null || a.prop === b.prop;
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
/***************************************************************
|
|
1725
|
+
* Do two paths of steps meet: is the shorter one a prefix of the
|
|
1726
|
+
* longer one, step by step?
|
|
1727
|
+
***************************************************************/
|
|
1728
|
+
function paths_meet(a, b)
|
|
1729
|
+
{
|
|
1730
|
+
let n = Math.min(a.length, b.length);
|
|
1731
|
+
for(let i = 0; i < n; i++) {
|
|
1732
|
+
if(!steps_meet(a[i], b[i])) {
|
|
1733
|
+
return false;
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
return true;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
/* The array methods that write ELEMENTS into an array: the index of
|
|
1740
|
+
* the first argument that is one (fill: only that one) */
|
|
1741
|
+
const ELEMENT_WRITERS = new Map([["push", 0], ["unshift", 0], ["splice", 2], ["fill", 0]]);
|
|
1742
|
+
|
|
1743
|
+
/***************************************************************
|
|
1744
|
+
* Everything a plain `let` / `var` is GIVEN, in the scope that
|
|
1745
|
+
* declares it: the value of each declarator (one with none holds
|
|
1746
|
+
* nothing), and the right side of every `=` to it (`||=`, `??=`,
|
|
1747
|
+
* `&&=` too: the old value stays possible besides). A write whose
|
|
1748
|
+
* value cannot be read -- `+=`, `++`, a destructuring, a for...in/of
|
|
1749
|
+
* over it -- is listed in `unknown`. A name of another binding
|
|
1750
|
+
* (shadowing) is not a write to it.
|
|
1751
|
+
*
|
|
1752
|
+
* With `members` (it holds a table, and what is read is a member of
|
|
1753
|
+
* it), also what is written INTO it: `x.a = v`, `x[k] = v`,
|
|
1754
|
+
* `x.a.b = v` (`steps`: from the variable to the member written),
|
|
1755
|
+
* the elements `x.push(v)`, `x.unshift(v)`, `x.splice(i, n, v)`,
|
|
1756
|
+
* `x.fill(v)` give it (a spread: every element of what is spread),
|
|
1757
|
+
* and `Object.assign(x, v)` (v written over x). A `+=` or `++` on a
|
|
1758
|
+
* member is unknown at that member.
|
|
1759
|
+
*
|
|
1760
|
+
* -> {values: [{node, scope, steps, spread, into}], unknown: [{node, steps}]}
|
|
1761
|
+
* `node` null: nothing (a declarator with no value); `into`:
|
|
1762
|
+
* written into the variable, not given to it
|
|
1763
|
+
***************************************************************/
|
|
1764
|
+
function assigned_values(binding, members = false)
|
|
1765
|
+
{
|
|
1766
|
+
let values = [];
|
|
1767
|
+
let unknown = [];
|
|
1768
|
+
let first = binding.declarators[0];
|
|
1769
|
+
let root = binding.scope[binding.scope.length - 1];
|
|
1770
|
+
let is_it = (id, chain) => {
|
|
1771
|
+
if(!id || id.type !== "Identifier" || id.name !== binding.name) {
|
|
1772
|
+
return false;
|
|
1773
|
+
}
|
|
1774
|
+
let b = binding_of(id.name, chain);
|
|
1775
|
+
return !!b && b.kind === "var" && b.declarators[0] === first;
|
|
1776
|
+
};
|
|
1777
|
+
walk(root, (n, ancestors) => {
|
|
1778
|
+
let chain = [...ancestors, n];
|
|
1779
|
+
let into = (node) => (members? steps_to(node, (id) => is_it(id, chain)) : null);
|
|
1780
|
+
switch(n.type) {
|
|
1781
|
+
case "VariableDeclarator":
|
|
1782
|
+
if(binding.declarators.includes(n)) {
|
|
1783
|
+
values.push(n.init? {node: n.init, scope: chain, steps: []} : {node: null, steps: []});
|
|
1784
|
+
}
|
|
1785
|
+
break;
|
|
1786
|
+
case "AssignmentExpression": {
|
|
1787
|
+
let readable = /^(=|\|\|=|\?\?=|&&=)$/.test(n.operator);
|
|
1788
|
+
if(n.left.type === "Identifier") {
|
|
1789
|
+
if(is_it(n.left, chain)) {
|
|
1790
|
+
if(readable) {
|
|
1791
|
+
values.push({node: n.right, scope: chain, steps: []});
|
|
1792
|
+
} else {
|
|
1793
|
+
unknown.push({node: n, steps: []});
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
} else if(n.left.type === "MemberExpression") {
|
|
1797
|
+
let steps = into(n.left);
|
|
1798
|
+
if(steps && readable) {
|
|
1799
|
+
values.push({node: n.right, scope: chain, steps, into: true});
|
|
1800
|
+
} else if(steps) {
|
|
1801
|
+
unknown.push({node: n, steps});
|
|
1802
|
+
}
|
|
1803
|
+
} else if(pattern_names(n.left).includes(binding.name)
|
|
1804
|
+
&& is_it({type: "Identifier", name: binding.name}, chain)) {
|
|
1805
|
+
unknown.push({node: n, steps: []});
|
|
1806
|
+
}
|
|
1807
|
+
break;
|
|
1808
|
+
}
|
|
1809
|
+
case "UpdateExpression":
|
|
1810
|
+
if(is_it(n.argument, chain)) {
|
|
1811
|
+
unknown.push({node: n, steps: []});
|
|
1812
|
+
} else if(n.argument.type === "MemberExpression") {
|
|
1813
|
+
let steps = into(n.argument);
|
|
1814
|
+
if(steps) {
|
|
1815
|
+
unknown.push({node: n, steps});
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
break;
|
|
1819
|
+
case "CallExpression": {
|
|
1820
|
+
if(!members) {
|
|
1821
|
+
break;
|
|
1822
|
+
}
|
|
1823
|
+
let c = n.callee;
|
|
1824
|
+
if(c.type === "MemberExpression" && !c.computed && c.property.type === "Identifier"
|
|
1825
|
+
&& ELEMENT_WRITERS.has(c.property.name)) {
|
|
1826
|
+
let steps = into(c.object);
|
|
1827
|
+
if(!steps) {
|
|
1828
|
+
break;
|
|
1829
|
+
}
|
|
1830
|
+
let from = ELEMENT_WRITERS.get(c.property.name);
|
|
1831
|
+
let to = (c.property.name === "fill")? from + 1 : n.arguments.length;
|
|
1832
|
+
for(let a of n.arguments.slice(from, to)) {
|
|
1833
|
+
let spread = (a.type === "SpreadElement");
|
|
1834
|
+
values.push({node: spread? a.argument : a, scope: chain,
|
|
1835
|
+
steps: [...steps, {each: true}], spread, into: true});
|
|
1836
|
+
}
|
|
1837
|
+
} else if(is_object_call(n, "assign") && n.arguments.length > 0) {
|
|
1838
|
+
let steps = into(n.arguments[0]);
|
|
1839
|
+
if(!steps) {
|
|
1840
|
+
break;
|
|
1841
|
+
}
|
|
1842
|
+
for(let a of n.arguments.slice(1)) {
|
|
1843
|
+
if(a.type === "SpreadElement") {
|
|
1844
|
+
unknown.push({node: a, steps});
|
|
1845
|
+
} else {
|
|
1846
|
+
values.push({node: a, scope: chain, steps, into: true});
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
break;
|
|
1851
|
+
}
|
|
1852
|
+
case "ForOfStatement":
|
|
1853
|
+
case "ForInStatement":
|
|
1854
|
+
if(is_it(n.left, chain)) {
|
|
1855
|
+
unknown.push({node: n, steps: []});
|
|
1856
|
+
}
|
|
1857
|
+
break;
|
|
1858
|
+
default:
|
|
1859
|
+
break;
|
|
1860
|
+
}
|
|
1861
|
+
}, binding.scope.slice(0, -1));
|
|
1862
|
+
return {values, unknown};
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
/***************************************************************
|
|
1866
|
+
* The leaves of what is read from a `let` / `var` -- the variable
|
|
1867
|
+
* itself, or a member or an element of it (`root`, a var_root of
|
|
1868
|
+
* expand()): every value it is given (assigned_values()), followed
|
|
1869
|
+
* with expand() and then along the steps of `root`, a `let` met on
|
|
1870
|
+
* the way followed into it.
|
|
1871
|
+
*
|
|
1872
|
+
* -> {leaves: [leaf], complete, written, unread: [node], lets: [binding]}
|
|
1873
|
+
*
|
|
1874
|
+
* `complete`: every leaf is known (a literal, a const that is one,
|
|
1875
|
+
* nothing, DOM) and every write was read. `written`: some leaf was
|
|
1876
|
+
* written INTO the variable (`x.a = "k"`, `x.push("k")`), which a
|
|
1877
|
+
* const would not carry to the reader. `unread`: the node of each
|
|
1878
|
+
* value it could not read. `lets`: the variables it went through,
|
|
1879
|
+
* the first one first.
|
|
1880
|
+
***************************************************************/
|
|
1881
|
+
function var_leaves(root, seen = new Set(), depth = 0)
|
|
1882
|
+
{
|
|
1883
|
+
let b = root.binding;
|
|
1884
|
+
let steps = root.steps;
|
|
1885
|
+
let out = {leaves: [], complete: true, written: false, unread: [], lets: [b]};
|
|
1886
|
+
let id = `${b.declarators[0].start}:${JSON.stringify(steps)}`;
|
|
1887
|
+
if(seen.has(id)) {
|
|
1888
|
+
return out;
|
|
1889
|
+
}
|
|
1890
|
+
seen.add(id);
|
|
1891
|
+
if(depth > MAX_DEPTH) {
|
|
1892
|
+
out.complete = false;
|
|
1893
|
+
out.unread.push(b.declarators[0]);
|
|
1894
|
+
return out;
|
|
1895
|
+
}
|
|
1896
|
+
let {values, unknown} = assigned_values(b, steps.length > 0);
|
|
1897
|
+
for(let u of unknown) {
|
|
1898
|
+
if(paths_meet(u.steps, steps)) {
|
|
1899
|
+
out.complete = false;
|
|
1900
|
+
out.unread.push(u.node);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
for(let v of values) {
|
|
1904
|
+
if(!v.node || !paths_meet(v.steps, steps)) {
|
|
1905
|
+
continue;
|
|
1906
|
+
}
|
|
1907
|
+
if(v.steps.length > steps.length) {
|
|
1908
|
+
/* Written below what is read: what is read holds it, a table */
|
|
1909
|
+
out.complete = false;
|
|
1910
|
+
out.unread.push(v.node);
|
|
1911
|
+
continue;
|
|
1912
|
+
}
|
|
1913
|
+
if(v.into) {
|
|
1914
|
+
out.written = true;
|
|
1915
|
+
}
|
|
1916
|
+
let ctx = {scope: v.scope, via: [], off: false};
|
|
1917
|
+
let cur = v.spread? elements_of(v.node, ctx, 0) : expand(v.node, ctx);
|
|
1918
|
+
for(let step of steps.slice(v.steps.length)) {
|
|
1919
|
+
cur = cur.flatMap((o) => step_values(o, step, null, 0));
|
|
1920
|
+
}
|
|
1921
|
+
for(let o of cur) {
|
|
1922
|
+
let leaf = classify(o);
|
|
1923
|
+
if(leaf.kind === "unknown" && leaf.var_root) {
|
|
1924
|
+
let r = var_leaves(leaf.var_root, seen, depth + 1);
|
|
1925
|
+
out.leaves.push(...r.leaves);
|
|
1926
|
+
out.unread.push(...r.unread);
|
|
1927
|
+
out.lets.push(...r.lets);
|
|
1928
|
+
out.complete = out.complete && r.complete;
|
|
1929
|
+
out.written = out.written || r.written;
|
|
1930
|
+
continue;
|
|
1931
|
+
}
|
|
1932
|
+
out.leaves.push(leaf);
|
|
1933
|
+
if(!KNOWN.has(leaf.kind)) {
|
|
1934
|
+
out.complete = false;
|
|
1935
|
+
out.unread.push(leaf.node);
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
return out;
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
/***************************************************************
|
|
1943
|
+
* What a SOURCE can hold, for a test that derives a published list
|
|
1944
|
+
* from it (i18n_key_functions.test.js): the keys of leaves_of(), a
|
|
1945
|
+
* `let` / `var` met followed into everything it is given
|
|
1946
|
+
* (var_leaves()). `scope` is the chain of ancestors of `node`.
|
|
1947
|
+
*
|
|
1948
|
+
* -> {keys: [string], unread: [{at, node}]}
|
|
1949
|
+
*
|
|
1950
|
+
* `unread`: every value it cannot read (a parameter, a call, a
|
|
1951
|
+
* write it cannot follow). `at` is the node of the place it is read,
|
|
1952
|
+
* `node` the value there -- or inside the `let` read there -- that
|
|
1953
|
+
* stops the reader.
|
|
1954
|
+
***************************************************************/
|
|
1955
|
+
function source_keys(node, scope = [])
|
|
1956
|
+
{
|
|
1957
|
+
let keys = [];
|
|
1958
|
+
let unread = [];
|
|
1959
|
+
for(let leaf of leaves_of(node, scope)) {
|
|
1960
|
+
if(leaf.kind === "unknown" && leaf.var_root) {
|
|
1961
|
+
let r = var_leaves(leaf.var_root);
|
|
1962
|
+
for(let l of r.leaves) {
|
|
1963
|
+
if(l.kind === "key") {
|
|
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
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
for(let n of r.unread) {
|
|
1970
|
+
unread.push({at: leaf.node, node: n});
|
|
1971
|
+
}
|
|
1972
|
+
} else if(leaf.kind === "key") {
|
|
1973
|
+
keys.push(leaf.value);
|
|
1974
|
+
} else if(leaf.kind !== "nothing") {
|
|
1975
|
+
unread.push({at: leaf.node, node: leaf.node});
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
return {keys, unread};
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
|
|
1982
|
+
/***************************
|
|
1983
|
+
* Markers
|
|
1984
|
+
***************************/
|
|
1985
|
+
|
|
1986
|
+
|
|
1987
|
+
/*
|
|
1988
|
+
* A marker is a comment, at the place a key is read, that says where
|
|
1989
|
+
* the keys of what stands after it come from:
|
|
1990
|
+
*
|
|
1991
|
+
* /\* i18n: data *\/ data, no key of the library:
|
|
1992
|
+
* a backend's comment, a topic
|
|
1993
|
+
* name, a host's own key, a
|
|
1994
|
+
* DOM node
|
|
1995
|
+
* /\* i18n: C_YUI_FOO_I18N_KEYS *\/ the keys a published list
|
|
1996
|
+
* holds (several, by commas)
|
|
1997
|
+
*
|
|
1998
|
+
* It marks the node that starts right after it (only whitespace in
|
|
1999
|
+
* between) and everything inside that node: an argument, a branch of
|
|
2000
|
+
* it, a property, or the whole call.
|
|
2001
|
+
*/
|
|
2002
|
+
const MARKER_START_RE = /^\s*i18n\s*:/;
|
|
2003
|
+
const MARKER_RE = /^\s*i18n\s*:\s*(.*?)\s*$/;
|
|
2004
|
+
const MARKER_LIST_RE = /^[A-Za-z_$][\w$]*_I18N_KEYS$/;
|
|
2005
|
+
|
|
2006
|
+
/***************************************************************
|
|
2007
|
+
* The markers of a parsed file
|
|
2008
|
+
*
|
|
2009
|
+
* -> {at: Map(position of the node marked -> marker),
|
|
2010
|
+
* all: [marker], errors: [{line, why}]}
|
|
2011
|
+
*
|
|
2012
|
+
* marker: {kind: "data" | "lists", lists: [name], line, used: false}
|
|
2013
|
+
***************************************************************/
|
|
2014
|
+
function markers_of(ast)
|
|
2015
|
+
{
|
|
2016
|
+
let at = new Map();
|
|
2017
|
+
let all = [];
|
|
2018
|
+
let errors = [];
|
|
2019
|
+
for(let c of ast.comments || []) {
|
|
2020
|
+
if(!MARKER_START_RE.test(c.value)) {
|
|
2021
|
+
continue;
|
|
2022
|
+
}
|
|
2023
|
+
let line = line_of(ast, c.start);
|
|
2024
|
+
let body = c.value.match(MARKER_RE)[1];
|
|
2025
|
+
let marker = null;
|
|
2026
|
+
if(body === "data") {
|
|
2027
|
+
marker = {kind: "data", lists: [], line, used: false};
|
|
2028
|
+
} else {
|
|
2029
|
+
let lists = body.split(/\s*,\s*/);
|
|
2030
|
+
if(lists.length > 0 && lists.every((x) => MARKER_LIST_RE.test(x))) {
|
|
2031
|
+
marker = {kind: "lists", lists, line, used: false};
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
if(!marker) {
|
|
2035
|
+
errors.push({line, why: `"${c.value.trim()}" is no i18n marker: ` +
|
|
2036
|
+
`\`i18n: data\` or \`i18n: <NAME>_I18N_KEYS[, ...]\``});
|
|
2037
|
+
continue;
|
|
2038
|
+
}
|
|
2039
|
+
let next = c.end;
|
|
2040
|
+
while(next < ast.code.length && /\s/.test(ast.code[next])) {
|
|
2041
|
+
next++;
|
|
2042
|
+
}
|
|
2043
|
+
marker.next = next;
|
|
2044
|
+
at.set(next, marker);
|
|
2045
|
+
all.push(marker);
|
|
2046
|
+
}
|
|
2047
|
+
return {at, all, errors};
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
/***************************************************************
|
|
2051
|
+
* The marker of a leaf: the innermost one standing before a node of
|
|
2052
|
+
* the place it was read (its `via`), or null
|
|
2053
|
+
***************************************************************/
|
|
2054
|
+
function marker_of(leaf, markers)
|
|
2055
|
+
{
|
|
2056
|
+
let via = leaf.via || [];
|
|
2057
|
+
for(let i = via.length - 1; i >= 0; i--) {
|
|
2058
|
+
let m = markers.at.get(via[i].start);
|
|
2059
|
+
if(m) {
|
|
2060
|
+
return m;
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
return null;
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
|
|
2067
|
+
/***************************
|
|
2068
|
+
* Key functions
|
|
2069
|
+
***************************/
|
|
2070
|
+
|
|
2071
|
+
|
|
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);
|
|
2083
|
+
}
|
|
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);
|
|
2094
|
+
}
|
|
2095
|
+
let index = {exported, by_module};
|
|
2096
|
+
KEY_FN_INDEX.set(table, index);
|
|
2097
|
+
return index;
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
/***************************************************************
|
|
2101
|
+
* Does an import source name a gobj-ui module (the barrel, a deep
|
|
2102
|
+
* `src/` module, or, inside the library, a sibling)?
|
|
2103
|
+
***************************************************************/
|
|
2104
|
+
function is_gobj_ui_source(src, lib_module)
|
|
2105
|
+
{
|
|
2106
|
+
if(/^@yuneta\/gobj-ui(?:\/index\.js|\/src\/[\w.-]+\.js)?$/.test(src)) {
|
|
2107
|
+
return true;
|
|
2108
|
+
}
|
|
2109
|
+
return !!lib_module && /^\.\/[\w.-]+\.js$/.test(src);
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
/***************************************************************
|
|
2113
|
+
* The calls of a file to a key function, each with what the file
|
|
2114
|
+
* bound the callee to: an import of a gobj-ui function (by its
|
|
2115
|
+
* exported name), or, in the library module that defines it, the
|
|
2116
|
+
* function itself. `table` is I18N_KEY_FUNCTIONS by default.
|
|
2117
|
+
*
|
|
2118
|
+
* -> [{entry, call, scope}], `scope` the ancestors of the call
|
|
2119
|
+
***************************************************************/
|
|
2120
|
+
function key_calls(ast, lib_module, table)
|
|
2121
|
+
{
|
|
2122
|
+
let index = key_fn_index(table);
|
|
2123
|
+
let direct = new Map(); // local name -> entry
|
|
2124
|
+
let namespaces = new Set(); // `import * as ns` of a gobj-ui module
|
|
2125
|
+
for(let node of ast.body) {
|
|
2126
|
+
if(node.type !== "ImportDeclaration" || !is_gobj_ui_source(node.source.value, lib_module)) {
|
|
2127
|
+
continue;
|
|
2128
|
+
}
|
|
2129
|
+
for(let spec of node.specifiers) {
|
|
2130
|
+
if(spec.type === "ImportSpecifier") {
|
|
2131
|
+
let name = spec.imported.name || spec.imported.value;
|
|
2132
|
+
let entry = index.exported.get(name);
|
|
2133
|
+
if(entry) {
|
|
2134
|
+
direct.set(spec.local.name, entry);
|
|
2135
|
+
}
|
|
2136
|
+
} else if(spec.type === "ImportNamespaceSpecifier") {
|
|
2137
|
+
namespaces.add(spec.local.name);
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
if(lib_module && index.by_module.has(lib_module)) {
|
|
2142
|
+
for(let [name, entry] of index.by_module.get(lib_module)) {
|
|
2143
|
+
direct.set(name, entry);
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
if(direct.size === 0 && namespaces.size === 0) {
|
|
2147
|
+
return [];
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
let out = [];
|
|
2151
|
+
walk(ast, (n, ancestors) => {
|
|
2152
|
+
if(n.type !== "CallExpression") {
|
|
2153
|
+
return;
|
|
2154
|
+
}
|
|
2155
|
+
let entry = null;
|
|
2156
|
+
let c = n.callee;
|
|
2157
|
+
if(c.type === "Identifier") {
|
|
2158
|
+
entry = direct.get(c.name) || null;
|
|
2159
|
+
} else if(c.type === "MemberExpression" && !c.computed
|
|
2160
|
+
&& c.object.type === "Identifier" && namespaces.has(c.object.name)) {
|
|
2161
|
+
entry = index.exported.get(c.property.name) || null;
|
|
2162
|
+
}
|
|
2163
|
+
if(entry) {
|
|
2164
|
+
out.push({entry, call: n, scope: ancestors.slice()});
|
|
2165
|
+
}
|
|
2166
|
+
});
|
|
2167
|
+
return out;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
/***************************************************************
|
|
2171
|
+
* Is a leaf a key position of the function it is in, handed on?
|
|
2172
|
+
* `message` where `message` is a key parameter of that function,
|
|
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.
|
|
2176
|
+
***************************************************************/
|
|
2177
|
+
function is_forwarded(leaf, lib_module, table)
|
|
2178
|
+
{
|
|
2179
|
+
let index = key_fn_index(table);
|
|
2180
|
+
if(leaf.kind !== "param" || !lib_module || !index.by_module.has(lib_module)) {
|
|
2181
|
+
return false;
|
|
2182
|
+
}
|
|
2183
|
+
let entry = index.by_module.get(lib_module).get(leaf.binding.name);
|
|
2184
|
+
if(!entry) {
|
|
2185
|
+
return false;
|
|
2186
|
+
}
|
|
2187
|
+
return entry.keys.includes(`${leaf.binding.index}${leaf.path}`);
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
/***************************************************************
|
|
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
|
|
2221
|
+
*
|
|
2222
|
+
* a) a string literal, or a `?:` / `||` / `??` of them, nothing,
|
|
2223
|
+
* or DOM (an array spec, createElement2(), ...: is_dom());
|
|
2224
|
+
* b) a `const` the reader follows to one of those (also through a
|
|
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);
|
|
2231
|
+
* d) MARKED where it is handed: `/\* i18n: <NAME>_I18N_KEYS *\/`
|
|
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.
|
|
2237
|
+
*
|
|
2238
|
+
* -> [{line, call_line, name, path, text, why}]
|
|
2239
|
+
***************************************************************/
|
|
2240
|
+
function call_rule_violations(ast, lib_module, table)
|
|
2241
|
+
{
|
|
2242
|
+
let markers = markers_of(ast);
|
|
2243
|
+
let out = [];
|
|
2244
|
+
for(let {entry, call, scope} of key_calls(ast, lib_module, table)) {
|
|
2245
|
+
for(let path of entry.keys) {
|
|
2246
|
+
for(let leaf of values_at(call, path, scope)) {
|
|
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)) {
|
|
2252
|
+
continue;
|
|
2253
|
+
}
|
|
2254
|
+
out.push({
|
|
2255
|
+
line: line_of(ast, leaf.node.start),
|
|
2256
|
+
call_line: line_of(ast, call.start),
|
|
2257
|
+
name: entry.name,
|
|
2258
|
+
path: path,
|
|
2259
|
+
text: ast.code.slice(leaf.node.start, leaf.node.end).slice(0, 120),
|
|
2260
|
+
why: why
|
|
2261
|
+
});
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
return out;
|
|
2266
|
+
}
|
|
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
|
+
|
|
2284
|
+
|
|
2285
|
+
/***************************
|
|
2286
|
+
* Published lists
|
|
2287
|
+
***************************/
|
|
2288
|
+
|
|
2289
|
+
|
|
2290
|
+
/***************************************************************
|
|
2291
|
+
* The `<NAME>_I18N_KEYS` arrays a module exports, READ, never run:
|
|
2292
|
+
* `export const|let|var X = [...]`, `const X = [...]; export {X}`,
|
|
2293
|
+
* either wrapped in Object.freeze(). Each element is a string literal,
|
|
2294
|
+
* a template with no `${}`, or a spread of another array of the same
|
|
2295
|
+
* module read the same way -- a top-level `const`, or a list it
|
|
2296
|
+
* publishes: `[...BASE_I18N_KEYS, "k"]`.
|
|
2297
|
+
*
|
|
2298
|
+
* -> {lists: [{name, keys}], errors: [why]}
|
|
2299
|
+
*
|
|
2300
|
+
* Anything else so named is an ERROR, never skipped: the keys it
|
|
2301
|
+
* holds would go unasked, and the build say OK. That covers an
|
|
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
|
|
2304
|
+
* read: one declared and not exported, one declared inside a
|
|
2305
|
+
* function or a block, one declared by destructuring, one exported
|
|
2306
|
+
* under a name that does not end in _I18N_KEYS. A re-export
|
|
2307
|
+
* (`export {X} from "./y.js"`) is read in its own module.
|
|
2308
|
+
***************************************************************/
|
|
2309
|
+
function published_lists(ast)
|
|
2310
|
+
{
|
|
2311
|
+
let top = new Map(); // a top-level name -> its declaration
|
|
2312
|
+
for(let node of ast.body) {
|
|
2313
|
+
let decl = (node.type === "ExportNamedDeclaration")? node.declaration : node;
|
|
2314
|
+
if(!decl) {
|
|
2315
|
+
continue;
|
|
2316
|
+
}
|
|
2317
|
+
if(decl.type === "VariableDeclaration") {
|
|
2318
|
+
for(let d of decl.declarations) {
|
|
2319
|
+
if(d.id.type === "Identifier") {
|
|
2320
|
+
top.set(d.id.name, {init: d.init, kind: decl.kind});
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
} else if(decl.id && (decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration")) {
|
|
2324
|
+
top.set(decl.id.name, {init: null, what: decl.type});
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
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
|
+
|
|
2337
|
+
let lists = [];
|
|
2338
|
+
let errors = [];
|
|
2339
|
+
let read_as = new Set(); // local names exported under a list's name
|
|
2340
|
+
let read = (exported, local) => {
|
|
2341
|
+
read_as.add(local);
|
|
2342
|
+
let d = top.get(local);
|
|
2343
|
+
if(!d) {
|
|
2344
|
+
errors.push(`${exported}: not declared at the top of the module`);
|
|
2345
|
+
return;
|
|
2346
|
+
}
|
|
2347
|
+
if(!d.init) {
|
|
2348
|
+
errors.push(`${exported}: ${d.what? "a " + d.what : "declared with no value"}, not an array`);
|
|
2349
|
+
return;
|
|
2350
|
+
}
|
|
2351
|
+
let r = read_string_array(d.init, base_of, new Set([local]));
|
|
2352
|
+
if(r.error) {
|
|
2353
|
+
errors.push(`${exported}: ${r.error}`);
|
|
2354
|
+
return;
|
|
2355
|
+
}
|
|
2356
|
+
lists.push({name: exported, keys: r.keys});
|
|
2357
|
+
};
|
|
2358
|
+
let exported_as = new Map(); // local name -> [exported names]
|
|
2359
|
+
for(let node of ast.body) {
|
|
2360
|
+
if(node.type !== "ExportNamedDeclaration") {
|
|
2361
|
+
continue;
|
|
2362
|
+
}
|
|
2363
|
+
if(node.declaration) {
|
|
2364
|
+
let decl = node.declaration;
|
|
2365
|
+
if(decl.type === "VariableDeclaration") {
|
|
2366
|
+
for(let d of decl.declarations) {
|
|
2367
|
+
if(d.id.type === "Identifier" && PUBLISHED_LIST_RE.test(d.id.name)) {
|
|
2368
|
+
read(d.id.name, d.id.name);
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
} else if(decl.id && PUBLISHED_LIST_RE.test(decl.id.name)) {
|
|
2372
|
+
read(decl.id.name, decl.id.name);
|
|
2373
|
+
}
|
|
2374
|
+
continue;
|
|
2375
|
+
}
|
|
2376
|
+
if(node.source) {
|
|
2377
|
+
continue; // a re-export: read where it is declared
|
|
2378
|
+
}
|
|
2379
|
+
for(let spec of node.specifiers) {
|
|
2380
|
+
let exported = spec.exported.name || spec.exported.value;
|
|
2381
|
+
if(!exported_as.has(spec.local.name)) {
|
|
2382
|
+
exported_as.set(spec.local.name, []);
|
|
2383
|
+
}
|
|
2384
|
+
exported_as.get(spec.local.name).push(exported);
|
|
2385
|
+
if(PUBLISHED_LIST_RE.test(exported)) {
|
|
2386
|
+
read(exported, spec.local.name);
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
/* A list that no export makes readable */
|
|
2392
|
+
walk(ast, (n, ancestors) => {
|
|
2393
|
+
if(n.type !== "VariableDeclarator") {
|
|
2394
|
+
return;
|
|
2395
|
+
}
|
|
2396
|
+
for(let name of pattern_names(n.id)) {
|
|
2397
|
+
if(!PUBLISHED_LIST_RE.test(name)) {
|
|
2398
|
+
continue;
|
|
2399
|
+
}
|
|
2400
|
+
let at_top = (ancestors.length === 2 && ancestors[1].type === "VariableDeclaration")
|
|
2401
|
+
|| (ancestors.length === 3 && ancestors[1].type === "ExportNamedDeclaration");
|
|
2402
|
+
if(n.id.type !== "Identifier") {
|
|
2403
|
+
errors.push(`${name}: declared by destructuring, which no reader follows: ` +
|
|
2404
|
+
`write it as an array of literals`);
|
|
2405
|
+
} else if(!at_top) {
|
|
2406
|
+
errors.push(`${name}: declared inside a function or a block, where no consumer reads it`);
|
|
2407
|
+
} else if(!read_as.has(name)) {
|
|
2408
|
+
let as = exported_as.get(name);
|
|
2409
|
+
errors.push(as? `${name}: exported as ${as.join(", ")}, a name the validator does not read` :
|
|
2410
|
+
`${name}: not exported, so no consumer reads it`);
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
});
|
|
2414
|
+
return {lists, errors};
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
/***************************************************************
|
|
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.
|
|
2422
|
+
***************************************************************/
|
|
2423
|
+
function read_string_array(init, base_of = () => null, seen = new Set())
|
|
2424
|
+
{
|
|
2425
|
+
let n = init;
|
|
2426
|
+
while(is_object_call(n, "freeze") && n.arguments.length === 1) {
|
|
2427
|
+
n = n.arguments[0];
|
|
2428
|
+
}
|
|
2429
|
+
if(n.type !== "ArrayExpression") {
|
|
2430
|
+
return {error: `a ${n.type}, not an array of string literals`};
|
|
2431
|
+
}
|
|
2432
|
+
let keys = [];
|
|
2433
|
+
for(let i = 0; i < n.elements.length; i++) {
|
|
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
|
+
}
|
|
2449
|
+
let s = null;
|
|
2450
|
+
if(el && el.type === "Literal" && typeof el.value === "string") {
|
|
2451
|
+
s = el.value;
|
|
2452
|
+
} else if(el && el.type === "TemplateLiteral" && el.expressions.length === 0) {
|
|
2453
|
+
s = el.quasis[0].value.cooked;
|
|
2454
|
+
}
|
|
2455
|
+
if(!s) {
|
|
2456
|
+
return {error: `element ${i} is ${el? "a " + el.type : "a hole"}` +
|
|
2457
|
+
`${s === ""? " (an empty string)" : ""}, not a string literal`};
|
|
2458
|
+
}
|
|
2459
|
+
keys.push(s);
|
|
2460
|
+
}
|
|
2461
|
+
return {keys};
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
|
|
2465
|
+
/***************************
|
|
2466
|
+
* The scan
|
|
2467
|
+
***************************/
|
|
2468
|
+
|
|
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
|
+
|
|
2561
|
+
/***************************************************************
|
|
2562
|
+
* The keys one parsed file asks for.
|
|
2563
|
+
*
|
|
2564
|
+
* ctx: {lib_module (its name in src/, or null for an app file),
|
|
2565
|
+
* attr_keys, array_keys (Sets), declared (Set)}
|
|
2566
|
+
* -> {keys: Set, read_keys: Set (those READ, not published by a
|
|
2567
|
+
* list), declared_seen: Set,
|
|
2568
|
+
* lists: [{name, keys}], list_errors: [why],
|
|
2569
|
+
* marker_refs: [{lists, keys, line}], marker_errors: [{line, why}]}
|
|
2570
|
+
*
|
|
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.
|
|
2578
|
+
***************************************************************/
|
|
2579
|
+
function scan_ast(ast, ctx)
|
|
2580
|
+
{
|
|
2581
|
+
let keys = new Set();
|
|
2582
|
+
let declared_seen = new Set();
|
|
2583
|
+
let declared = ctx.declared || new Set();
|
|
2584
|
+
let markers = markers_of(ast);
|
|
2585
|
+
let var_errors = [];
|
|
2586
|
+
let vars_checked = new Set();
|
|
2587
|
+
|
|
2588
|
+
/*
|
|
2589
|
+
* A marker says where the keys of what it marks come from, and a
|
|
2590
|
+
* `let` / `var` it marks -- itself, or a member or an element read
|
|
2591
|
+
* from it -- is held to it: everything the variable is given (and,
|
|
2592
|
+
* for a member, written into it) is read, each key of it is asked
|
|
2593
|
+
* for and must be in the lists the marker names. One that is only
|
|
2594
|
+
* ever given literals needs no marker at all: it is a const that
|
|
2595
|
+
* was not written as one. (What is WRITTEN into a table is not that
|
|
2596
|
+
* case: a const would not carry it to the reader.)
|
|
2597
|
+
*/
|
|
2598
|
+
let check_marked_var = (leaf, m) => {
|
|
2599
|
+
let root = leaf.var_root;
|
|
2600
|
+
let b = root.binding;
|
|
2601
|
+
let id = `${m.line}:${b.declarators[0].start}:${JSON.stringify(root.steps)}`;
|
|
2602
|
+
if(vars_checked.has(id)) {
|
|
2603
|
+
return;
|
|
2604
|
+
}
|
|
2605
|
+
vars_checked.add(id);
|
|
2606
|
+
let r = var_leaves(root);
|
|
2607
|
+
let held = [...new Set(r.leaves.filter((l) => l.kind === "key").map((l) => l.value))];
|
|
2608
|
+
for(let k of held) {
|
|
2609
|
+
keys.add(k);
|
|
2610
|
+
}
|
|
2611
|
+
if(r.complete && !r.written && held.length > 0) {
|
|
2612
|
+
let decl_line = (x) => line_of(ast, x.declarators[0].start);
|
|
2613
|
+
let seen_lets = new Set([b.declarators[0]]);
|
|
2614
|
+
let others = r.lets.filter((x) => {
|
|
2615
|
+
let had = seen_lets.has(x.declarators[0]);
|
|
2616
|
+
seen_lets.add(x.declarators[0]);
|
|
2617
|
+
return !had;
|
|
2618
|
+
});
|
|
2619
|
+
let through = others.map((x) => `the ${x.decl} \`${x.name}\` (line ${decl_line(x)})`);
|
|
2620
|
+
var_errors.push({line: line_of(ast, leaf.node.start),
|
|
2621
|
+
why: `\`${b.name}\` is a ${b.decl} (line ${decl_line(b)}) that only ` +
|
|
2622
|
+
`ever holds literals (${held.map((k) => JSON.stringify(k)).join(", ")})` +
|
|
2623
|
+
(through.length? `, through ${through.join(", ")}` : "") +
|
|
2624
|
+
`: make ${through.length? "them consts" : "it a const"}, which the reader follows, ` +
|
|
2625
|
+
`and drop the marker`});
|
|
2626
|
+
return;
|
|
2627
|
+
}
|
|
2628
|
+
if(m.kind === "lists") {
|
|
2629
|
+
for(let k of held) {
|
|
2630
|
+
m.keys.add(k);
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
};
|
|
2634
|
+
|
|
2635
|
+
let take = (leaves) => {
|
|
2636
|
+
for(let leaf of leaves) {
|
|
2637
|
+
if(leaf.kind === "key") {
|
|
2638
|
+
keys.add(leaf.value);
|
|
2639
|
+
}
|
|
2640
|
+
let m = marker_of(leaf, markers);
|
|
2641
|
+
if(m) {
|
|
2642
|
+
m.used = true;
|
|
2643
|
+
if(!m.keys) {
|
|
2644
|
+
m.keys = new Set();
|
|
2645
|
+
}
|
|
2646
|
+
if(!m.leaves) {
|
|
2647
|
+
m.leaves = [];
|
|
2648
|
+
}
|
|
2649
|
+
m.leaves.push(leaf);
|
|
2650
|
+
if(leaf.kind === "key") {
|
|
2651
|
+
m.keys.add(leaf.value);
|
|
2652
|
+
}
|
|
2653
|
+
if(leaf.kind === "unknown" && leaf.var_root) {
|
|
2654
|
+
check_marked_var(leaf, m);
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
};
|
|
2659
|
+
let add_markup = (text) => {
|
|
2660
|
+
if(typeof text !== "string" || !text.includes("data-i18n")) {
|
|
2661
|
+
return;
|
|
2662
|
+
}
|
|
2663
|
+
DATA_I18N_MARKUP_RE.lastIndex = 0;
|
|
2664
|
+
let m;
|
|
2665
|
+
while((m = DATA_I18N_MARKUP_RE.exec(text)) !== null) {
|
|
2666
|
+
if(!m[1].includes("${")) {
|
|
2667
|
+
keys.add(m[1]);
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
};
|
|
2671
|
+
|
|
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);
|
|
2680
|
+
}
|
|
2681
|
+
} else if(n.type === "Literal") {
|
|
2682
|
+
add_markup(n.value);
|
|
2683
|
+
} else if(n.type === "TemplateElement") {
|
|
2684
|
+
add_markup(n.value.cooked);
|
|
2685
|
+
}
|
|
2686
|
+
});
|
|
2687
|
+
|
|
2688
|
+
/* The keys a module publishes as data */
|
|
2689
|
+
let read_keys = new Set(keys);
|
|
2690
|
+
let published = published_lists(ast);
|
|
2691
|
+
for(let list of published.lists) {
|
|
2692
|
+
for(let k of list.keys) {
|
|
2693
|
+
keys.add(k);
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
/* The markers: each one stands where a key is read, and is one */
|
|
2698
|
+
let marker_errors = [...markers.errors, ...var_errors];
|
|
2699
|
+
let marker_refs = [];
|
|
2700
|
+
for(let m of markers.all) {
|
|
2701
|
+
if(!m.used) {
|
|
2702
|
+
marker_errors.push({line: m.line, why: `the i18n marker stands before no place a key is read ` +
|
|
2703
|
+
`(a key position of a key function, t(), an i18n or data-i18n* value)`});
|
|
2704
|
+
} else if(m.kind === "lists") {
|
|
2705
|
+
marker_refs.push({lists: m.lists, keys: [...(m.keys || [])], line: m.line});
|
|
2706
|
+
} else if(m.kind === "data" && m.leaves.every((l) => KNOWN.has(l.kind)) && m.keys.size > 0) {
|
|
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)`});
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
return {
|
|
2714
|
+
keys,
|
|
2715
|
+
read_keys,
|
|
2716
|
+
declared_seen,
|
|
2717
|
+
lists: published.lists,
|
|
2718
|
+
list_errors: published.errors,
|
|
2719
|
+
marker_refs,
|
|
2720
|
+
marker_errors
|
|
2721
|
+
};
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
/***************************************************************
|
|
2725
|
+
* The i18n keys the source asks for.
|
|
2726
|
+
*
|
|
2727
|
+
* opts: {src: [dir URL], locales: dir URL (not scanned),
|
|
2728
|
+
* exclude: [file URL] (not scanned either),
|
|
2729
|
+
* config: file URL or [file URL] (the shell declarations),
|
|
2730
|
+
* config_key: (value) -> bool (a config value is a key?),
|
|
2731
|
+
* attr_keys, array_keys: [property name],
|
|
2732
|
+
* language_names: Set}
|
|
2733
|
+
* -> {keys: Set (all), lib_keys: Set (those of the gobj-ui
|
|
2734
|
+
* modules the app mounts)}
|
|
2735
|
+
*
|
|
2736
|
+
* A violation found on the way (a file that cannot be parsed, a list
|
|
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().
|
|
2741
|
+
***************************************************************/
|
|
2742
|
+
function collect_used_keys(opts)
|
|
2743
|
+
{
|
|
2744
|
+
let keys = new Set();
|
|
2745
|
+
let lib_keys = new Set();
|
|
2746
|
+
let attr_keys = opts.attr_keys || [];
|
|
2747
|
+
let array_keys = opts.array_keys || [];
|
|
2748
|
+
let ctx = {
|
|
2749
|
+
attr_keys: new Set(attr_keys),
|
|
2750
|
+
array_keys: new Set(array_keys),
|
|
2751
|
+
declared: new Set([...attr_keys, ...array_keys])
|
|
2752
|
+
};
|
|
2753
|
+
let declared_seen = new Set();
|
|
2754
|
+
let barrel = barrel_export_map();
|
|
2755
|
+
let published = new Map(); // list name -> Set of keys
|
|
2756
|
+
let refs = []; // [{label, line, lists, keys}]
|
|
2757
|
+
|
|
2758
|
+
let scan = (url, lib_module) => {
|
|
2759
|
+
let label = lib_module? `gobj-ui/src/${lib_module}` : relative(process.cwd(), fileURLToPath(url));
|
|
2760
|
+
let ast;
|
|
2761
|
+
try {
|
|
2762
|
+
ast = parse_source(readFileSync(url, "utf8"));
|
|
2763
|
+
} catch(e) {
|
|
2764
|
+
fail(`${label} cannot be parsed (${e.message}): the keys it asks for are NOT checked`);
|
|
2765
|
+
return [];
|
|
2766
|
+
}
|
|
2767
|
+
let r = scan_ast(ast, {...ctx, lib_module});
|
|
2768
|
+
for(let k of r.keys) {
|
|
2769
|
+
keys.add(k);
|
|
2770
|
+
if(lib_module) {
|
|
2771
|
+
lib_keys.add(k);
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
for(let name of r.declared_seen) {
|
|
2775
|
+
declared_seen.add(name);
|
|
2776
|
+
}
|
|
2777
|
+
for(let why of r.list_errors) {
|
|
2778
|
+
fail(`${label}: the published list ${why}: its keys are NOT asked for`);
|
|
2779
|
+
}
|
|
2780
|
+
for(let list of r.lists) {
|
|
2781
|
+
if(!published.has(list.name)) {
|
|
2782
|
+
published.set(list.name, new Set());
|
|
2783
|
+
}
|
|
2784
|
+
for(let k of list.keys) {
|
|
2785
|
+
published.get(list.name).add(k);
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
for(let e of r.marker_errors) {
|
|
2789
|
+
fail(`${label}:${e.line}: ${e.why}`);
|
|
2790
|
+
}
|
|
2791
|
+
for(let ref of r.marker_refs) {
|
|
2792
|
+
refs.push({label, ...ref});
|
|
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
|
+
}
|
|
2800
|
+
return lib_imports(ast, barrel, lib_module);
|
|
2801
|
+
};
|
|
2802
|
+
|
|
2803
|
+
/* The app's own source, and the code it shares with others */
|
|
2804
|
+
let pending = [];
|
|
2805
|
+
let excluded = new Set((opts.exclude || []).map((u) => u.href));
|
|
2806
|
+
for(let dir of opts.src) {
|
|
2807
|
+
if(!existsSync(dir)) {
|
|
2808
|
+
abort(`no source directory at ${dir.pathname}`);
|
|
2809
|
+
}
|
|
2810
|
+
for(let url of source_files(dir, opts.locales)) {
|
|
2811
|
+
if(!excluded.has(url.href)) {
|
|
2812
|
+
pending.push(...scan(url, null));
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
/* A name declared and found in no source is a typo that turns its check off */
|
|
2818
|
+
for(let name of ctx.declared) {
|
|
2819
|
+
if(!declared_seen.has(name)) {
|
|
2820
|
+
fail(`package.json "validate-locales" names ${JSON.stringify(name)}, which no source uses: a typo turns its check off`);
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
let configs = [].concat(opts.config || []);
|
|
2825
|
+
let is_key = opts.config_key || ((v) => v === v.toLowerCase() && !(opts.language_names || new Set()).has(v));
|
|
2826
|
+
for(let url of configs) {
|
|
2827
|
+
keys_from_shell_config(url, keys, is_key);
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
/*
|
|
2831
|
+
* ...and the gobj-ui views it MOUNTS. Only the modules actually
|
|
2832
|
+
* imported are scanned, transitively: the library carries views the
|
|
2833
|
+
* app never mounts.
|
|
2834
|
+
*/
|
|
2835
|
+
if(!existsSync(LIB_SRC)) {
|
|
2836
|
+
abort(`no gobj-ui source at ${LIB_SRC.pathname}`);
|
|
2837
|
+
}
|
|
2838
|
+
let missing = [];
|
|
2839
|
+
let seen = new Set();
|
|
2840
|
+
while(pending.length > 0) {
|
|
2841
|
+
let name = pending.pop();
|
|
2842
|
+
if(seen.has(name)) {
|
|
2843
|
+
continue;
|
|
2844
|
+
}
|
|
2845
|
+
seen.add(name);
|
|
2846
|
+
let url = new URL(name, LIB_SRC);
|
|
2847
|
+
if(!existsSync(url)) {
|
|
2848
|
+
missing.push(name);
|
|
2849
|
+
continue;
|
|
2850
|
+
}
|
|
2851
|
+
pending.push(...scan(url, name));
|
|
2852
|
+
}
|
|
2853
|
+
if(missing.length > 0) {
|
|
2854
|
+
abort(`modules of gobj-ui not found in ${LIB_SRC.pathname}: ${missing.join(", ")}`);
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2857
|
+
/* A marker names lists that were read, and what it marks is in them */
|
|
2858
|
+
for(let ref of refs) {
|
|
2859
|
+
let names = ref.lists.filter((name) => published.has(name));
|
|
2860
|
+
for(let name of ref.lists) {
|
|
2861
|
+
if(!published.has(name)) {
|
|
2862
|
+
fail(`${ref.label}:${ref.line}: the i18n marker names ${name}, ` +
|
|
2863
|
+
`a list no module the app mounts publishes`);
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
if(names.length < ref.lists.length) {
|
|
2867
|
+
continue;
|
|
2868
|
+
}
|
|
2869
|
+
for(let k of ref.keys) {
|
|
2870
|
+
if(!names.some((name) => published.get(name).has(k))) {
|
|
2871
|
+
fail(`${ref.label}:${ref.line}: ${JSON.stringify(k)} is marked as a key of ` +
|
|
2872
|
+
`${ref.lists.join(", ")}, and is not in it`);
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
return {keys, lib_keys};
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
/***************************************************************
|
|
2881
|
+
* The .js / .mjs files of a source directory, recursively: not the tests
|
|
2882
|
+
* (*.test.js, *.spec.js, __tests__/, test/, tests/), not a locales/
|
|
2883
|
+
* directory (its keys are definitions, not uses), not node_modules or a
|
|
2884
|
+
* build
|
|
2885
|
+
***************************************************************/
|
|
2886
|
+
const SKIPPED_DIRS = new Set(["node_modules", "dist", "locales", "__tests__", "test", "tests"]);
|
|
2887
|
+
|
|
2888
|
+
function source_files(dir, locales_dir)
|
|
2889
|
+
{
|
|
2890
|
+
let out = [];
|
|
2891
|
+
let walk_dir = (d) => {
|
|
2892
|
+
if(locales_dir && d.href === locales_dir.href) {
|
|
2893
|
+
return;
|
|
2894
|
+
}
|
|
2895
|
+
for(let f of readdirSync(d).sort()) {
|
|
2896
|
+
let url = new URL(f, d);
|
|
2897
|
+
let st = statSync(url);
|
|
2898
|
+
if(st.isDirectory()) {
|
|
2899
|
+
if(SKIPPED_DIRS.has(f) || f.startsWith(".")) {
|
|
2900
|
+
continue;
|
|
2901
|
+
}
|
|
2902
|
+
walk_dir(new URL(f + "/", d));
|
|
2903
|
+
} else if(/\.m?js$/.test(f) && !/\.(test|spec)\.m?js$/.test(f)) {
|
|
2904
|
+
out.push(url);
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
};
|
|
2908
|
+
walk_dir(dir);
|
|
2909
|
+
return out;
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
/***************************************************************
|
|
2913
|
+
* The shell declaration (`app_config.json`) holds i18n keys in DATA:
|
|
2914
|
+
* every `name`, `aria_label` and `tooltip` of an item, the `label` of a
|
|
2915
|
+
* C_YUI_NODE node, the `title` / `subtitle` of a route's kw. No `t()`
|
|
2916
|
+
* names them, and a missing one renders in raw lower-case English for
|
|
2917
|
+
* ever -- which is how four toolbar buttons announced themselves as
|
|
2918
|
+
* "toggle language" in Spanish.
|
|
2919
|
+
*
|
|
2920
|
+
* `is_key(value)` says which values are keys. By default, a value that
|
|
2921
|
+
* is not lower-case is a literal the config carries on purpose ("ES/EN"
|
|
2922
|
+
* on a language button); so is the endonym of one of the app's locales
|
|
2923
|
+
* (its `name`: "中文", "español") in a language menu. Any other value is
|
|
2924
|
+
* a key -- "configuración" is a Spanish literal that forgot its key,
|
|
2925
|
+
* and it is reported, as an ASCII one would be. `wordmark` and `alt` are
|
|
2926
|
+
* deliberately out: a brand name is not translated. A key starting with
|
|
2927
|
+
* `_` is a comment.
|
|
2928
|
+
***************************************************************/
|
|
2929
|
+
const CONFIG_I18N_FIELDS = ["name", "aria_label", "tooltip", "label", "title", "subtitle"];
|
|
2930
|
+
|
|
2931
|
+
function keys_from_shell_config(url, keys, is_key)
|
|
2932
|
+
{
|
|
2933
|
+
if(!existsSync(url)) {
|
|
2934
|
+
note(`no ${url.pathname}: no shell keys checked`);
|
|
2935
|
+
return;
|
|
2936
|
+
}
|
|
2937
|
+
let config;
|
|
2938
|
+
try {
|
|
2939
|
+
config = JSON.parse(readFileSync(url, "utf8"));
|
|
2940
|
+
} catch(e) {
|
|
2941
|
+
abort(`${url.pathname} is not valid json: ${e.message}`);
|
|
2942
|
+
}
|
|
2943
|
+
let walk_config = (node) => {
|
|
2944
|
+
if(Array.isArray(node)) {
|
|
2945
|
+
for(let item of node) {
|
|
2946
|
+
walk_config(item);
|
|
2947
|
+
}
|
|
2948
|
+
return;
|
|
2949
|
+
}
|
|
2950
|
+
if(!node || typeof node !== "object") {
|
|
2951
|
+
return;
|
|
2952
|
+
}
|
|
2953
|
+
for(let [k, v] of Object.entries(node)) {
|
|
2954
|
+
if(k.startsWith("_")) {
|
|
2955
|
+
continue;
|
|
2956
|
+
}
|
|
2957
|
+
if(CONFIG_I18N_FIELDS.includes(k) && typeof v === "string" && v && is_key(v)) {
|
|
2958
|
+
keys.add(v);
|
|
2959
|
+
}
|
|
2960
|
+
walk_config(v);
|
|
2961
|
+
}
|
|
2962
|
+
};
|
|
2963
|
+
walk_config(config);
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
/***************************************************************
|
|
2967
|
+
* Drop /* ... *\/ and // ... comments (good enough for the duplicate
|
|
2968
|
+
* key scan of a locale file)
|
|
2969
|
+
***************************************************************/
|
|
2970
|
+
function strip_comments(src)
|
|
2971
|
+
{
|
|
2972
|
+
return src
|
|
2973
|
+
.replace(/\/\*[\s\S]*?\*\//g, " ")
|
|
2974
|
+
.replace(/(^|[^:"'`\\])\/\/[^\n]*/g, "$1");
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2977
|
+
/***************************************************************
|
|
2978
|
+
* The barrel's public surface: exported symbol -> the src/ module that
|
|
2979
|
+
* defines it, read from index.js's re-exports
|
|
2980
|
+
* (`export {a, b as c} from "./src/x.js"`). A barrel import is resolved
|
|
2981
|
+
* to the modules it really pulls in, not to the whole library.
|
|
2982
|
+
***************************************************************/
|
|
2983
|
+
function barrel_export_map()
|
|
2984
|
+
{
|
|
2985
|
+
let map = new Map();
|
|
2986
|
+
let ast;
|
|
2987
|
+
try {
|
|
2988
|
+
ast = parse_source(readFileSync(LIB_INDEX, "utf8"));
|
|
2989
|
+
} catch(e) {
|
|
2990
|
+
abort(`cannot read the gobj-ui barrel at ${LIB_INDEX.pathname}: ${e.message}`);
|
|
2991
|
+
}
|
|
2992
|
+
for(let node of ast.body) {
|
|
2993
|
+
if(node.type !== "ExportNamedDeclaration" || !node.source) {
|
|
2994
|
+
continue;
|
|
2995
|
+
}
|
|
2996
|
+
let m = String(node.source.value).match(/^\.\/src\/([\w.-]+\.js)$/);
|
|
2997
|
+
if(!m) {
|
|
2998
|
+
continue;
|
|
2999
|
+
}
|
|
3000
|
+
for(let spec of node.specifiers) {
|
|
3001
|
+
/* `a as b` is published as `b` */
|
|
3002
|
+
map.set(spec.exported.name || spec.exported.value, m[1]);
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
return map;
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
/***************************************************************
|
|
3009
|
+
* The gobj-ui source modules a parsed file imports (or re-exports):
|
|
3010
|
+
* - deep: "@yuneta/gobj-ui/src/x.js", also by a dynamic import()
|
|
3011
|
+
* - barrel: "@yuneta/gobj-ui[/index.js]", resolved per imported symbol
|
|
3012
|
+
* - inside the library: "./x.js" (in the app it names an app file,
|
|
3013
|
+
* scanned already)
|
|
3014
|
+
***************************************************************/
|
|
3015
|
+
function lib_imports(ast, barrel, lib_module)
|
|
3016
|
+
{
|
|
3017
|
+
let out = [];
|
|
3018
|
+
let module_of = (src) => {
|
|
3019
|
+
if(typeof src !== "string") {
|
|
3020
|
+
return null;
|
|
3021
|
+
}
|
|
3022
|
+
let m = src.match(/^@yuneta\/gobj-ui\/src\/([\w.-]+\.js)$/);
|
|
3023
|
+
if(m) {
|
|
3024
|
+
return m[1];
|
|
3025
|
+
}
|
|
3026
|
+
if(lib_module) {
|
|
3027
|
+
m = src.match(/^\.\/([\w.-]+\.js)$/);
|
|
3028
|
+
if(m) {
|
|
3029
|
+
return m[1];
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
return null;
|
|
3033
|
+
};
|
|
3034
|
+
let is_barrel = (src) => /^@yuneta\/gobj-ui(?:\/index\.js)?$/.test(src);
|
|
3035
|
+
|
|
3036
|
+
walk(ast, (n) => {
|
|
3037
|
+
let src = null;
|
|
3038
|
+
let names = null;
|
|
3039
|
+
if(n.type === "ImportDeclaration") {
|
|
3040
|
+
src = n.source.value;
|
|
3041
|
+
names = n.specifiers
|
|
3042
|
+
.filter((s) => s.type === "ImportSpecifier")
|
|
3043
|
+
.map((s) => s.imported.name || s.imported.value);
|
|
3044
|
+
} else if((n.type === "ExportNamedDeclaration" || n.type === "ExportAllDeclaration") && n.source) {
|
|
3045
|
+
src = n.source.value;
|
|
3046
|
+
names = (n.specifiers || []).map((s) => s.local.name || s.local.value);
|
|
3047
|
+
} else if(n.type === "ImportExpression" && n.source.type === "Literal") {
|
|
3048
|
+
src = n.source.value;
|
|
3049
|
+
}
|
|
3050
|
+
if(src === null) {
|
|
3051
|
+
return;
|
|
3052
|
+
}
|
|
3053
|
+
let mod = module_of(src);
|
|
3054
|
+
if(mod) {
|
|
3055
|
+
out.push(mod);
|
|
3056
|
+
} else if(is_barrel(src) && names) {
|
|
3057
|
+
for(let name of names) {
|
|
3058
|
+
let m = barrel.get(name);
|
|
3059
|
+
if(m) {
|
|
3060
|
+
out.push(m);
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
});
|
|
3065
|
+
return out;
|
|
3066
|
+
}
|
|
3067
|
+
|
|
3068
|
+
/* The readers, for a test of the library and for an app whose catalog
|
|
3069
|
+
* has another shape (gobj-ui's test-app) */
|
|
3070
|
+
export {
|
|
3071
|
+
parse_source,
|
|
3072
|
+
walk,
|
|
3073
|
+
line_of,
|
|
3074
|
+
prop_name,
|
|
3075
|
+
binding_of,
|
|
3076
|
+
leaves_of,
|
|
3077
|
+
static_keys,
|
|
3078
|
+
source_keys,
|
|
3079
|
+
var_leaves,
|
|
3080
|
+
values_at,
|
|
3081
|
+
is_dom,
|
|
3082
|
+
markers_of,
|
|
3083
|
+
marker_of,
|
|
3084
|
+
key_calls,
|
|
3085
|
+
key_places,
|
|
3086
|
+
is_forwarded,
|
|
3087
|
+
call_rule_violations,
|
|
3088
|
+
call_rule_message,
|
|
3089
|
+
published_lists,
|
|
3090
|
+
scan_ast,
|
|
3091
|
+
collect_used_keys,
|
|
3092
|
+
keys_from_shell_config,
|
|
3093
|
+
source_files,
|
|
3094
|
+
lib_imports,
|
|
3095
|
+
find_plural_groups,
|
|
3096
|
+
violation_count
|
|
3097
|
+
};
|
|
3098
|
+
|
|
3099
|
+
/*
|
|
3100
|
+
* Run when it is the script, not when a test or another script imports
|
|
3101
|
+
* it. Both sides are resolved: with --preserve-symlinks-main (also from
|
|
3102
|
+
* NODE_OPTIONS) and a symlinked package, argv[1] and import.meta.url
|
|
3103
|
+
* both keep the link, and only the real paths compare.
|
|
3104
|
+
*/
|
|
3105
|
+
let is_main = false;
|
|
3106
|
+
try {
|
|
3107
|
+
is_main = !!process.argv[1]
|
|
3108
|
+
&& realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
|
3109
|
+
} catch(e) {
|
|
3110
|
+
is_main = false;
|
|
3111
|
+
}
|
|
3112
|
+
if(is_main) {
|
|
3113
|
+
await main();
|
|
3114
|
+
}
|