iobroker.statechart 0.1.1 → 0.2.0
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 +130 -21
- package/admin/build/tab.js +132 -130
- package/admin/statechart.png +0 -0
- package/build/lib/chart-codegen.js +338 -0
- package/build/lib/chart-codegen.js.map +7 -0
- package/build/lib/chart-helpers.js +90 -0
- package/build/lib/chart-helpers.js.map +7 -0
- package/build/lib/chart-model.js +353 -0
- package/build/lib/chart-model.js.map +7 -0
- package/build/lib/chart-runtime.js +876 -0
- package/build/lib/chart-runtime.js.map +7 -0
- package/build/lib/chart-structure.js +101 -0
- package/build/lib/chart-structure.js.map +7 -0
- package/build/lib/chart-validation.js +303 -0
- package/build/lib/chart-validation.js.map +7 -0
- package/build/main.js +342 -0
- package/build/main.js.map +7 -0
- package/io-package.json +14 -1
- package/package.json +20 -17
- package/lib/adapter-config.d.ts +0 -19
- package/lib/chart-codegen.js +0 -436
- package/lib/chart-codegen.test.js +0 -132
- package/lib/chart-helpers.js +0 -74
- package/lib/chart-helpers.test.js +0 -38
- package/lib/chart-model.js +0 -577
- package/lib/chart-model.test.js +0 -189
- package/lib/chart-runtime.js +0 -797
- package/lib/chart-runtime.test.js +0 -699
- package/lib/chart-structure.js +0 -106
- package/lib/chart-validation.js +0 -328
- package/lib/chart-validation.test.js +0 -149
- package/main.js +0 -279
package/lib/chart-structure.js
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
/*
|
|
4
|
-
* The structure of a chart (hierarchy, transition order, kind of transitions) as the runtime executes it.
|
|
5
|
-
* Shared by the runtime and the code generator, so the generated code always shows what is executed.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* @typedef {import("./chart-model").Chart} Chart
|
|
10
|
-
* @typedef {import("./chart-model").ChartState} ChartState
|
|
11
|
-
* @typedef {import("./chart-model").ChartTransition} ChartTransition
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
class ChartStructure {
|
|
15
|
-
/**
|
|
16
|
-
* @param {Chart} chart
|
|
17
|
-
*/
|
|
18
|
-
constructor(chart) {
|
|
19
|
-
this.chart = chart;
|
|
20
|
-
/** @type {Map<string, ChartState>} */
|
|
21
|
-
this.statesById = new Map(chart.states.map(s => [s.id, s]));
|
|
22
|
-
/** @type {Map<string, ChartState[]>} children by parent ID, "" for the top level */
|
|
23
|
-
this.childrenByParent = new Map();
|
|
24
|
-
for (const state of chart.states) {
|
|
25
|
-
// states with an unknown parent are treated like top level states
|
|
26
|
-
const parent = state.parent && this.statesById.has(state.parent) ? state.parent : "";
|
|
27
|
-
this.childrenByParent.set(parent, [...(this.childrenByParent.get(parent) || []), state]);
|
|
28
|
-
}
|
|
29
|
-
/** @type {Map<string, ChartTransition[]>} transitions by source, sorted by priority */
|
|
30
|
-
this.transitionsBySource = new Map();
|
|
31
|
-
for (const transition of chart.transitions) {
|
|
32
|
-
const list = this.transitionsBySource.get(transition.source) || [];
|
|
33
|
-
this.transitionsBySource.set(transition.source, [...list, transition]);
|
|
34
|
-
}
|
|
35
|
-
for (const list of this.transitionsBySource.values()) {
|
|
36
|
-
// the sort is stable: transitions without priority keep their order, after the others
|
|
37
|
-
list.sort((a, b) => (a.priority ?? Number.MAX_SAFE_INTEGER) - (b.priority ?? Number.MAX_SAFE_INTEGER));
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* @param {string | null} parentId null for the top level
|
|
43
|
-
* @returns {ChartState[]}
|
|
44
|
-
*/
|
|
45
|
-
getChildren(parentId) {
|
|
46
|
-
return this.childrenByParent.get(parentId || "") || [];
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* The initial state of a level: the configured one if it is a child of this level, otherwise the first child
|
|
51
|
-
*
|
|
52
|
-
* @param {string | null} parentId null for the top level
|
|
53
|
-
* @returns {string | undefined} undefined if the level has no states
|
|
54
|
-
*/
|
|
55
|
-
getInitialChild(parentId) {
|
|
56
|
-
const children = this.getChildren(parentId);
|
|
57
|
-
const configured = parentId ? this.statesById.get(parentId)?.initial : this.chart.initial;
|
|
58
|
-
return children.some(s => s.id === configured) ? configured : children[0]?.id;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* @param {string} stateId
|
|
63
|
-
* @returns {ChartTransition[]} the transitions starting at the state, sorted by priority
|
|
64
|
-
*/
|
|
65
|
-
getTransitions(stateId) {
|
|
66
|
-
return this.transitionsBySource.get(stateId) || [];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* @param {string} ancestorId
|
|
71
|
-
* @param {string} id
|
|
72
|
-
* @returns {boolean} true if the state "id" is a (direct or indirect) child of "ancestorId"
|
|
73
|
-
*/
|
|
74
|
-
isDescendant(ancestorId, id) {
|
|
75
|
-
let parent = this.statesById.get(id)?.parent;
|
|
76
|
-
// the counter protects against cycles in invalid charts
|
|
77
|
-
for (let i = 0; parent && i < this.statesById.size; i++) {
|
|
78
|
-
if (parent === ancestorId) {
|
|
79
|
-
return true;
|
|
80
|
-
}
|
|
81
|
-
parent = this.statesById.get(parent)?.parent;
|
|
82
|
-
}
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* How a transition is executed
|
|
88
|
-
*
|
|
89
|
-
* @param {ChartTransition} transition
|
|
90
|
-
* @returns {{ inner: boolean; reenter: boolean }} inner: checked after the during action of the source,
|
|
91
|
-
* the source stays active. reenter: the source state is left and entered again (XState "reenter").
|
|
92
|
-
*/
|
|
93
|
-
getTransitionKind(transition) {
|
|
94
|
-
const { source, target } = transition;
|
|
95
|
-
const toChild = this.isDescendant(source, target);
|
|
96
|
-
// an inner transition stays in the source state, only its active child is left
|
|
97
|
-
const inner = toChild && !transition.reenterParent;
|
|
98
|
-
// Like Stateflow, the source state is left and entered again by self transitions, by transitions
|
|
99
|
-
// to an own child which should re-enter the parent, and by transitions from a child to its parent.
|
|
100
|
-
// Without "reenter", XState does not leave the source state if the target is a child of it.
|
|
101
|
-
const reenter = source === target || (toChild && !inner) || this.isDescendant(target, source);
|
|
102
|
-
return { inner, reenter };
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
module.exports = { ChartStructure };
|
package/lib/chart-validation.js
DELETED
|
@@ -1,328 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
/*
|
|
4
|
-
* Checks conditions and actions of a chart: JavaScript syntax, unknown variables and events.
|
|
5
|
-
* Used by the editor (to underline errors) and by the runtime (to log them when a chart starts).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const acorn = require("acorn");
|
|
9
|
-
const walk = require("acorn-walk");
|
|
10
|
-
const { HELPERS } = require("./chart-helpers");
|
|
11
|
-
const { LabelSyntaxError, formatMessage, parseTransitionLabelWithRanges } = require("./chart-model");
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* @typedef {import("./chart-model").Chart} Chart
|
|
15
|
-
* @typedef {import("./chart-model").ChartSymbol} ChartSymbol
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
/** Names which can be used in conditions and actions besides the chart variables */
|
|
19
|
-
const GLOBALS = new Set([
|
|
20
|
-
// the helper functions, e.g. log() and setState()
|
|
21
|
-
...Object.keys(HELPERS),
|
|
22
|
-
"undefined",
|
|
23
|
-
"NaN",
|
|
24
|
-
"Infinity",
|
|
25
|
-
"Math",
|
|
26
|
-
"Date",
|
|
27
|
-
"JSON",
|
|
28
|
-
"Number",
|
|
29
|
-
"String",
|
|
30
|
-
"Boolean",
|
|
31
|
-
"Array",
|
|
32
|
-
"Object",
|
|
33
|
-
"RegExp",
|
|
34
|
-
"Map",
|
|
35
|
-
"Set",
|
|
36
|
-
"Error",
|
|
37
|
-
"parseInt",
|
|
38
|
-
"parseFloat",
|
|
39
|
-
"isNaN",
|
|
40
|
-
"isFinite",
|
|
41
|
-
"encodeURIComponent",
|
|
42
|
-
"decodeURIComponent",
|
|
43
|
-
]);
|
|
44
|
-
|
|
45
|
-
/** Fields of a state which contain actions */
|
|
46
|
-
const STATE_ACTIONS = /** @type {const} */ (["entry", "during", "exit"]);
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* @typedef {object} Diagnostic
|
|
50
|
-
* @property {number} from Position in the checked text
|
|
51
|
-
* @property {number} to
|
|
52
|
-
* @property {"error" | "warning"} severity
|
|
53
|
-
* @property {string} message English message
|
|
54
|
-
* @property {string} key Message with %s placeholders, used to translate it
|
|
55
|
-
* @property {string[]} args
|
|
56
|
-
*/
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* @param {number} from
|
|
60
|
-
* @param {number} to
|
|
61
|
-
* @param {"error" | "warning"} severity
|
|
62
|
-
* @param {string} key
|
|
63
|
-
* @param {string[]} [args]
|
|
64
|
-
* @returns {Diagnostic}
|
|
65
|
-
*/
|
|
66
|
-
function createDiagnostic(from, to, severity, key, args = []) {
|
|
67
|
-
return { from, to, severity, key, args, message: formatMessage(key, args) };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Parses JavaScript and converts syntax errors to diagnostics
|
|
72
|
-
*
|
|
73
|
-
* @param {string} code
|
|
74
|
-
* @param {"expression" | "statements"} kind
|
|
75
|
-
* @returns {{ ast: acorn.Node } | { diagnostic: Diagnostic }}
|
|
76
|
-
*/
|
|
77
|
-
function parseCode(code, kind) {
|
|
78
|
-
try {
|
|
79
|
-
if (kind === "statements") {
|
|
80
|
-
return { ast: acorn.parse(code, { ecmaVersion: "latest", sourceType: "script" }) };
|
|
81
|
-
}
|
|
82
|
-
const ast = acorn.parseExpressionAt(code, 0, { ecmaVersion: "latest" });
|
|
83
|
-
const rest = code.substring(ast.end);
|
|
84
|
-
if (rest.trim()) {
|
|
85
|
-
const from = ast.end + rest.length - rest.trimStart().length;
|
|
86
|
-
return { diagnostic: createDiagnostic(from, code.length, "error", 'Unexpected "%s"', [rest.trim()]) };
|
|
87
|
-
}
|
|
88
|
-
return { ast };
|
|
89
|
-
} catch (err) {
|
|
90
|
-
const pos = typeof err.pos === "number" ? Math.min(err.pos, code.length) : 0;
|
|
91
|
-
// acorn adds the position "(line:column)" to its messages, it is shown by the underline anyway
|
|
92
|
-
const message = String(err.message).replace(/\s*\(\d+:\d+\)$/, "");
|
|
93
|
-
// underline the token at the error position, or the last character if the code ends unexpectedly
|
|
94
|
-
const from = pos < code.length ? pos : Math.max(0, code.length - 1);
|
|
95
|
-
const token = /^(\w+|\S)/.exec(code.substring(from));
|
|
96
|
-
const to = Math.min(code.length, from + (token ? token[0].length : 1));
|
|
97
|
-
return { diagnostic: createDiagnostic(from, to, "error", "Syntax error: %s", [message]) };
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Checks a condition or action
|
|
103
|
-
*
|
|
104
|
-
* @param {string | undefined} code
|
|
105
|
-
* @param {"expression" | "statements"} kind Condition (expression) or action (statements)
|
|
106
|
-
* @param {ChartSymbol[]} symbols
|
|
107
|
-
* @param {number} [offset] Position of the code in the text, added to all positions
|
|
108
|
-
* @param {string[]} [functionNames] Names declared in the function blocks of the chart
|
|
109
|
-
* @returns {Diagnostic[]}
|
|
110
|
-
*/
|
|
111
|
-
function validateCode(code, kind, symbols, offset = 0, functionNames = []) {
|
|
112
|
-
if (!code?.trim()) {
|
|
113
|
-
return [];
|
|
114
|
-
}
|
|
115
|
-
const parsed = parseCode(code, kind);
|
|
116
|
-
if ("diagnostic" in parsed) {
|
|
117
|
-
const d = parsed.diagnostic;
|
|
118
|
-
return [{ ...d, from: d.from + offset, to: d.to + offset }];
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** names declared in the code itself (let, const, function parameters, ...) */
|
|
122
|
-
const declared = new Set();
|
|
123
|
-
/** @type {{ node: any; write: boolean }[]} */
|
|
124
|
-
const references = [];
|
|
125
|
-
walk.fullAncestor(parsed.ast, (node, _state, ancestors, type) => {
|
|
126
|
-
if (node.type !== "Identifier") {
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
/** @type {any} */
|
|
130
|
-
const parent = ancestors[ancestors.length - 2];
|
|
131
|
-
if (type === "VariablePattern") {
|
|
132
|
-
// acorn-walk uses "VariablePattern" for declarations and for assignment targets
|
|
133
|
-
if (parent?.type === "AssignmentExpression" && parent.left === node) {
|
|
134
|
-
references.push({ node, write: true });
|
|
135
|
-
} else {
|
|
136
|
-
declared.add(node.name);
|
|
137
|
-
}
|
|
138
|
-
} else {
|
|
139
|
-
references.push({ node, write: parent?.type === "UpdateExpression" });
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
const inputs = new Set(symbols.filter(s => s.scope === "input").map(s => s.name));
|
|
144
|
-
const known = new Set([...symbols.map(s => s.name), ...functionNames]);
|
|
145
|
-
/** @type {Diagnostic[]} */
|
|
146
|
-
const diagnostics = [];
|
|
147
|
-
for (const { node, write } of references) {
|
|
148
|
-
const from = node.start + offset;
|
|
149
|
-
const to = node.end + offset;
|
|
150
|
-
if (!known.has(node.name) && !declared.has(node.name) && !GLOBALS.has(node.name)) {
|
|
151
|
-
diagnostics.push(createDiagnostic(from, to, "error", 'Unknown variable "%s"', [node.name]));
|
|
152
|
-
} else if (write && inputs.has(node.name) && !declared.has(node.name)) {
|
|
153
|
-
diagnostics.push(
|
|
154
|
-
createDiagnostic(from, to, "warning", 'Input "%s" is overwritten on its next change', [node.name]),
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
return diagnostics;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Checks a transition label: its structure, the event and the JavaScript of condition and action
|
|
163
|
-
*
|
|
164
|
-
* @param {string | undefined} label
|
|
165
|
-
* @param {ChartSymbol[]} symbols
|
|
166
|
-
* @param {string[]} [functionNames] Names declared in the function blocks of the chart
|
|
167
|
-
* @returns {Diagnostic[]}
|
|
168
|
-
*/
|
|
169
|
-
function validateTransitionLabel(label, symbols, functionNames = []) {
|
|
170
|
-
let parsed;
|
|
171
|
-
try {
|
|
172
|
-
parsed = parseTransitionLabelWithRanges(label);
|
|
173
|
-
} catch (err) {
|
|
174
|
-
if (err instanceof LabelSyntaxError) {
|
|
175
|
-
return [createDiagnostic(err.from, err.to, "error", err.key, err.args)];
|
|
176
|
-
}
|
|
177
|
-
throw err;
|
|
178
|
-
}
|
|
179
|
-
const { label: parts, ranges } = parsed;
|
|
180
|
-
|
|
181
|
-
/** @type {Diagnostic[]} */
|
|
182
|
-
const diagnostics = [];
|
|
183
|
-
if (parts.event && ranges.event && !symbols.some(s => s.scope === "input" && s.name === parts.event)) {
|
|
184
|
-
diagnostics.push(
|
|
185
|
-
createDiagnostic(
|
|
186
|
-
ranges.event.from,
|
|
187
|
-
ranges.event.to,
|
|
188
|
-
"error",
|
|
189
|
-
'Unknown event "%s", use the name of an input',
|
|
190
|
-
[parts.event],
|
|
191
|
-
),
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
if (parts.condition && ranges.condition) {
|
|
195
|
-
diagnostics.push(...validateCode(parts.condition, "expression", symbols, ranges.condition.from, functionNames));
|
|
196
|
-
}
|
|
197
|
-
if (parts.action && ranges.action) {
|
|
198
|
-
diagnostics.push(...validateCode(parts.action, "statements", symbols, ranges.action.from, functionNames));
|
|
199
|
-
}
|
|
200
|
-
return diagnostics;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* @typedef {object} Declaration A name declared at the top level of a function block
|
|
205
|
-
* @property {string} name
|
|
206
|
-
* @property {string | null} signature e.g. "clamp(x, lo, hi)", null if it is no function
|
|
207
|
-
* @property {number} from Position of the name in the code
|
|
208
|
-
* @property {number} to
|
|
209
|
-
*/
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Finds the names declared at the top level of a function block: functions, but also constants etc.
|
|
213
|
-
*
|
|
214
|
-
* @param {string | undefined} code
|
|
215
|
-
* @returns {Declaration[]} empty if the code has syntax errors
|
|
216
|
-
*/
|
|
217
|
-
function getDeclarations(code) {
|
|
218
|
-
if (!code?.trim()) {
|
|
219
|
-
return [];
|
|
220
|
-
}
|
|
221
|
-
const parsed = parseCode(code, "statements");
|
|
222
|
-
if ("diagnostic" in parsed) {
|
|
223
|
-
return [];
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* @param {any} fn Function node
|
|
227
|
-
* @param {string} name
|
|
228
|
-
*/
|
|
229
|
-
const signature = (fn, name) => `${name}(${fn.params.map(p => code.substring(p.start, p.end)).join(", ")})`;
|
|
230
|
-
|
|
231
|
-
/** @type {Declaration[]} */
|
|
232
|
-
const declarations = [];
|
|
233
|
-
for (const node of /** @type {any} */ (parsed.ast).body) {
|
|
234
|
-
if (node.type === "FunctionDeclaration" && node.id) {
|
|
235
|
-
const { name, start, end } = node.id;
|
|
236
|
-
declarations.push({ name, signature: signature(node, name), from: start, to: end });
|
|
237
|
-
} else if (node.type === "VariableDeclaration") {
|
|
238
|
-
for (const declarator of node.declarations) {
|
|
239
|
-
if (declarator.id.type !== "Identifier") {
|
|
240
|
-
continue;
|
|
241
|
-
}
|
|
242
|
-
const { name, start, end } = declarator.id;
|
|
243
|
-
const init = declarator.init;
|
|
244
|
-
const isFunction = init?.type === "ArrowFunctionExpression" || init?.type === "FunctionExpression";
|
|
245
|
-
declarations.push({ name, signature: isFunction ? signature(init, name) : null, from: start, to: end });
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
return declarations;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Names declared in all function blocks of a chart
|
|
254
|
-
*
|
|
255
|
-
* @param {Chart} chart
|
|
256
|
-
* @returns {string[]}
|
|
257
|
-
*/
|
|
258
|
-
function getFunctionNames(chart) {
|
|
259
|
-
return (chart.functions || []).flatMap(fn => getDeclarations(fn.code).map(d => d.name));
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* @typedef {object} ChartProblem
|
|
264
|
-
* @property {"state" | "transition" | "function"} kind
|
|
265
|
-
* @property {string} id ID of the state, transition or function block
|
|
266
|
-
* @property {string} field "entry", "during", "exit", "label" or "code"
|
|
267
|
-
* @property {Diagnostic} diagnostic
|
|
268
|
-
*/
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Checks all actions, transition labels and function blocks of a chart
|
|
272
|
-
*
|
|
273
|
-
* @param {Chart} chart
|
|
274
|
-
* @returns {ChartProblem[]}
|
|
275
|
-
*/
|
|
276
|
-
function validateChart(chart) {
|
|
277
|
-
const functionNames = getFunctionNames(chart);
|
|
278
|
-
/** @type {ChartProblem[]} */
|
|
279
|
-
const problems = [];
|
|
280
|
-
for (const state of chart.states) {
|
|
281
|
-
for (const field of STATE_ACTIONS) {
|
|
282
|
-
for (const diagnostic of validateCode(state[field], "statements", chart.symbols, 0, functionNames)) {
|
|
283
|
-
problems.push({ kind: "state", id: state.id, field, diagnostic });
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
for (const transition of chart.transitions) {
|
|
288
|
-
for (const diagnostic of validateTransitionLabel(transition.label, chart.symbols, functionNames)) {
|
|
289
|
-
problems.push({ kind: "transition", id: transition.id, field: "label", diagnostic });
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// a name may only be declared once in all function blocks, and not hide a variable
|
|
294
|
-
const symbolNames = new Set(chart.symbols.map(s => s.name));
|
|
295
|
-
const seen = new Set();
|
|
296
|
-
for (const fn of chart.functions || []) {
|
|
297
|
-
for (const diagnostic of validateCode(fn.code, "statements", chart.symbols, 0, functionNames)) {
|
|
298
|
-
problems.push({ kind: "function", id: fn.id, field: "code", diagnostic });
|
|
299
|
-
}
|
|
300
|
-
for (const { name, from, to } of getDeclarations(fn.code)) {
|
|
301
|
-
let key = null;
|
|
302
|
-
if (seen.has(name)) {
|
|
303
|
-
key = '"%s" is declared in more than one function block';
|
|
304
|
-
} else if (symbolNames.has(name) || GLOBALS.has(name)) {
|
|
305
|
-
key = '"%s" is already the name of a variable';
|
|
306
|
-
}
|
|
307
|
-
if (key) {
|
|
308
|
-
problems.push({
|
|
309
|
-
kind: "function",
|
|
310
|
-
id: fn.id,
|
|
311
|
-
field: "code",
|
|
312
|
-
diagnostic: createDiagnostic(from, to, "error", key, [name]),
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
seen.add(name);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
return problems;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
module.exports = {
|
|
322
|
-
GLOBALS,
|
|
323
|
-
getDeclarations,
|
|
324
|
-
getFunctionNames,
|
|
325
|
-
validateChart,
|
|
326
|
-
validateCode,
|
|
327
|
-
validateTransitionLabel,
|
|
328
|
-
};
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const { expect } = require("chai");
|
|
4
|
-
const { getDeclarations, validateChart, validateCode, validateTransitionLabel } = require("./chart-validation");
|
|
5
|
-
|
|
6
|
-
/** @type {import("./chart-model").ChartSymbol[]} */
|
|
7
|
-
const symbols = [
|
|
8
|
-
{ name: "motion", scope: "input", stateId: "test.0.motion" },
|
|
9
|
-
{ name: "light", scope: "output", stateId: "test.0.light" },
|
|
10
|
-
{ name: "count", scope: "local", initial: 0 },
|
|
11
|
-
];
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* @param {import("./chart-validation").Diagnostic[]} diagnostics
|
|
15
|
-
* @param {string} text
|
|
16
|
-
*/
|
|
17
|
-
const summarize = (diagnostics, text) =>
|
|
18
|
-
diagnostics.map(d => `${d.severity}: ${d.message} @${text.substring(d.from, d.to)}`);
|
|
19
|
-
|
|
20
|
-
describe("chart-validation => validateCode", () => {
|
|
21
|
-
it("accepts valid code", () => {
|
|
22
|
-
expect(validateCode("light = motion && count > 1; log(Math.max(1, 2));", "statements", symbols)).to.deep.equal(
|
|
23
|
-
[],
|
|
24
|
-
);
|
|
25
|
-
expect(validateCode("", "statements", symbols)).to.deep.equal([]);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("reports unknown variables with their position", () => {
|
|
29
|
-
const code = "light = motoin;";
|
|
30
|
-
expect(summarize(validateCode(code, "statements", symbols), code)).to.deep.equal([
|
|
31
|
-
'error: Unknown variable "motoin" @motoin',
|
|
32
|
-
]);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("knows variables declared in the code", () => {
|
|
36
|
-
const code = "let x = 1; const f = (a) => a + x; for (const i of [1]) { light = f(i); }";
|
|
37
|
-
expect(validateCode(code, "statements", symbols)).to.deep.equal([]);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("ignores property names", () => {
|
|
41
|
-
expect(validateCode("light = { on: true }.on && JSON.parse('1').foo;", "statements", symbols)).to.deep.equal(
|
|
42
|
-
[],
|
|
43
|
-
);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("warns when an input is assigned", () => {
|
|
47
|
-
const code = "motion = false; count++;";
|
|
48
|
-
expect(summarize(validateCode(code, "statements", symbols), code)).to.deep.equal([
|
|
49
|
-
'warning: Input "motion" is overwritten on its next change @motion',
|
|
50
|
-
]);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("reports syntax errors", () => {
|
|
54
|
-
const code = "light = = true;";
|
|
55
|
-
const diagnostics = validateCode(code, "statements", symbols);
|
|
56
|
-
expect(diagnostics).to.have.length(1);
|
|
57
|
-
expect(diagnostics[0].message).to.match(/^Syntax error: Unexpected token/);
|
|
58
|
-
expect(code.substring(diagnostics[0].from, diagnostics[0].to)).to.equal("=");
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("rejects statements in conditions", () => {
|
|
62
|
-
const code = "motion; light = 1";
|
|
63
|
-
expect(summarize(validateCode(code, "expression", symbols), code)).to.deep.equal([
|
|
64
|
-
'error: Unexpected "; light = 1" @; light = 1',
|
|
65
|
-
]);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
describe("chart-validation => validateTransitionLabel", () => {
|
|
70
|
-
it("accepts valid labels", () => {
|
|
71
|
-
expect(validateTransitionLabel("motion[motion && count < 3]{count++;}", symbols)).to.deep.equal([]);
|
|
72
|
-
expect(validateTransitionLabel("after(5, sec)", symbols)).to.deep.equal([]);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("reports problems at their position in the label", () => {
|
|
76
|
-
const label = "motoin[motion && lightt]{light = tru;}";
|
|
77
|
-
expect(summarize(validateTransitionLabel(label, symbols), label)).to.deep.equal([
|
|
78
|
-
'error: Unknown event "motoin", use the name of an input @motoin',
|
|
79
|
-
'error: Unknown variable "lightt" @lightt',
|
|
80
|
-
'error: Unknown variable "tru" @tru',
|
|
81
|
-
]);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("finds problems in multi line labels", () => {
|
|
85
|
-
const label = "[motion]\n{\n light = true;\n cnt++;\n}";
|
|
86
|
-
expect(summarize(validateTransitionLabel(label, symbols), label)).to.deep.equal([
|
|
87
|
-
'error: Unknown variable "cnt" @cnt',
|
|
88
|
-
]);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("reports structural errors with key and arguments for translation", () => {
|
|
92
|
-
const [diagnostic] = validateTransitionLabel("[motion", symbols);
|
|
93
|
-
expect(diagnostic).to.include({ key: 'Missing "%s"', message: 'Missing "]"', from: 0, to: 7 });
|
|
94
|
-
expect(diagnostic.args).to.deep.equal(["]"]);
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
describe("chart-validation => validateChart", () => {
|
|
99
|
-
it("reports problems of states and transitions", () => {
|
|
100
|
-
const problems = validateChart({
|
|
101
|
-
version: 1,
|
|
102
|
-
name: "Test",
|
|
103
|
-
enabled: true,
|
|
104
|
-
symbols,
|
|
105
|
-
states: [{ id: "s1", name: "S1", entry: "lihgt = true;", x: 0, y: 0 }],
|
|
106
|
-
transitions: [{ id: "t1", source: "s1", target: "s1", label: "[x]" }],
|
|
107
|
-
});
|
|
108
|
-
expect(problems.map(p => `${p.kind} ${p.id} ${p.field}: ${p.diagnostic.message}`)).to.deep.equal([
|
|
109
|
-
'state s1 entry: Unknown variable "lihgt"',
|
|
110
|
-
'transition t1 label: Unknown variable "x"',
|
|
111
|
-
]);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("knows the functions of function blocks and checks the blocks", () => {
|
|
115
|
-
const problems = validateChart({
|
|
116
|
-
version: 1,
|
|
117
|
-
name: "Test",
|
|
118
|
-
enabled: true,
|
|
119
|
-
symbols,
|
|
120
|
-
states: [{ id: "s1", name: "S1", entry: "light = isOn(count) && LIMIT > 1;", x: 0, y: 0 }],
|
|
121
|
-
transitions: [{ id: "t1", source: "s1", target: "s1", label: "[clamp(count, 0, 5) > 1]" }],
|
|
122
|
-
functions: [
|
|
123
|
-
{ id: "f1", code: "function clamp(x, lo, hi) { return Math.min(hi, Math.max(lo, x)); }", x: 0, y: 0 },
|
|
124
|
-
{ id: "f2", code: "const LIMIT = 3;\nconst isOn = c => c > LIMIT && motoin;", x: 0, y: 0 },
|
|
125
|
-
{ id: "f3", code: "function clamp() {}\nfunction count() {}", x: 0, y: 0 },
|
|
126
|
-
],
|
|
127
|
-
});
|
|
128
|
-
expect(problems.map(p => `${p.kind} ${p.id} ${p.field}: ${p.diagnostic.message}`)).to.deep.equal([
|
|
129
|
-
'function f2 code: Unknown variable "motoin"',
|
|
130
|
-
'function f3 code: "clamp" is declared in more than one function block',
|
|
131
|
-
'function f3 code: "count" is already the name of a variable',
|
|
132
|
-
]);
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
describe("chart-validation => getDeclarations", () => {
|
|
137
|
-
it("returns functions with their signature and other declarations", () => {
|
|
138
|
-
const code = "function clamp(x, lo = 0, ...rest) {}\nconst f = (a, b) => a + b;\nlet counter = 0;";
|
|
139
|
-
expect(getDeclarations(code).map(d => [d.name, d.signature, code.substring(d.from, d.to)])).to.deep.equal([
|
|
140
|
-
["clamp", "clamp(x, lo = 0, ...rest)", "clamp"],
|
|
141
|
-
["f", "f(a, b)", "f"],
|
|
142
|
-
["counter", null, "counter"],
|
|
143
|
-
]);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it("returns nothing for code with syntax errors", () => {
|
|
147
|
-
expect(getDeclarations("function (")).to.deep.equal([]);
|
|
148
|
-
});
|
|
149
|
-
});
|