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