@yuneta/gobj-ui 7.26.31 → 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 +237 -72
- package/dist/gobj-ui.cjs.js +22 -16
- package/dist/gobj-ui.es.js +22 -16
- 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 +5 -4
- package/src/c_yui_treedb_graph.js +6 -3
- package/src/c_yui_treedb_topics.js +6 -3
- package/src/i18n_key_functions.js +54 -31
- package/src/i18n_key_functions.test.js +461 -49
- package/src/yui_table_select.js +20 -13
|
@@ -22,6 +22,13 @@
|
|
|
22
22
|
* one nothing calls by name (an FSM action, a callback) marks what
|
|
23
23
|
* it hands on. The search runs to a fixpoint, and fails naming the
|
|
24
24
|
* entry to add;
|
|
25
|
+
* - the table is CORRECT (V22): the same search started from no
|
|
26
|
+
* table finds every entry, at every position, with the same
|
|
27
|
+
* `exported` -- an entry nothing forwards to fails;
|
|
28
|
+
* - a parameter handed on through what no position can name (V22:
|
|
29
|
+
* `k.toLowerCase()`, a concatenation, `Object.entries(opts)`, a
|
|
30
|
+
* rest parameter walked, `arguments[i]`, an array pattern) fails,
|
|
31
|
+
* unless it is marked where it is handed;
|
|
25
32
|
* - THE RULE, per call, in every module: each value a key position
|
|
26
33
|
* of a key function can take is a string literal (or nothing, or
|
|
27
34
|
* DOM), a const the reader follows to one, a key position of the
|
|
@@ -29,16 +36,20 @@
|
|
|
29
36
|
* handed -- a comment `i18n: <NAME>_I18N_KEYS` (the module
|
|
30
37
|
* publishes those keys) or `i18n: data` (no key a list can hold).
|
|
31
38
|
* Anything else fails, naming the file and the line; so does a
|
|
32
|
-
* translated text
|
|
39
|
+
* translated text -- t("..."), tr(), an alias of t -- marked or
|
|
40
|
+
* not;
|
|
33
41
|
* - every marker is one and stands where a key is read; the lists
|
|
34
42
|
* it names are published by its module or one it imports; the
|
|
35
43
|
* keys it marks that can be read are in them -- a marked `let`
|
|
36
44
|
* included (or a member or an element read from one), whose
|
|
37
45
|
* every literal is read, and which fails when it holds nothing
|
|
38
46
|
* but literals (a const not written as one); a `data` marker on
|
|
39
|
-
* what the reader follows to keys fails (V21.4.2)
|
|
40
|
-
*
|
|
41
|
-
*
|
|
47
|
+
* what the reader follows to keys fails (V21.4.2), and so does
|
|
48
|
+
* one that would hide a key (V22: a call mixing keys and data, an
|
|
49
|
+
* array of string literals);
|
|
50
|
+
* - every published list is readable (an `export const` array of
|
|
51
|
+
* literals), never mutated, lower-case ASCII, has no duplicate
|
|
52
|
+
* and no stale key;
|
|
42
53
|
* - a list a module derives from its SOURCE (a table, a default, a
|
|
43
54
|
* value it returns or assigns) covers it (COVERAGE), the source
|
|
44
55
|
* read with the validator's reader -- a const followed, a `let`
|
|
@@ -56,7 +67,7 @@
|
|
|
56
67
|
import {describe, test, expect} from "vitest";
|
|
57
68
|
import {readFileSync, readdirSync, existsSync} from "node:fs";
|
|
58
69
|
import {dirname, join} from "node:path";
|
|
59
|
-
import {fileURLToPath} from "node:url";
|
|
70
|
+
import {fileURLToPath, pathToFileURL} from "node:url";
|
|
60
71
|
|
|
61
72
|
import {I18N_KEY_FUNCTIONS} from "./i18n_key_functions.js";
|
|
62
73
|
import {
|
|
@@ -72,8 +83,12 @@ import {
|
|
|
72
83
|
marker_of,
|
|
73
84
|
key_calls,
|
|
74
85
|
key_places,
|
|
86
|
+
call_kind,
|
|
87
|
+
is_translation,
|
|
75
88
|
call_rule_violations,
|
|
89
|
+
param_flows,
|
|
76
90
|
published_lists,
|
|
91
|
+
list_mutations,
|
|
77
92
|
scan_ast,
|
|
78
93
|
lib_imports
|
|
79
94
|
} from "../scripts/validate-locales.mjs";
|
|
@@ -84,10 +99,11 @@ const MODULES = readdirSync(SRC)
|
|
|
84
99
|
.filter((f) => f.endsWith(".js") && !f.endsWith(".test.js"))
|
|
85
100
|
.sort();
|
|
86
101
|
|
|
87
|
-
/* A module's code parsed, knowing its name (for the messages)
|
|
102
|
+
/* A module's code parsed, knowing its name (for the messages) and its
|
|
103
|
+
* file (an import of a sibling is followed there) */
|
|
88
104
|
function parse_as(code, module)
|
|
89
105
|
{
|
|
90
|
-
let ast = parse_source(code);
|
|
106
|
+
let ast = parse_source(code, pathToFileURL(join(SRC, module)));
|
|
91
107
|
Object.defineProperty(ast, "module", {value: module, enumerable: false});
|
|
92
108
|
return ast;
|
|
93
109
|
}
|
|
@@ -595,16 +611,18 @@ function with_key_dropped(lists, name, key)
|
|
|
595
611
|
|
|
596
612
|
/*
|
|
597
613
|
* The FORWARDERS: the functions of the library that hand one of their
|
|
598
|
-
* parameters on to a place a key is read (key_places():
|
|
599
|
-
* `i18n`, `code` or
|
|
600
|
-
* `
|
|
601
|
-
*
|
|
602
|
-
*
|
|
603
|
-
* `
|
|
604
|
-
*
|
|
605
|
-
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
614
|
+
* parameters on to a place a key is read (key_places(): a translator
|
|
615
|
+
* call -- t(), tr(), an alias, `.call()` --, an `i18n`, `code` or
|
|
616
|
+
* `data-i18n*` value -- a property, setAttribute(), `x.i18n = ...`,
|
|
617
|
+
* `el.dataset.i18nTitle = ...` --, a key position of a function of the
|
|
618
|
+
* table): the parameter itself or what is read from it (`opts.title`,
|
|
619
|
+
* `opts?.title`, a destructured `{label}` -- in the parameter or in the
|
|
620
|
+
* body --, each `items[].name`, `arguments[1]`, `rest[0]`, a copy
|
|
621
|
+
* `{...opts}`), directly or through a `let` it is given to. What their
|
|
622
|
+
* callers give there is a key the validator reads ONLY when the
|
|
623
|
+
* function is in I18N_KEY_FUNCTIONS: up to 7.26.28 39 were not (the
|
|
624
|
+
* review named 17 of them), 6 entries lacked positions, and a literal
|
|
625
|
+
* handed to one would have gone unasked (REV-CLOUD V21.4.1).
|
|
608
626
|
*
|
|
609
627
|
* - A forwarder called by NAME -- in its module, or exported -- goes
|
|
610
628
|
* in the table, with that position.
|
|
@@ -615,16 +633,32 @@ function with_key_dropped(lists, name, key)
|
|
|
615
633
|
* - What a gobj keeps in its `priv` is its own state: a caller hands
|
|
616
634
|
* the gobj, never what its priv holds. A path through the priv of
|
|
617
635
|
* a parameter is no position of the function.
|
|
636
|
+
* - A parameter handed on through what NO position can name (V22) --
|
|
637
|
+
* a key transformed (`k.toLowerCase()`, a concatenation, a template),
|
|
638
|
+
* an iteration of `Object.entries(opts)`, a rest parameter walked or
|
|
639
|
+
* read by a computed index, `arguments` --, unmarked, is OPAQUE: the
|
|
640
|
+
* callers' keys reach no reader. It is restructured (the key handed
|
|
641
|
+
* on as it arrives, the function then in the table) or marked.
|
|
618
642
|
*
|
|
619
643
|
* The search runs to a fixpoint: a function that hands a parameter to
|
|
620
|
-
* a key position of a forwarder it found is a forwarder too.
|
|
644
|
+
* a key position of a forwarder it found is a forwarder too. Each round
|
|
645
|
+
* reads the library again with the table of the round before (a `tr`
|
|
646
|
+
* of the table is a key function, read at its positions, and not a
|
|
647
|
+
* translator); `table` is where it starts: the real table, to find what
|
|
648
|
+
* it lacks, or none at all, to find what the library ALONE says
|
|
649
|
+
* (correctness).
|
|
621
650
|
*
|
|
622
651
|
* -> {missing: [{module, name, exported, keys, line}],
|
|
623
|
-
* unmarked: [{module, name, how, pos, line}]
|
|
652
|
+
* unmarked: [{module, name, how, pos, line}],
|
|
653
|
+
* opaque: [{module, name, pos, line, text}],
|
|
654
|
+
* found: [{module, name, exported, keys}]}
|
|
624
655
|
*
|
|
625
656
|
* `missing`: what the table lacks, each function with the positions it
|
|
626
657
|
* lacks and the first line that hands one. `unmarked`: a forwarder
|
|
627
|
-
* nothing calls by name that hands a parameter on unmarked.
|
|
658
|
+
* nothing calls by name that hands a parameter on unmarked. `opaque`:
|
|
659
|
+
* a function that hands a parameter on unmarked through what no
|
|
660
|
+
* position can name. `found`: every forwarder and position the last
|
|
661
|
+
* round found, the table aside.
|
|
628
662
|
*/
|
|
629
663
|
const PRIV_PATH_RE = /^\.priv(?:[.[]|$)/;
|
|
630
664
|
|
|
@@ -639,6 +673,29 @@ function handed_params(leaf)
|
|
|
639
673
|
return [];
|
|
640
674
|
}
|
|
641
675
|
|
|
676
|
+
/* The parameters a leaf hands on through what no position can name
|
|
677
|
+
* (param_flows()), a `let` it holds followed into what it is given */
|
|
678
|
+
function opaque_params(leaf)
|
|
679
|
+
{
|
|
680
|
+
if(leaf.kind !== "unknown") {
|
|
681
|
+
return [];
|
|
682
|
+
}
|
|
683
|
+
if(!leaf.var_root) {
|
|
684
|
+
return param_flows(leaf.node, leaf.scope || []);
|
|
685
|
+
}
|
|
686
|
+
let r = var_leaves(leaf.var_root);
|
|
687
|
+
let out = [];
|
|
688
|
+
for(let l of r.leaves) {
|
|
689
|
+
if(l.kind === "unknown" && !l.var_root) {
|
|
690
|
+
out.push(...param_flows(l.node, l.scope || []));
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
for(let o of r.opaque) {
|
|
694
|
+
out.push(...param_flows(o.node, o.scope || []));
|
|
695
|
+
}
|
|
696
|
+
return out;
|
|
697
|
+
}
|
|
698
|
+
|
|
642
699
|
/* Is `fn` called by its name in the module (a call that resolves to it)? */
|
|
643
700
|
function is_called_by_name(ast, fn)
|
|
644
701
|
{
|
|
@@ -683,21 +740,70 @@ function declared_as(ast, fn)
|
|
|
683
740
|
return how;
|
|
684
741
|
}
|
|
685
742
|
|
|
743
|
+
/* A flow of param_flows() out of a gobj's OWN state: the parameter
|
|
744
|
+
* `gobj` of a gclass function (what is read from it, through a call
|
|
745
|
+
* too -- gobj_read_attr(gobj, "title") -- is its attrs, set by whoever
|
|
746
|
+
* created it), or a path through a `priv`. No caller hands a key that
|
|
747
|
+
* way. */
|
|
748
|
+
function is_gobj_state(f)
|
|
749
|
+
{
|
|
750
|
+
if(f.priv || (f.path !== null && /\.priv(?:[.[]|$)/.test(f.path))) {
|
|
751
|
+
return true;
|
|
752
|
+
}
|
|
753
|
+
let b = f.binding;
|
|
754
|
+
let p = (b.kind === "arguments")? null : b.fn.params[b.index];
|
|
755
|
+
if(p && p.type === "AssignmentPattern") {
|
|
756
|
+
p = p.left;
|
|
757
|
+
}
|
|
758
|
+
return !!p && p.type === "Identifier" && p.name === "gobj";
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/* The parameter a flow of param_flows() comes from, and what is read
|
|
762
|
+
* from it when that can be said */
|
|
763
|
+
function flow_position(f)
|
|
764
|
+
{
|
|
765
|
+
let b = f.binding;
|
|
766
|
+
if(b.kind === "arguments") {
|
|
767
|
+
return "arguments";
|
|
768
|
+
}
|
|
769
|
+
return `${b.index}${f.path || ""}`;
|
|
770
|
+
}
|
|
771
|
+
|
|
686
772
|
function find_forwarders(modules = MODULES, asts = ast_of, table = I18N_KEY_FUNCTIONS)
|
|
687
773
|
{
|
|
688
|
-
let
|
|
774
|
+
let start = table.map((e) => Object.assign({}, e, {keys: [...e.keys]}));
|
|
775
|
+
let named = new Map(); // a function node -> {by_name, how}
|
|
776
|
+
let round_table = start;
|
|
777
|
+
let found = new Map(); // "<module> <name>" -> {module, name, exported, keys}
|
|
689
778
|
let first_line = new Map(); // "<module> <name> <pos>" -> line
|
|
690
779
|
let unmarked = new Map(); // "<module>:<line> <pos>" -> {...}
|
|
691
|
-
let
|
|
780
|
+
let opaque = new Map(); // "<module>:<line> <pos>" -> {...}
|
|
781
|
+
let signature = (entries) => entries.map((e) => `${e.module} ${e.name} [${[...e.keys].sort()}]`).sort().join("\n");
|
|
782
|
+
let seen = new Set();
|
|
692
783
|
for(let round = 0; round < 32; round++) {
|
|
693
|
-
|
|
694
|
-
|
|
784
|
+
found = new Map();
|
|
785
|
+
first_line = new Map();
|
|
786
|
+
unmarked = new Map();
|
|
787
|
+
opaque = new Map();
|
|
695
788
|
for(let module of modules) {
|
|
696
789
|
let ast = asts(module);
|
|
697
790
|
let markers = markers_of(ast);
|
|
698
791
|
let exported = exported_names(ast);
|
|
792
|
+
let by_name_of = (b) => {
|
|
793
|
+
if(!named.has(b.fn)) {
|
|
794
|
+
let how = declared_as(ast, b.fn);
|
|
795
|
+
let by_name = !!b.name && (how === "declaration" || how === "variable")
|
|
796
|
+
&& (exported.has(b.name) || is_called_by_name(ast, b.fn));
|
|
797
|
+
named.set(b.fn, {by_name, how});
|
|
798
|
+
}
|
|
799
|
+
return named.get(b.fn);
|
|
800
|
+
};
|
|
699
801
|
for(let place of key_places(ast, {lib_module: module, table: round_table})) {
|
|
802
|
+
let place_line = line_of(ast, place.node.start);
|
|
700
803
|
for(let leaf of place.leaves) {
|
|
804
|
+
if(marker_of(leaf, markers)) {
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
701
807
|
for(let p of handed_params(leaf)) {
|
|
702
808
|
if(PRIV_PATH_RE.test(p.path)) {
|
|
703
809
|
continue;
|
|
@@ -705,44 +811,64 @@ function find_forwarders(modules = MODULES, asts = ast_of, table = I18N_KEY_FUNC
|
|
|
705
811
|
let b = p.binding;
|
|
706
812
|
let pos = `${b.index}${p.path}`;
|
|
707
813
|
let line = line_of(ast, leaf.node.start); // the place it is handed to
|
|
708
|
-
|
|
709
|
-
let how = declared_as(ast, b.fn);
|
|
710
|
-
let by_name = !!b.name && (how === "declaration" || how === "variable")
|
|
711
|
-
&& (exported.has(b.name) || is_called_by_name(ast, b.fn));
|
|
712
|
-
named.set(b.fn, {by_name, how});
|
|
713
|
-
}
|
|
714
|
-
let {by_name, how} = named.get(b.fn);
|
|
814
|
+
let {by_name, how} = by_name_of(b);
|
|
715
815
|
if(!by_name) {
|
|
716
|
-
|
|
717
|
-
unmarked.set(`${module}:${line} ${pos}`,
|
|
718
|
-
{module, name: b.name, how, pos, line});
|
|
719
|
-
}
|
|
816
|
+
unmarked.set(`${module}:${line} ${pos}`, {module, name: b.name, how, pos, line});
|
|
720
817
|
continue;
|
|
721
818
|
}
|
|
722
819
|
let id = `${module} ${b.name} ${pos}`;
|
|
723
820
|
if(!first_line.has(id) || line < first_line.get(id)) {
|
|
724
821
|
first_line.set(id, line);
|
|
725
822
|
}
|
|
726
|
-
let
|
|
727
|
-
if(!
|
|
728
|
-
|
|
729
|
-
|
|
823
|
+
let key = `${module} ${b.name}`;
|
|
824
|
+
if(!found.has(key)) {
|
|
825
|
+
found.set(key, {module, name: b.name, exported: exported.has(b.name), keys: []});
|
|
826
|
+
}
|
|
827
|
+
if(!found.get(key).keys.includes(pos)) {
|
|
828
|
+
found.get(key).keys.push(pos);
|
|
730
829
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
830
|
+
}
|
|
831
|
+
if(is_translation(leaf)) {
|
|
832
|
+
continue; // the rule test says what is wrong with it
|
|
833
|
+
}
|
|
834
|
+
for(let f of opaque_params(leaf)) {
|
|
835
|
+
if(is_gobj_state(f)) {
|
|
836
|
+
continue;
|
|
734
837
|
}
|
|
838
|
+
let pos = flow_position(f);
|
|
839
|
+
opaque.set(`${module}:${place_line} ${pos}`, {
|
|
840
|
+
module, name: f.binding.name, pos, line: place_line,
|
|
841
|
+
text: ast.code.slice(leaf.node.start, leaf.node.end).replace(/\s+/g, " ").slice(0, 80)
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
/* The next round's table: where it started, and what this one found */
|
|
848
|
+
let next = start.map((e) => Object.assign({}, e, {keys: [...e.keys]}));
|
|
849
|
+
for(let f of found.values()) {
|
|
850
|
+
let e = next.find((x) => x.module === f.module && x.name === f.name);
|
|
851
|
+
if(!e) {
|
|
852
|
+
next.push({module: f.module, name: f.name, exported: f.exported, keys: [...f.keys]});
|
|
853
|
+
} else {
|
|
854
|
+
for(let k of f.keys) {
|
|
855
|
+
if(!e.keys.includes(k)) {
|
|
856
|
+
e.keys.push(k);
|
|
735
857
|
}
|
|
736
858
|
}
|
|
737
859
|
}
|
|
738
860
|
}
|
|
739
|
-
|
|
861
|
+
let sig = signature(next);
|
|
862
|
+
if(sig === signature(round_table) || seen.has(sig)) {
|
|
863
|
+
round_table = next;
|
|
740
864
|
break;
|
|
741
865
|
}
|
|
866
|
+
seen.add(sig);
|
|
867
|
+
round_table = next;
|
|
742
868
|
}
|
|
743
869
|
|
|
744
870
|
let missing = [];
|
|
745
|
-
for(let entry of
|
|
871
|
+
for(let entry of round_table) {
|
|
746
872
|
let real = table.find((e) => e.module === entry.module && e.name === entry.name);
|
|
747
873
|
let keys = entry.keys.filter((k) => !real || !real.keys.includes(k));
|
|
748
874
|
if(keys.length === 0) {
|
|
@@ -752,7 +878,7 @@ function find_forwarders(modules = MODULES, asts = ast_of, table = I18N_KEY_FUNC
|
|
|
752
878
|
missing.push({module: entry.module, name: entry.name, exported: entry.exported, keys,
|
|
753
879
|
line: Math.min(...lines), in_table: !!real});
|
|
754
880
|
}
|
|
755
|
-
return {missing, unmarked: [...unmarked.values()]};
|
|
881
|
+
return {missing, unmarked: [...unmarked.values()], opaque: [...opaque.values()], found: [...found.values()]};
|
|
756
882
|
}
|
|
757
883
|
|
|
758
884
|
function forwarders_report(found)
|
|
@@ -769,6 +895,39 @@ function forwarders_report(found)
|
|
|
769
895
|
`hands its parameter [${u.pos}] to a place a key is read, unmarked: no caller can be ` +
|
|
770
896
|
`read, so mark what it is where it hands it`);
|
|
771
897
|
}
|
|
898
|
+
for(let o of found.opaque || []) {
|
|
899
|
+
out.push(`${o.module}:${o.line} ${o.name || "a function"}() hands its parameter [${o.pos}] to a place ` +
|
|
900
|
+
`a key is read through \`${o.text}\`, which no position can name: hand the key on as it ` +
|
|
901
|
+
`arrives (the function then goes in the table), or mark it where it hands it`);
|
|
902
|
+
}
|
|
903
|
+
return out;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/*
|
|
907
|
+
* The table is CORRECT: every entry, at every position, is a hand-off
|
|
908
|
+
* the library itself shows -- the search started from NO table finds
|
|
909
|
+
* it, with the same `exported`. An entry nothing forwards to would make
|
|
910
|
+
* the validator read keys where no key goes, and a wrong `exported`
|
|
911
|
+
* hides the calls of another module. (Up to 7.26.31 the test proved
|
|
912
|
+
* only that the table was complete: an extra entry passed it.)
|
|
913
|
+
*/
|
|
914
|
+
function table_errors(found, table = I18N_KEY_FUNCTIONS)
|
|
915
|
+
{
|
|
916
|
+
let out = [];
|
|
917
|
+
for(let e of table) {
|
|
918
|
+
let f = found.find((x) => x.module === e.module && x.name === e.name);
|
|
919
|
+
for(let k of e.keys) {
|
|
920
|
+
if(!f || !f.keys.includes(k)) {
|
|
921
|
+
out.push(`${e.module} ${e.name} [${k}]: no hand-off the search finds -- the function does not ` +
|
|
922
|
+
`hand that position to a place a key is read: drop it from I18N_KEY_FUNCTIONS, or ` +
|
|
923
|
+
`write the hand-off in a form the reader follows`);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
if(f && f.exported !== e.exported) {
|
|
927
|
+
out.push(`${e.module} ${e.name}: \`exported: ${e.exported}\`, and the module ` +
|
|
928
|
+
`${f.exported? "exports" : "does not export"} it`);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
772
931
|
return out;
|
|
773
932
|
}
|
|
774
933
|
|
|
@@ -818,6 +977,10 @@ describe("REV-CLOUD V21.4.1: every forwarder is in the table", () => {
|
|
|
818
977
|
expect(forwarders_report(find_forwarders())).toEqual([]);
|
|
819
978
|
});
|
|
820
979
|
|
|
980
|
+
test("REV-CLOUD V22: the table is CORRECT -- every entry is a hand-off the library shows", () => {
|
|
981
|
+
expect(table_errors(find_forwarders(MODULES, ast_of, []).found)).toEqual([]);
|
|
982
|
+
});
|
|
983
|
+
|
|
821
984
|
test("the search finds what it should, and nothing else", () => {
|
|
822
985
|
let code = [
|
|
823
986
|
`import {yui_shell_show_error} from "./shell_modals.js";`,
|
|
@@ -975,8 +1138,8 @@ describe("the reader the rule and the validator share", () => {
|
|
|
975
1138
|
].join("\n")), {lib_module: module});
|
|
976
1139
|
expect(r.marker_errors.map((e) => e.line)).toEqual([1, 2]);
|
|
977
1140
|
expect(r.marker_errors[0].why).toMatch(/is no i18n marker/);
|
|
978
|
-
expect(r.marker_errors[1].why).toMatch(
|
|
979
|
-
expect(r.marker_refs).toEqual([{lists: ["C_YUI_FOO_I18N_KEYS"], keys: ["listed"], line: 3}]);
|
|
1141
|
+
expect(r.marker_errors[1].why).toMatch(/^the i18n marker stands before `1;`, the value a variable is DECLARED with/);
|
|
1142
|
+
expect(r.marker_refs).toEqual([{lists: ["C_YUI_FOO_I18N_KEYS"], keys: ["listed"], line: 3, shapes: []}]);
|
|
980
1143
|
});
|
|
981
1144
|
|
|
982
1145
|
test("a let or var a marker stands before is HELD to it: everything it is given is read", () => {
|
|
@@ -1376,7 +1539,6 @@ describe("the published *_I18N_KEYS lists", () => {
|
|
|
1376
1539
|
let report = unpinned_keys(lists, COVERAGE.filter((e) => !added.includes(e.list)));
|
|
1377
1540
|
let reported = (name, key) => report.some((line) => line.includes(`${name}: ${JSON.stringify(key)} `));
|
|
1378
1541
|
for(let [name, key] of [
|
|
1379
|
-
["C_YUI_FORM_I18N_KEYS", "this field is required"],
|
|
1380
1542
|
["FORM_JSON_FIELD_I18N_KEYS", "invalid json"],
|
|
1381
1543
|
["SHELL_ROUTE_MAP_I18N_KEYS", "toolbar"],
|
|
1382
1544
|
["SHELL_ROUTE_MAP_I18N_KEYS", "other routes"],
|
|
@@ -1397,6 +1559,13 @@ describe("the published *_I18N_KEYS lists", () => {
|
|
|
1397
1559
|
let route_map_refs = scan_of("shell_route_map.js").marker_refs
|
|
1398
1560
|
.filter((ref) => ref.lists.includes("SHELL_ROUTE_MAP_I18N_KEYS"));
|
|
1399
1561
|
expect(route_map_refs.flatMap((ref) => ref.keys)).toContain("app");
|
|
1562
|
+
/* ...and "this field is required" since V22: field_validation_key()
|
|
1563
|
+
* returns it OR the json field's own error, and the list marker on
|
|
1564
|
+
* the `let key` that holds the call holds that key to its lists */
|
|
1565
|
+
expect(reported("C_YUI_FORM_I18N_KEYS", "this field is required")).toBe(false);
|
|
1566
|
+
let form_refs = scan_of("c_yui_form.js").marker_refs
|
|
1567
|
+
.filter((ref) => ref.lists.includes("C_YUI_FORM_I18N_KEYS"));
|
|
1568
|
+
expect(form_refs.flatMap((ref) => ref.keys)).toContain("this field is required");
|
|
1400
1569
|
|
|
1401
1570
|
for(let [name, key] of [
|
|
1402
1571
|
["C_YUI_FORM_I18N_KEYS", "this field is required"],
|
|
@@ -1446,7 +1615,7 @@ describe("the published *_I18N_KEYS lists", () => {
|
|
|
1446
1615
|
`const K = "not in it";`,
|
|
1447
1616
|
`t(/* i18n: X_I18N_KEYS */ K);`
|
|
1448
1617
|
].join("\n")), {lib_module: "x.js"});
|
|
1449
|
-
expect(r.marker_refs).toEqual([{lists: ["X_I18N_KEYS"], keys: ["not in it"], line: 3}]);
|
|
1618
|
+
expect(r.marker_refs).toEqual([{lists: ["X_I18N_KEYS"], keys: ["not in it"], line: 3, shapes: []}]);
|
|
1450
1619
|
});
|
|
1451
1620
|
});
|
|
1452
1621
|
|
|
@@ -1523,3 +1692,246 @@ describe("REV-CLOUD V20 (js3_v lists #2): a COVERAGE source is read the way the
|
|
|
1523
1692
|
/^c_yui_form\.js:\d+: C_YUI_FORM_I18N_KEYS is derived from this source, and the reader cannot follow \$input\.yui_json_error:/)]);
|
|
1524
1693
|
});
|
|
1525
1694
|
});
|
|
1695
|
+
|
|
1696
|
+
describe("REV-CLOUD V22.1: a recursive helper is read once, and never hangs the reader", () => {
|
|
1697
|
+
let module = "x.js";
|
|
1698
|
+
let head = [`import {yui_shell_show_info} from "./shell_modals.js";`];
|
|
1699
|
+
let rule = (lines) => rule_report(parse_as([...head, ...lines].join("\n"), module), module);
|
|
1700
|
+
let scan = (lines) => scan_ast(parse_as([...head, ...lines].join("\n"), module), {lib_module: module});
|
|
1701
|
+
|
|
1702
|
+
/* The review's case, verbatim: 7.26.28 read it in 12 ms, 7.26.29 in
|
|
1703
|
+
* more than two minutes (x3 per level of MAX_DEPTH), and with three
|
|
1704
|
+
* recursive ifs it never ended */
|
|
1705
|
+
let label_of = (branches) => [
|
|
1706
|
+
`function label_of(n) {`,
|
|
1707
|
+
` if(n.alias) { return label_of(n.alias); }`,
|
|
1708
|
+
` if(n.parent) { return label_of(n.parent); }`,
|
|
1709
|
+
...(branches > 2? [` if(n.other) { return label_of(n.other); }`] : []),
|
|
1710
|
+
` return n.name;`,
|
|
1711
|
+
`}`
|
|
1712
|
+
];
|
|
1713
|
+
|
|
1714
|
+
test("the review's case (two and three recursive branches): fast, and `n.name` is data", () => {
|
|
1715
|
+
for(let branches of [2, 3]) {
|
|
1716
|
+
let t0 = Date.now();
|
|
1717
|
+
let lines = [...label_of(branches),
|
|
1718
|
+
`export function f($el, node, shell) {`,
|
|
1719
|
+
` $el.textContent = t(label_of(node));`,
|
|
1720
|
+
` yui_shell_show_info(shell, label_of(node));`,
|
|
1721
|
+
`}`];
|
|
1722
|
+
let at = 1 + lines.indexOf(` yui_shell_show_info(shell, label_of(node));`) + 1;
|
|
1723
|
+
expect(rule(lines)).toEqual([`${module}:${at} yui_shell_show_info() [1]: label_of(node)`]);
|
|
1724
|
+
let marked = lines.map((l) => l.replace("yui_shell_show_info(shell, label_of(node))",
|
|
1725
|
+
"yui_shell_show_info(shell, /* i18n: data */ label_of(node))"));
|
|
1726
|
+
expect(rule(marked)).toEqual([]);
|
|
1727
|
+
expect(scan(marked).marker_errors).toEqual([]);
|
|
1728
|
+
expect(Date.now() - t0).toBeLessThan(2000);
|
|
1729
|
+
}
|
|
1730
|
+
});
|
|
1731
|
+
|
|
1732
|
+
test("a recursion is not followed, whatever it returns; a helper called on many paths is read once", () => {
|
|
1733
|
+
/* Returns only literals, through itself: the reader does not follow
|
|
1734
|
+
* a recursion, so the call is a value it cannot follow */
|
|
1735
|
+
expect(rule([`function up(n) { if(n.parent) { return up(n.parent); } return "root"; }`,
|
|
1736
|
+
`yui_shell_show_info(shell, up(node));`]))
|
|
1737
|
+
.toEqual([`${module}:3 yui_shell_show_info() [1]: up(node)`]);
|
|
1738
|
+
/* Mutual recursion, the same */
|
|
1739
|
+
expect(rule([`function a(n) { return n? b(n.x) : "from a"; }`,
|
|
1740
|
+
`function b(n) { return n? a(n.y) : "from b"; }`,
|
|
1741
|
+
`yui_shell_show_info(shell, a(node));`]).length).toBe(1);
|
|
1742
|
+
|
|
1743
|
+
/* A fan: 16 helpers, each calling the next in three branches. Read
|
|
1744
|
+
* at every call, as 7.26.29 did, it is 3^16 reads */
|
|
1745
|
+
let fan = [];
|
|
1746
|
+
for(let i = 0; i < 16; i++) {
|
|
1747
|
+
fan.push(`function h${i}(c) { if(c === 1) { return h${i + 1}(c); } ` +
|
|
1748
|
+
`if(c === 2) { return h${i + 1}(c); } return h${i + 1}(c); }`);
|
|
1749
|
+
}
|
|
1750
|
+
fan.push(`function h16(c) { return c? "deep key" : "other deep key"; }`);
|
|
1751
|
+
let t0 = Date.now();
|
|
1752
|
+
let r = scan([...fan, `t(h0(1));`]);
|
|
1753
|
+
expect(Date.now() - t0).toBeLessThan(2000);
|
|
1754
|
+
expect(r.keys.has("deep key") && r.keys.has("other deep key")).toBe(true);
|
|
1755
|
+
});
|
|
1756
|
+
});
|
|
1757
|
+
|
|
1758
|
+
describe("REV-CLOUD V22.2: what the search for forwarders sees, and what fails it", () => {
|
|
1759
|
+
let module = "x.js";
|
|
1760
|
+
let found_in = (lines) => {
|
|
1761
|
+
let ast = parse_as(lines.join("\n"), module);
|
|
1762
|
+
return find_forwarders([module], () => ast, []);
|
|
1763
|
+
};
|
|
1764
|
+
let keys_of = (found) => Object.fromEntries(found.found.map((f) => [f.name, f.keys.slice().sort()]));
|
|
1765
|
+
|
|
1766
|
+
test("the forms it follows: each one is a position of the function", () => {
|
|
1767
|
+
let found = found_in([
|
|
1768
|
+
`import i18next from "i18next";`,
|
|
1769
|
+
`import {t as tt} from "i18next";`,
|
|
1770
|
+
`const ATTR = "data-i18n-title";`,
|
|
1771
|
+
`const PROP = "data-i18n";`,
|
|
1772
|
+
`export function f_opt(gobj, opts) { return t(opts?.title); }`,
|
|
1773
|
+
`export function f_args(gobj) { return t(arguments[1]); }`,
|
|
1774
|
+
`export function f_alias(gobj, k) { const translate = t; return translate(k); }`,
|
|
1775
|
+
`export function f_import(gobj, k) { return tt(k); }`,
|
|
1776
|
+
`export function f_optcall(gobj, k) { return t?.(k); }`,
|
|
1777
|
+
`export function f_call(gobj, k) { return t.call(null, k); }`,
|
|
1778
|
+
`export function f_bind(gobj, k) { const tb = i18next.t.bind(i18next); return tb(k); }`,
|
|
1779
|
+
`export function f_destr(gobj, opts) { const {title} = opts; let {label} = opts; return [t(title), t(label)]; }`,
|
|
1780
|
+
`export function f_copy(gobj, opts) { const o = {...opts}; const p = Object.assign({}, opts); return [t(o.title), t(p.label)]; }`,
|
|
1781
|
+
`export function f_rest(gobj, ...rest) { return t(rest[0]); }`,
|
|
1782
|
+
`export function f_list(gobj, k) { return t([k, "a fallback key"]); }`,
|
|
1783
|
+
`export function f_dataset(gobj, el, k) { el.dataset.i18nTitle = k; }`,
|
|
1784
|
+
`export function f_attr(gobj, el, k) { el.setAttribute(ATTR, k); }`,
|
|
1785
|
+
`export function f_prop(gobj, k) { return {[PROP]: k}; }`,
|
|
1786
|
+
]);
|
|
1787
|
+
expect(found.opaque).toEqual([]);
|
|
1788
|
+
expect(keys_of(found)).toEqual({
|
|
1789
|
+
f_opt: ["1.title"], f_args: ["1"], f_alias: ["1"], f_import: ["1"], f_optcall: ["1"],
|
|
1790
|
+
f_call: ["1"], f_bind: ["1"], f_destr: ["1.label", "1.title"], f_copy: ["1.label", "1.title"],
|
|
1791
|
+
f_rest: ["1"], f_list: ["1"], f_dataset: ["2"], f_attr: ["2"], f_prop: ["1"]
|
|
1792
|
+
});
|
|
1793
|
+
let r = scan_ast(parse_as(`export function f(gobj, k) { return t([k, "a fallback key"]); }`, module),
|
|
1794
|
+
{lib_module: module});
|
|
1795
|
+
expect(r.keys.has("a fallback key")).toBe(true); // an array given to t() is a list of KEYS
|
|
1796
|
+
});
|
|
1797
|
+
|
|
1798
|
+
test("what no position can name FAILS, named, with the file and the line -- unless it is marked", () => {
|
|
1799
|
+
let lines = [
|
|
1800
|
+
`export function o_lower(gobj, k) { return t(k.toLowerCase()); }`,
|
|
1801
|
+
`export function o_entries(gobj, opts) { Object.entries(opts).forEach(([k, v]) => t(v)); }`,
|
|
1802
|
+
`export function o_rest(gobj, ...rest) { rest.forEach((x) => t(x)); }`,
|
|
1803
|
+
`export function o_concat(gobj, k) { return {i18n: "prefix " + k}; }`,
|
|
1804
|
+
`export function o_index(gobj, i) { return t(arguments[i]); }`,
|
|
1805
|
+
`export function o_array(gobj, [a, b]) { return t(a); }`,
|
|
1806
|
+
`export function o_let(gobj, k) { let x = "a"; x += k; return t(x); }`,
|
|
1807
|
+
`export function o_own(gobj) { return t(gobj_read_attr(gobj, "title").trim()); }`, // the gobj's own state
|
|
1808
|
+
`export function o_priv(gobj, ctx) { return t(ctx.gobj.priv.label.trim()); }`, // a priv
|
|
1809
|
+
];
|
|
1810
|
+
let found = found_in(lines);
|
|
1811
|
+
expect(found.opaque.map((o) => `${o.name} ${o.line} ${o.pos}`)).toEqual([
|
|
1812
|
+
"o_lower 1 1", "o_entries 2 1", "o_rest 3 1", "o_concat 4 1", "o_index 5 arguments",
|
|
1813
|
+
"o_array 6 1", "o_let 7 1"
|
|
1814
|
+
]);
|
|
1815
|
+
expect(forwarders_report(found)[0]).toBe("x.js:1 o_lower() hands its parameter [1] to a place a key " +
|
|
1816
|
+
"is read through `k.toLowerCase()`, which no position can name: hand the key on as it arrives (the " +
|
|
1817
|
+
"function then goes in the table), or mark it where it hands it");
|
|
1818
|
+
/* ...and marked where it hands it, it passes */
|
|
1819
|
+
let marked = found_in([
|
|
1820
|
+
`export function o_lower(gobj, k) { return t(/* i18n: data */ k.toLowerCase()); }`,
|
|
1821
|
+
`export function o_entries(gobj, opts) { Object.entries(opts).forEach(([k, v]) => t(/* i18n: data */ v)); }`
|
|
1822
|
+
]);
|
|
1823
|
+
expect(marked.opaque).toEqual([]);
|
|
1824
|
+
});
|
|
1825
|
+
|
|
1826
|
+
test("the table is checked for CORRECTNESS, not only for completeness: an extra entry fails", () => {
|
|
1827
|
+
let found = find_forwarders(MODULES, ast_of, []).found;
|
|
1828
|
+
let extra = [...I18N_KEY_FUNCTIONS.filter((e) => e.name !== "yui_shell_show_info"),
|
|
1829
|
+
{module: "shell_modals.js", name: "yui_shell_show_info", exported: true, keys: ["1", "2.extra"]},
|
|
1830
|
+
{module: "shell_modals.js", name: "yui_shell_show_warning", exported: false, keys: ["1"]},
|
|
1831
|
+
{module: "c_yui_nav.js", name: "no_such_forwarder", exported: false, keys: ["0"]}];
|
|
1832
|
+
let errors = table_errors(found, extra);
|
|
1833
|
+
expect(errors).toEqual(expect.arrayContaining([
|
|
1834
|
+
expect.stringMatching(/^shell_modals\.js yui_shell_show_info \[2\.extra\]: no hand-off the search finds/),
|
|
1835
|
+
"shell_modals.js yui_shell_show_warning: `exported: false`, and the module exports it",
|
|
1836
|
+
expect.stringMatching(/^c_yui_nav\.js no_such_forwarder \[0\]: no hand-off the search finds/)
|
|
1837
|
+
]));
|
|
1838
|
+
/* ...as the real table was, before V22: an action label that went
|
|
1839
|
+
* through a DOM property, a flag that hid an export */
|
|
1840
|
+
expect(table_errors(found)).toEqual([]);
|
|
1841
|
+
});
|
|
1842
|
+
});
|
|
1843
|
+
|
|
1844
|
+
describe("REV-CLOUD V22.3-4: the data marker hides no key; one notion of a translator", () => {
|
|
1845
|
+
let module = "x.js";
|
|
1846
|
+
let head = [`import {yui_shell_show_info} from "./shell_modals.js";`,
|
|
1847
|
+
`import {t as tt} from "i18next";`];
|
|
1848
|
+
let scan = (lines) => scan_ast(parse_as([...head, ...lines].join("\n"), module), {lib_module: module});
|
|
1849
|
+
let rule = (lines) => rule_report(parse_as([...head, ...lines].join("\n"), module), module);
|
|
1850
|
+
let whys = (r) => r.marker_errors.map((e) => `${e.line}: ${e.why}`);
|
|
1851
|
+
|
|
1852
|
+
test("a data marker on a call that MIXES a key with data fails: split the branches", () => {
|
|
1853
|
+
let r = scan([`function label(kw) { return kw.ok? "a mixed key" : kw.comment; }`,
|
|
1854
|
+
`yui_shell_show_info(shell, /* i18n: data */ label(kw));`,
|
|
1855
|
+
`yui_shell_show_info(shell, /* i18n: data */ kw.comment || "a split key");`]);
|
|
1856
|
+
expect(whys(r)).toEqual([expect.stringMatching(
|
|
1857
|
+
/^4: marked as data, but `label\(kw\)` can also return the key "a mixed key": .* Split the branches/)]);
|
|
1858
|
+
expect(r.keys.has("a split key")).toBe(true);
|
|
1859
|
+
/* Under a list marker, that key is asked for and must be in the list */
|
|
1860
|
+
let l = scan([`export const X_I18N_KEYS = ["in it"];`,
|
|
1861
|
+
`function label(kw) { return kw.ok? "a mixed key" : kw.comment; }`,
|
|
1862
|
+
`yui_shell_show_info(shell, /* i18n: X_I18N_KEYS */ label(kw));`]);
|
|
1863
|
+
expect(l.marker_refs).toEqual([{lists: ["X_I18N_KEYS"], keys: ["a mixed key"], line: 5, shapes: []}]);
|
|
1864
|
+
});
|
|
1865
|
+
|
|
1866
|
+
test("a data marker on an array of strings fails: it is no DOM", () => {
|
|
1867
|
+
let r = scan([`yui_shell_show_info(shell, /* i18n: data */ ["a key", "another key"]);`,
|
|
1868
|
+
`yui_shell_show_info(shell, /* i18n: data */ ["div", {}, "a spec is DOM"]);`]);
|
|
1869
|
+
expect(whys(r)).toEqual([expect.stringMatching(
|
|
1870
|
+
/^3: marked as data, but what it marks is an array of string literals \("a key", "another key"\), which is no DOM/)]);
|
|
1871
|
+
expect(rule([`yui_shell_show_info(shell, ["a key", "another key"]);`]))
|
|
1872
|
+
.toEqual([`${module}:3 yui_shell_show_info() [1]: ["a key", "another key"]`]);
|
|
1873
|
+
});
|
|
1874
|
+
|
|
1875
|
+
test("a marker attaches through a parenthesis, and one that attaches nowhere says what it stands before", () => {
|
|
1876
|
+
let r = scan([`yui_shell_show_info(shell, /* i18n: data */ (kw.comment || "a fallback in parens"));`,
|
|
1877
|
+
`function f(kw) { return t(/* i18n: data */ (kw.a || kw.b)); }`,
|
|
1878
|
+
`let x = /* i18n: data */ kw.comment;`,
|
|
1879
|
+
`x = /* i18n: data */ kw.other;`,
|
|
1880
|
+
`function g() { return /* i18n: data */ kw.third; }`,
|
|
1881
|
+
`t("a" + /* i18n: data */ kw.fourth);`]);
|
|
1882
|
+
expect(r.keys.has("a fallback in parens")).toBe(true);
|
|
1883
|
+
expect(whys(r)).toEqual([
|
|
1884
|
+
expect.stringMatching(/^5: the i18n marker stands before `kw\.comment;`, the value a variable is DECLARED with/),
|
|
1885
|
+
expect.stringMatching(/^6: the i18n marker stands before `kw\.other;`, a value ASSIGNED/),
|
|
1886
|
+
expect.stringMatching(/^7: the i18n marker stands before `kw\.third; }`, a value RETURNED/),
|
|
1887
|
+
expect.stringMatching(/^8: the i18n marker stands before `kw\.fourth\);`, inside a concatenation/)
|
|
1888
|
+
]);
|
|
1889
|
+
});
|
|
1890
|
+
|
|
1891
|
+
test("a translated text at a key position fails, MARKED too, whatever the translator is called", () => {
|
|
1892
|
+
let lines = [
|
|
1893
|
+
`const translate = t;`,
|
|
1894
|
+
`function tr(k) { return t(k); }`,
|
|
1895
|
+
`yui_shell_show_info(shell, /* i18n: data */ tr("saved"));`,
|
|
1896
|
+
`yui_shell_show_info(shell, /* i18n: data */ translate("saved"));`,
|
|
1897
|
+
`yui_shell_show_info(shell, /* i18n: data */ tt("saved"));`,
|
|
1898
|
+
`yui_shell_show_info(shell, /* i18n: data */ t?.("saved"));`,
|
|
1899
|
+
`yui_shell_show_info(shell, /* i18n: data */ t.call(null, "saved"));`,
|
|
1900
|
+
`yui_shell_show_info(shell, tr("saved"));`
|
|
1901
|
+
];
|
|
1902
|
+
expect(rule(lines)).toEqual([
|
|
1903
|
+
[5, `tr("saved")`], [6, `translate("saved")`], [7, `tt("saved")`], [8, `t?.("saved")`],
|
|
1904
|
+
[9, `t.call(null, "saved")`], [10, `tr("saved")`]
|
|
1905
|
+
].map(([n, text]) => `${module}:${n} yui_shell_show_info() [1]: ${text} -- a TRANSLATED text where a key goes: hand the key`));
|
|
1906
|
+
});
|
|
1907
|
+
|
|
1908
|
+
test("a key function is read however it is called: an alias, a map, .call(), .apply()", () => {
|
|
1909
|
+
let r = scan([`const show = yui_shell_show_info;`,
|
|
1910
|
+
`const API = {info: yui_shell_show_info};`,
|
|
1911
|
+
`show(shell, "an alias key");`,
|
|
1912
|
+
`API.info(shell, "a map key");`,
|
|
1913
|
+
`yui_shell_show_info.call(null, shell, "a call key");`,
|
|
1914
|
+
`yui_shell_show_info.apply(null, [shell, "an apply key"]);`]);
|
|
1915
|
+
for(let k of ["an alias key", "a map key", "a call key", "an apply key"]) {
|
|
1916
|
+
expect(r.keys.has(k), k).toBe(true);
|
|
1917
|
+
}
|
|
1918
|
+
expect(rule([`const show = yui_shell_show_info;`, `let k = "a let key";`, `show(shell, k);`]))
|
|
1919
|
+
.toEqual([`${module}:5 yui_shell_show_info() [1]: k`]);
|
|
1920
|
+
});
|
|
1921
|
+
|
|
1922
|
+
test("REV-CLOUD V22.5: no published list of the library is a let, and none is mutated", () => {
|
|
1923
|
+
let wrong = [];
|
|
1924
|
+
for(let module of MODULES) {
|
|
1925
|
+
let ast = ast_of(module);
|
|
1926
|
+
for(let e of list_mutations(ast, published_lists(ast).lists)) {
|
|
1927
|
+
wrong.push(`${module}:${e.line}: ${e.why}`);
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
expect(wrong).toEqual([]);
|
|
1931
|
+
let r = published_lists(parse_source(`export let A_I18N_KEYS = ["a"];`));
|
|
1932
|
+
expect(r.errors).toEqual(["A_I18N_KEYS: declared with let, which can be given other keys no reader sees: " +
|
|
1933
|
+
"a published list is an `export const` array of literals"]);
|
|
1934
|
+
expect(list_mutations(parse_source(`export const B_I18N_KEYS = ["b"];\nB_I18N_KEYS.push("c");`),
|
|
1935
|
+
[{name: "B_I18N_KEYS", keys: ["b"]}]).map((e) => e.line)).toEqual([2]);
|
|
1936
|
+
});
|
|
1937
|
+
});
|