@yuneta/gobj-ui 7.23.70 → 7.23.72
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/dist/gobj-ui.cjs.js +53 -1
- package/dist/gobj-ui.es.js +53 -1
- package/package.json +1 -1
- package/src/c_yui_form.js +8 -2
- package/src/c_yui_treedb_topic_with_form.js +2 -0
- package/src/form_field_readonly.js +61 -0
- package/src/form_field_readonly.test.js +50 -0
- package/src/priv_declared.test.js +212 -0
package/dist/gobj-ui.cjs.js
CHANGED
|
@@ -6403,6 +6403,57 @@ function plan_toolbar(wanted) {
|
|
|
6403
6403
|
};
|
|
6404
6404
|
}
|
|
6405
6405
|
//#endregion
|
|
6406
|
+
//#region src/form_field_readonly.js
|
|
6407
|
+
/***********************************************************************
|
|
6408
|
+
* form_field_readonly.js
|
|
6409
|
+
*
|
|
6410
|
+
* Is this field of the form read-only?
|
|
6411
|
+
*
|
|
6412
|
+
* Pure, so the rule can be tested without a DOM: the gclass turns
|
|
6413
|
+
* the answer into `readonly`/`disabled` on a control, this decides
|
|
6414
|
+
* the answer. Same split as form_toolbar_plan.js and
|
|
6415
|
+
* treedb_write_plan.js.
|
|
6416
|
+
*
|
|
6417
|
+
* The rule it exists to state ONCE: **`writable` governs the WRITE
|
|
6418
|
+
* of a column's VALUE, and an fkey is not written — it is LINKED.**
|
|
6419
|
+
* An fkey column is normally declared with no `writable` flag at
|
|
6420
|
+
* all (`treedb_authzs`'s `users.roles` is `['fkey']`, and so is
|
|
6421
|
+
* almost every fkey in the tree), so reading "not writable" as "not
|
|
6422
|
+
* editable" takes away the only way to link a record to its parent
|
|
6423
|
+
* — with `users.roles`, the only way to give a person a role.
|
|
6424
|
+
*
|
|
6425
|
+
* The other half of this rule is already written down in
|
|
6426
|
+
* c_yui_treedb_topic_with_form.js, where the record travels back:
|
|
6427
|
+
* the form sends "the writable cols, the fkeys (a link is edited by
|
|
6428
|
+
* linking) and the pkey". The two halves have to agree, and when
|
|
6429
|
+
* they did not, the form disabled the control whose value the save
|
|
6430
|
+
* path was still waiting for.
|
|
6431
|
+
*
|
|
6432
|
+
* A `file` column is an fkey too (flagged `['fkey','file']`) and is
|
|
6433
|
+
* NOT covered: it answers `type: "file"`, its bytes travel beside
|
|
6434
|
+
* the record, and the SDK declares as legal a `file` column that
|
|
6435
|
+
* only a load fills.
|
|
6436
|
+
*
|
|
6437
|
+
* Copyright (c) 2026, ArtGins.
|
|
6438
|
+
* All Rights Reserved.
|
|
6439
|
+
***********************************************************************/
|
|
6440
|
+
/***************************************************************
|
|
6441
|
+
* field_is_readonly(form_readonly, field_desc) -> boolean
|
|
6442
|
+
*
|
|
6443
|
+
* form_readonly the FORM is read-only: a record opened to be
|
|
6444
|
+
* looked at, or a store nobody may write. It is
|
|
6445
|
+
* a property of the opening, not of the schema,
|
|
6446
|
+
* and it wins over everything.
|
|
6447
|
+
* field_desc as treedb_get_field_desc() answers it
|
|
6448
|
+
* (`type`, `is_writable`).
|
|
6449
|
+
***************************************************************/
|
|
6450
|
+
function field_is_readonly(form_readonly, field_desc) {
|
|
6451
|
+
if (form_readonly) return true;
|
|
6452
|
+
let desc = field_desc || {};
|
|
6453
|
+
if (desc.type === "fkey") return false;
|
|
6454
|
+
return !desc.is_writable;
|
|
6455
|
+
}
|
|
6456
|
+
//#endregion
|
|
6406
6457
|
//#region src/yui_theme.js
|
|
6407
6458
|
/***********************************************************************
|
|
6408
6459
|
* yui_theme.js
|
|
@@ -7215,7 +7266,7 @@ function build_form_field_conf(gobj, field_desc) {
|
|
|
7215
7266
|
placeholder: "",
|
|
7216
7267
|
options: [],
|
|
7217
7268
|
extras: {},
|
|
7218
|
-
readonly: (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "readonly")
|
|
7269
|
+
readonly: field_is_readonly((0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "readonly"), field_desc),
|
|
7219
7270
|
required: field_desc.is_required,
|
|
7220
7271
|
hidden: field_desc.is_hidden,
|
|
7221
7272
|
default_value: field_desc.default_value
|
|
@@ -25919,6 +25970,7 @@ function ac_paste_rows(gobj, event, kw, src) {
|
|
|
25919
25970
|
* fkeys, times and numbers). Route by the form's mode.
|
|
25920
25971
|
************************************************************/
|
|
25921
25972
|
function ac_form_save_record(gobj, event, kw, src) {
|
|
25973
|
+
let priv = gobj.priv;
|
|
25922
25974
|
if (refuse_if_readonly$2(gobj, event)) return -1;
|
|
25923
25975
|
let mode = (0, _yuneta_gobj_js.gobj_read_str_attr)(src, "form_mode");
|
|
25924
25976
|
if (priv.reading_files) {
|
package/dist/gobj-ui.es.js
CHANGED
|
@@ -6398,6 +6398,57 @@ function plan_toolbar(wanted) {
|
|
|
6398
6398
|
};
|
|
6399
6399
|
}
|
|
6400
6400
|
//#endregion
|
|
6401
|
+
//#region src/form_field_readonly.js
|
|
6402
|
+
/***********************************************************************
|
|
6403
|
+
* form_field_readonly.js
|
|
6404
|
+
*
|
|
6405
|
+
* Is this field of the form read-only?
|
|
6406
|
+
*
|
|
6407
|
+
* Pure, so the rule can be tested without a DOM: the gclass turns
|
|
6408
|
+
* the answer into `readonly`/`disabled` on a control, this decides
|
|
6409
|
+
* the answer. Same split as form_toolbar_plan.js and
|
|
6410
|
+
* treedb_write_plan.js.
|
|
6411
|
+
*
|
|
6412
|
+
* The rule it exists to state ONCE: **`writable` governs the WRITE
|
|
6413
|
+
* of a column's VALUE, and an fkey is not written — it is LINKED.**
|
|
6414
|
+
* An fkey column is normally declared with no `writable` flag at
|
|
6415
|
+
* all (`treedb_authzs`'s `users.roles` is `['fkey']`, and so is
|
|
6416
|
+
* almost every fkey in the tree), so reading "not writable" as "not
|
|
6417
|
+
* editable" takes away the only way to link a record to its parent
|
|
6418
|
+
* — with `users.roles`, the only way to give a person a role.
|
|
6419
|
+
*
|
|
6420
|
+
* The other half of this rule is already written down in
|
|
6421
|
+
* c_yui_treedb_topic_with_form.js, where the record travels back:
|
|
6422
|
+
* the form sends "the writable cols, the fkeys (a link is edited by
|
|
6423
|
+
* linking) and the pkey". The two halves have to agree, and when
|
|
6424
|
+
* they did not, the form disabled the control whose value the save
|
|
6425
|
+
* path was still waiting for.
|
|
6426
|
+
*
|
|
6427
|
+
* A `file` column is an fkey too (flagged `['fkey','file']`) and is
|
|
6428
|
+
* NOT covered: it answers `type: "file"`, its bytes travel beside
|
|
6429
|
+
* the record, and the SDK declares as legal a `file` column that
|
|
6430
|
+
* only a load fills.
|
|
6431
|
+
*
|
|
6432
|
+
* Copyright (c) 2026, ArtGins.
|
|
6433
|
+
* All Rights Reserved.
|
|
6434
|
+
***********************************************************************/
|
|
6435
|
+
/***************************************************************
|
|
6436
|
+
* field_is_readonly(form_readonly, field_desc) -> boolean
|
|
6437
|
+
*
|
|
6438
|
+
* form_readonly the FORM is read-only: a record opened to be
|
|
6439
|
+
* looked at, or a store nobody may write. It is
|
|
6440
|
+
* a property of the opening, not of the schema,
|
|
6441
|
+
* and it wins over everything.
|
|
6442
|
+
* field_desc as treedb_get_field_desc() answers it
|
|
6443
|
+
* (`type`, `is_writable`).
|
|
6444
|
+
***************************************************************/
|
|
6445
|
+
function field_is_readonly(form_readonly, field_desc) {
|
|
6446
|
+
if (form_readonly) return true;
|
|
6447
|
+
let desc = field_desc || {};
|
|
6448
|
+
if (desc.type === "fkey") return false;
|
|
6449
|
+
return !desc.is_writable;
|
|
6450
|
+
}
|
|
6451
|
+
//#endregion
|
|
6401
6452
|
//#region src/yui_theme.js
|
|
6402
6453
|
/***********************************************************************
|
|
6403
6454
|
* yui_theme.js
|
|
@@ -7210,7 +7261,7 @@ function build_form_field_conf(gobj, field_desc) {
|
|
|
7210
7261
|
placeholder: "",
|
|
7211
7262
|
options: [],
|
|
7212
7263
|
extras: {},
|
|
7213
|
-
readonly: gobj_read_bool_attr(gobj, "readonly")
|
|
7264
|
+
readonly: field_is_readonly(gobj_read_bool_attr(gobj, "readonly"), field_desc),
|
|
7214
7265
|
required: field_desc.is_required,
|
|
7215
7266
|
hidden: field_desc.is_hidden,
|
|
7216
7267
|
default_value: field_desc.default_value
|
|
@@ -25914,6 +25965,7 @@ function ac_paste_rows(gobj, event, kw, src) {
|
|
|
25914
25965
|
* fkeys, times and numbers). Route by the form's mode.
|
|
25915
25966
|
************************************************************/
|
|
25916
25967
|
function ac_form_save_record(gobj, event, kw, src) {
|
|
25968
|
+
let priv = gobj.priv;
|
|
25917
25969
|
if (refuse_if_readonly$2(gobj, event)) return -1;
|
|
25918
25970
|
let mode = gobj_read_str_attr(src, "form_mode");
|
|
25919
25971
|
if (priv.reading_files) {
|
package/package.json
CHANGED
package/src/c_yui_form.js
CHANGED
|
@@ -65,6 +65,7 @@ import {
|
|
|
65
65
|
import "./c_yui_form.css";
|
|
66
66
|
import {attach_clear, refresh_clear} from "./yui_inputs.js";
|
|
67
67
|
import {plan_toolbar, DEFAULT_TOOLBAR} from "./form_toolbar_plan.js";
|
|
68
|
+
import {field_is_readonly} from "./form_field_readonly.js";
|
|
68
69
|
/* Read at field-build time only — deliberately NOT watched: the hosting
|
|
69
70
|
* dialog rebuilds the form on every open, and re-rendering a form under
|
|
70
71
|
* the user mid-edit would throw away what they typed. */
|
|
@@ -601,8 +602,13 @@ function build_form_field_conf(gobj, field_desc)
|
|
|
601
602
|
extras: {}, // extra html input/textarea attributes
|
|
602
603
|
/* A form opened to LOOK at a record has no editable field, and
|
|
603
604
|
* that is a property of the opening, not of the schema: the same
|
|
604
|
-
* col is writable when the same form is opened to edit.
|
|
605
|
-
|
|
605
|
+
* col is writable when the same form is opened to edit. And an
|
|
606
|
+
* fkey is editable WITHOUT `writable`, because a link is not
|
|
607
|
+
* written, it is linked -- the rule and its why live in
|
|
608
|
+
* form_field_readonly.js. */
|
|
609
|
+
readonly: field_is_readonly(
|
|
610
|
+
gobj_read_bool_attr(gobj, "readonly"), field_desc
|
|
611
|
+
),
|
|
606
612
|
required: field_desc.is_required,
|
|
607
613
|
hidden: field_desc.is_hidden,
|
|
608
614
|
default_value: field_desc.default_value,
|
|
@@ -3479,6 +3479,8 @@ function ac_paste_rows(gobj, event, kw, src)
|
|
|
3479
3479
|
************************************************************/
|
|
3480
3480
|
function ac_form_save_record(gobj, event, kw, src)
|
|
3481
3481
|
{
|
|
3482
|
+
let priv = gobj.priv;
|
|
3483
|
+
|
|
3482
3484
|
if(refuse_if_readonly(gobj, event)) {
|
|
3483
3485
|
return -1; /* Error already logged */
|
|
3484
3486
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* form_field_readonly.js
|
|
3
|
+
*
|
|
4
|
+
* Is this field of the form read-only?
|
|
5
|
+
*
|
|
6
|
+
* Pure, so the rule can be tested without a DOM: the gclass turns
|
|
7
|
+
* the answer into `readonly`/`disabled` on a control, this decides
|
|
8
|
+
* the answer. Same split as form_toolbar_plan.js and
|
|
9
|
+
* treedb_write_plan.js.
|
|
10
|
+
*
|
|
11
|
+
* The rule it exists to state ONCE: **`writable` governs the WRITE
|
|
12
|
+
* of a column's VALUE, and an fkey is not written — it is LINKED.**
|
|
13
|
+
* An fkey column is normally declared with no `writable` flag at
|
|
14
|
+
* all (`treedb_authzs`'s `users.roles` is `['fkey']`, and so is
|
|
15
|
+
* almost every fkey in the tree), so reading "not writable" as "not
|
|
16
|
+
* editable" takes away the only way to link a record to its parent
|
|
17
|
+
* — with `users.roles`, the only way to give a person a role.
|
|
18
|
+
*
|
|
19
|
+
* The other half of this rule is already written down in
|
|
20
|
+
* c_yui_treedb_topic_with_form.js, where the record travels back:
|
|
21
|
+
* the form sends "the writable cols, the fkeys (a link is edited by
|
|
22
|
+
* linking) and the pkey". The two halves have to agree, and when
|
|
23
|
+
* they did not, the form disabled the control whose value the save
|
|
24
|
+
* path was still waiting for.
|
|
25
|
+
*
|
|
26
|
+
* A `file` column is an fkey too (flagged `['fkey','file']`) and is
|
|
27
|
+
* NOT covered: it answers `type: "file"`, its bytes travel beside
|
|
28
|
+
* the record, and the SDK declares as legal a `file` column that
|
|
29
|
+
* only a load fills.
|
|
30
|
+
*
|
|
31
|
+
* Copyright (c) 2026, ArtGins.
|
|
32
|
+
* All Rights Reserved.
|
|
33
|
+
***********************************************************************/
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
/***************************************************************
|
|
37
|
+
* field_is_readonly(form_readonly, field_desc) -> boolean
|
|
38
|
+
*
|
|
39
|
+
* form_readonly the FORM is read-only: a record opened to be
|
|
40
|
+
* looked at, or a store nobody may write. It is
|
|
41
|
+
* a property of the opening, not of the schema,
|
|
42
|
+
* and it wins over everything.
|
|
43
|
+
* field_desc as treedb_get_field_desc() answers it
|
|
44
|
+
* (`type`, `is_writable`).
|
|
45
|
+
***************************************************************/
|
|
46
|
+
function field_is_readonly(form_readonly, field_desc)
|
|
47
|
+
{
|
|
48
|
+
if(form_readonly) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let desc = field_desc || {};
|
|
53
|
+
if(desc.type === "fkey") {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return !desc.is_writable;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
export {field_is_readonly};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {describe, it, expect} from "vitest";
|
|
2
|
+
import {field_is_readonly} from "./form_field_readonly.js";
|
|
3
|
+
|
|
4
|
+
describe("field_is_readonly", () => {
|
|
5
|
+
|
|
6
|
+
it("a form opened to LOOK has no editable field, fkey included", () => {
|
|
7
|
+
/* A record opened to be read: the schema does not enter into it. */
|
|
8
|
+
expect(field_is_readonly(true, {type: "fkey", is_writable: false}))
|
|
9
|
+
.toBe(true);
|
|
10
|
+
expect(field_is_readonly(true, {type: "fkey", is_writable: true}))
|
|
11
|
+
.toBe(true);
|
|
12
|
+
expect(field_is_readonly(true, {type: "string", is_writable: true}))
|
|
13
|
+
.toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("an fkey is EDITABLE without `writable`: a link is edited by linking", () => {
|
|
17
|
+
/* `treedb_authzs`'s `users.roles` is declared `['fkey']` and
|
|
18
|
+
* nothing else -- as almost every fkey in the tree is. Reading
|
|
19
|
+
* that as "not editable" left a user with no way to be given a
|
|
20
|
+
* role, while the save path was still sending the column. */
|
|
21
|
+
expect(field_is_readonly(false, {type: "fkey", is_writable: false}))
|
|
22
|
+
.toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("a plain column still needs `writable`", () => {
|
|
26
|
+
expect(field_is_readonly(false, {type: "string", is_writable: false}))
|
|
27
|
+
.toBe(true);
|
|
28
|
+
expect(field_is_readonly(false, {type: "string", is_writable: true}))
|
|
29
|
+
.toBe(false);
|
|
30
|
+
expect(field_is_readonly(false, {type: "enum", is_writable: false}))
|
|
31
|
+
.toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("a `file` column is NOT covered by the fkey exemption", () => {
|
|
35
|
+
/* It is an fkey (`['fkey','file']`) but answers `type: "file"`,
|
|
36
|
+
* and the SDK declares as legal a `file` column that only a load
|
|
37
|
+
* fills. */
|
|
38
|
+
expect(field_is_readonly(false, {type: "file", is_writable: false, is_file: true}))
|
|
39
|
+
.toBe(true);
|
|
40
|
+
expect(field_is_readonly(false, {type: "file", is_writable: true, is_file: true}))
|
|
41
|
+
.toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("no descriptor is read-only, not editable", () => {
|
|
45
|
+
/* Nothing known about a field is not a reason to let it be
|
|
46
|
+
* written. */
|
|
47
|
+
expect(field_is_readonly(false, undefined)).toBe(true);
|
|
48
|
+
expect(field_is_readonly(false, {})).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* priv_declared.test.js
|
|
3
|
+
*
|
|
4
|
+
* Every function that reads `priv` must have declared it.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS IS A TEST AND NOT A REVIEW HABIT: a missing
|
|
7
|
+
* `let priv = gobj.priv;` is a ReferenceError that throws only
|
|
8
|
+
* when its LINE runs, and the line that reads `priv` is usually
|
|
9
|
+
* the first line of a path nobody walks every day. One shipped in
|
|
10
|
+
* `ac_form_save_record()` (7.23.64, the "form is busy while it
|
|
11
|
+
* reads the picked files" guard) and it meant that for three
|
|
12
|
+
* releases NO record could be saved from a treedb form dialog, in
|
|
13
|
+
* any consumer — the record travelled, the action threw before
|
|
14
|
+
* writing it, and the dialog just stayed open.
|
|
15
|
+
*
|
|
16
|
+
* The rule is the one every gclass of this repo already follows:
|
|
17
|
+
* the function that uses `priv` opens with `let priv = gobj.priv;`,
|
|
18
|
+
* or takes it as an argument, or is nested inside one that did.
|
|
19
|
+
*
|
|
20
|
+
* Copyright (c) 2026, ArtGins.
|
|
21
|
+
* All Rights Reserved.
|
|
22
|
+
***********************************************************************/
|
|
23
|
+
import {describe, it, expect} from "vitest";
|
|
24
|
+
import {readdirSync, readFileSync} from "node:fs";
|
|
25
|
+
import {dirname, join} from "node:path";
|
|
26
|
+
import {fileURLToPath} from "node:url";
|
|
27
|
+
|
|
28
|
+
const SRC = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
/***************************************************************
|
|
32
|
+
* The source with its comments blanked out, line count kept.
|
|
33
|
+
*
|
|
34
|
+
* A doc block that SHOWS the idiom (`priv.theme_observer = ...`)
|
|
35
|
+
* is not a use of it, and every early version of this check
|
|
36
|
+
* reported four of those before the one real bug.
|
|
37
|
+
***************************************************************/
|
|
38
|
+
function without_comments(src)
|
|
39
|
+
{
|
|
40
|
+
let out = "";
|
|
41
|
+
let i = 0;
|
|
42
|
+
let state = "code"; /* code | line | block | s | d | t */
|
|
43
|
+
|
|
44
|
+
while(i < src.length) {
|
|
45
|
+
const c = src[i];
|
|
46
|
+
const d = src[i + 1];
|
|
47
|
+
|
|
48
|
+
if(state === "code") {
|
|
49
|
+
if(c === "/" && d === "/") {
|
|
50
|
+
state = "line";
|
|
51
|
+
i += 2;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if(c === "/" && d === "*") {
|
|
55
|
+
state = "block";
|
|
56
|
+
out += " ";
|
|
57
|
+
i += 2;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if(c === "'") {
|
|
61
|
+
state = "s";
|
|
62
|
+
} else if(c === '"') {
|
|
63
|
+
state = "d";
|
|
64
|
+
} else if(c === "`") {
|
|
65
|
+
state = "t";
|
|
66
|
+
}
|
|
67
|
+
out += c;
|
|
68
|
+
i++;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if(state === "line") {
|
|
73
|
+
if(c === "\n") {
|
|
74
|
+
state = "code";
|
|
75
|
+
out += c;
|
|
76
|
+
}
|
|
77
|
+
i++;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if(state === "block") {
|
|
82
|
+
if(c === "*" && d === "/") {
|
|
83
|
+
state = "code";
|
|
84
|
+
out += " ";
|
|
85
|
+
i += 2;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
out += (c === "\n") ? "\n" : " ";
|
|
89
|
+
i++;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* Inside a string: it ends at its own quote, and a backslash
|
|
94
|
+
* escapes whatever follows. */
|
|
95
|
+
if(c === "\\") {
|
|
96
|
+
out += " ";
|
|
97
|
+
i += 2;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if((state === "s" && c === "'") || (state === "d" && c === '"')
|
|
101
|
+
|| (state === "t" && c === "`")) {
|
|
102
|
+
state = "code";
|
|
103
|
+
}
|
|
104
|
+
out += c;
|
|
105
|
+
i++;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
/***************************************************************
|
|
113
|
+
* Every top-level function of a file, as {name, line, params,
|
|
114
|
+
* body}. A nested function belongs to the body of the one that
|
|
115
|
+
* holds it, which is what lets it use the parent's `priv`.
|
|
116
|
+
***************************************************************/
|
|
117
|
+
function top_level_functions(src)
|
|
118
|
+
{
|
|
119
|
+
const lines = src.split("\n");
|
|
120
|
+
const starts = [];
|
|
121
|
+
for(let i = 0; i < lines.length; i++) {
|
|
122
|
+
if(/^(?:async )?function\s+\w+/.test(lines[i])) {
|
|
123
|
+
starts.push(i);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
starts.push(lines.length);
|
|
127
|
+
|
|
128
|
+
const out = [];
|
|
129
|
+
for(let k = 0; k < starts.length - 1; k++) {
|
|
130
|
+
const a = starts[k];
|
|
131
|
+
const m = /^(?:async )?function\s+(\w+)\s*\(([^)]*)/.exec(lines[a]);
|
|
132
|
+
if(!m) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
out.push({
|
|
136
|
+
name: m[1],
|
|
137
|
+
line: a + 1,
|
|
138
|
+
params: m[2],
|
|
139
|
+
body: lines.slice(a, starts[k + 1]).join("\n"),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
describe("every function that reads `priv` declares it", () => {
|
|
147
|
+
|
|
148
|
+
it("no bare `priv.` without `priv =` in the same function", () => {
|
|
149
|
+
const files = readdirSync(SRC)
|
|
150
|
+
.filter((f) => f.endsWith(".js") && !f.endsWith(".test.js"));
|
|
151
|
+
expect(files.length).toBeGreaterThan(10);
|
|
152
|
+
|
|
153
|
+
const offenders = [];
|
|
154
|
+
for(const f of files) {
|
|
155
|
+
const src = without_comments(readFileSync(join(SRC, f), "utf8"));
|
|
156
|
+
for(const fn of top_level_functions(src)) {
|
|
157
|
+
if(/(?<![.\w])priv(?![\w])/.test(fn.params)) {
|
|
158
|
+
continue; /* it takes priv as an argument */
|
|
159
|
+
}
|
|
160
|
+
const uses = /(?<![.\w])priv\s*\./.test(fn.body);
|
|
161
|
+
const declared = /(?<![.\w])priv\s*=[^=]/.test(fn.body);
|
|
162
|
+
if(uses && !declared) {
|
|
163
|
+
offenders.push(`${f}:${fn.line} ${fn.name}()`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
expect(offenders).toEqual([]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("the checker sees a missing declaration", () => {
|
|
172
|
+
/* The guard needs its own guard: a check that cannot fail is
|
|
173
|
+
* a check nobody can trust. */
|
|
174
|
+
const src = [
|
|
175
|
+
"function ac_broken(gobj, event, kw, src)",
|
|
176
|
+
"{",
|
|
177
|
+
" if(priv.reading_files) {",
|
|
178
|
+
" return -1;",
|
|
179
|
+
" }",
|
|
180
|
+
" return 0;",
|
|
181
|
+
"}",
|
|
182
|
+
].join("\n");
|
|
183
|
+
const fns = top_level_functions(without_comments(src));
|
|
184
|
+
expect(fns.length).toBe(1);
|
|
185
|
+
expect(/(?<![.\w])priv\s*\./.test(fns[0].body)).toBe(true);
|
|
186
|
+
expect(/(?<![.\w])priv\s*=[^=]/.test(fns[0].body)).toBe(false);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("a doc block that shows the idiom is not a use of it", () => {
|
|
190
|
+
const src = [
|
|
191
|
+
"/* priv.theme_observer = yui_watch_theme(gobj); */",
|
|
192
|
+
"function yui_is_dark()",
|
|
193
|
+
"{",
|
|
194
|
+
" return yui_theme_now() === 'dark';",
|
|
195
|
+
"}",
|
|
196
|
+
].join("\n");
|
|
197
|
+
const fns = top_level_functions(without_comments(src));
|
|
198
|
+
expect(fns.length).toBe(1);
|
|
199
|
+
expect(/(?<![.\w])priv\s*\./.test(fns[0].body)).toBe(false);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("`gobj.priv.x` is not a bare `priv`", () => {
|
|
203
|
+
const src = [
|
|
204
|
+
"function reads_through_gobj(gobj)",
|
|
205
|
+
"{",
|
|
206
|
+
" return gobj.priv.something;",
|
|
207
|
+
"}",
|
|
208
|
+
].join("\n");
|
|
209
|
+
const fns = top_level_functions(without_comments(src));
|
|
210
|
+
expect(/(?<![.\w])priv\s*\./.test(fns[0].body)).toBe(false);
|
|
211
|
+
});
|
|
212
|
+
});
|