bare-script 2.0.2

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.
@@ -0,0 +1,136 @@
1
+ // Licensed under the MIT License
2
+ // https://github.com/craigahobbs/bare-script/blob/main/LICENSE
3
+
4
+
5
+ /**
6
+ * Parse the library source for documentation tags
7
+ *
8
+ * @param {string[][]} files - The list of file name and source tuples
9
+ * @returns {Object} The [library documentation model]{@link https://craigahobbs.github.io/bare-script/library/#var.vDoc=1}
10
+ * @throws {Error}
11
+ * @ignore
12
+ */
13
+ export function parseLibraryDoc(files) {
14
+ // Parse each source file line-by-line
15
+ const errors = [];
16
+ const funcs = {};
17
+ let func = null;
18
+ for (const [file, source] of files) {
19
+ const lines = source.split(rSplit);
20
+ for (const [ixLine, line] of lines.entries()) {
21
+ // function/group/doc/return documentation keywords?
22
+ const matchKey = line.match(rKey);
23
+ if (matchKey !== null) {
24
+ const {key, text} = matchKey.groups;
25
+ const textTrim = text.trim();
26
+
27
+ // Keyword used outside of function?
28
+ if (key !== 'function' && func === null) {
29
+ errors.push(`${file}:${ixLine + 1}: ${key} keyword outside function`);
30
+ continue;
31
+ }
32
+
33
+ // Process the keyword
34
+ if (key === 'group') {
35
+ if (textTrim === '') {
36
+ errors.push(`${file}:${ixLine + 1}: Invalid function group name "${textTrim}"`);
37
+ continue;
38
+ }
39
+ if ('group' in func) {
40
+ errors.push(`${file}:${ixLine + 1}: Function "${func.name}" group redefinition`);
41
+ continue;
42
+ }
43
+
44
+ // Set the function group
45
+ func.group = textTrim;
46
+ } else if (key === 'doc' || key === 'return') {
47
+ // Add the documentation line - don't add leading blank lines
48
+ let funcDoc = func[key] ?? null;
49
+ if (funcDoc !== null || textTrim !== '') {
50
+ if (funcDoc === null) {
51
+ funcDoc = [];
52
+ func[key] = funcDoc;
53
+ }
54
+ funcDoc.push(text);
55
+ }
56
+ } else {
57
+ // key === 'function'
58
+ if (textTrim === '') {
59
+ errors.push(`${file}:${ixLine + 1}: Invalid function name "${textTrim}"`);
60
+ continue;
61
+ }
62
+ if (textTrim in funcs) {
63
+ errors.push(`${file}:${ixLine + 1}: Function "${textTrim}" redefinition`);
64
+ continue;
65
+ }
66
+
67
+ // Add the function
68
+ func = {'name': textTrim};
69
+ funcs[textTrim] = func;
70
+ }
71
+ } else {
72
+ // arg keyword?
73
+ const matchArg = line.match(rArg);
74
+ if (matchArg !== null) {
75
+ const {name, text} = matchArg.groups;
76
+ const textTrim = text.trim();
77
+
78
+ // Keyword used outside of function?
79
+ if (func === null) {
80
+ errors.push(`${file}:${ixLine + 1}: Function argument "${name}" outside function`);
81
+ continue;
82
+ }
83
+
84
+ // Add the function arg documentation line - don't add leading blank lines
85
+ let args = func.args ?? null;
86
+ let arg = (args !== null ? args.find((argFind) => argFind.name === name) : null) ?? null;
87
+ if (arg !== null || textTrim !== '') {
88
+ if (args === null) {
89
+ args = [];
90
+ func.args = args;
91
+ }
92
+ if (arg === null) {
93
+ arg = {'name': name, 'doc': []};
94
+ args.push(arg);
95
+ }
96
+ arg.doc.push(text);
97
+ }
98
+ }
99
+ }
100
+ }
101
+ }
102
+
103
+ // Create the library documentation model
104
+ const library = {
105
+ 'functions': Object.values(funcs).sort(
106
+ /* c8 ignore next */
107
+ (funcA, funcB) => (funcA.name < funcB.name ? -1 : (funcA.name === funcB.name ? 0 : 1))
108
+ )
109
+ };
110
+
111
+ // Validate
112
+ if (library.functions.length === 0) {
113
+ errors.push('error: No library functions');
114
+ }
115
+ for (const funcLib of library.functions) {
116
+ if (!('group' in funcLib)) {
117
+ errors.push(`error: Function "${funcLib.name}" missing group`);
118
+ }
119
+ if (!('doc' in funcLib)) {
120
+ errors.push(`error: Function "${funcLib.name}" missing documentation`);
121
+ }
122
+ }
123
+
124
+ // Errors?
125
+ if (errors.length !== 0) {
126
+ throw new Error(errors.join('\n'));
127
+ }
128
+
129
+ return library;
130
+ }
131
+
132
+
133
+ // Library documentation regular expressions
134
+ const rKey = /^\s*(?:\/\/|#)\s*\$(?<key>function|group|doc|return):\s?(?<text>.*)$/;
135
+ const rArg = /^\s*(?:\/\/|#)\s*\$arg\s+(?<name>[A-Za-z_][A-Za-z0-9_]*):\s?(?<text>.*)$/;
136
+ const rSplit = /\r?\n/;
package/lib/model.js ADDED
@@ -0,0 +1,477 @@
1
+ // Licensed under the MIT License
2
+ // https://github.com/craigahobbs/bare-script/blob/main/LICENSE
3
+
4
+ /** @module lib/model */
5
+
6
+ import {parseSchemaMarkdown} from 'schema-markdown/lib/parser.js';
7
+ import {validateType} from 'schema-markdown/lib/schema.js';
8
+
9
+
10
+ /**
11
+ * The BareScript type model
12
+ */
13
+ export const bareScriptTypes = parseSchemaMarkdown(`\
14
+ # A BareScript script
15
+ struct BareScript
16
+
17
+ # The script's statements
18
+ ScriptStatement[] statements
19
+
20
+
21
+ # A script statement
22
+ union ScriptStatement
23
+
24
+ # An expression
25
+ ExpressionStatement expr
26
+
27
+ # A jump statement
28
+ JumpStatement jump
29
+
30
+ # A return statement
31
+ ReturnStatement return
32
+
33
+ # A label definition
34
+ string label
35
+
36
+ # A function definition
37
+ FunctionStatement function
38
+
39
+ # An include statement
40
+ IncludeStatement include
41
+
42
+
43
+ # An expression statement
44
+ struct ExpressionStatement
45
+
46
+ # The variable name to assign the expression value
47
+ optional string name
48
+
49
+ # The expression to evaluate
50
+ Expression expr
51
+
52
+
53
+ # A jump statement
54
+ struct JumpStatement
55
+
56
+ # The label to jump to
57
+ string label
58
+
59
+ # The test expression
60
+ optional Expression expr
61
+
62
+
63
+ # A return statement
64
+ struct ReturnStatement
65
+
66
+ # The expression to return
67
+ optional Expression expr
68
+
69
+
70
+ # A function definition statement
71
+ struct FunctionStatement
72
+
73
+ # If true, the function is defined as async
74
+ optional bool async
75
+
76
+ # The function name
77
+ string name
78
+
79
+ # The function's argument names
80
+ optional string[len > 0] args
81
+
82
+ # The function's statements
83
+ ScriptStatement[] statements
84
+
85
+
86
+ # An include statement
87
+ struct IncludeStatement
88
+
89
+ # The list of include scripts to load and execute in the global scope
90
+ IncludeScript[len > 0] includes
91
+
92
+
93
+ # An include script
94
+ struct IncludeScript
95
+
96
+ # The include script URL
97
+ string url
98
+
99
+ # If true, this is a system include
100
+ optional bool system
101
+
102
+
103
+ # An expression
104
+ union Expression
105
+
106
+ # A number literal
107
+ float number
108
+
109
+ # A string literal
110
+ string string
111
+
112
+ # A variable value
113
+ string variable
114
+
115
+ # A function expression
116
+ FunctionExpression function
117
+
118
+ # A binary expression
119
+ BinaryExpression binary
120
+
121
+ # A unary expression
122
+ UnaryExpression unary
123
+
124
+ # An expression group
125
+ Expression group
126
+
127
+
128
+ # A binary expression
129
+ struct BinaryExpression
130
+
131
+ # The binary expression operator
132
+ BinaryExpressionOperator op
133
+
134
+ # The left expression
135
+ Expression left
136
+
137
+ # The right expression
138
+ Expression right
139
+
140
+
141
+ # A binary expression operator
142
+ enum BinaryExpressionOperator
143
+
144
+ # Exponentiation
145
+ "**"
146
+
147
+ # Multiplication
148
+ "*"
149
+
150
+ # Division
151
+ "/"
152
+
153
+ # Remainder
154
+ "%"
155
+
156
+ # Addition
157
+ "+"
158
+
159
+ # Subtraction
160
+ "-"
161
+
162
+ # Less than or equal
163
+ "<="
164
+
165
+ # Less than
166
+ "<"
167
+
168
+ # Greater than or equal
169
+ ">="
170
+
171
+ # Greater than
172
+ ">"
173
+
174
+ # Equal
175
+ "=="
176
+
177
+ # Not equal
178
+ "!="
179
+
180
+ # Logical AND
181
+ "&&"
182
+
183
+ # Logical OR
184
+ "||"
185
+
186
+
187
+ # A unary expression
188
+ struct UnaryExpression
189
+
190
+ # The unary expression operator
191
+ UnaryExpressionOperator op
192
+
193
+ # The expression
194
+ Expression expr
195
+
196
+
197
+ # A unary expression operator
198
+ enum UnaryExpressionOperator
199
+
200
+ # Unary negation
201
+ "-"
202
+
203
+ # Logical NOT
204
+ "!"
205
+
206
+
207
+ # A function expression
208
+ struct FunctionExpression
209
+
210
+ # The function name
211
+ string name
212
+
213
+ # The function arguments
214
+ optional Expression[] args
215
+ `);
216
+
217
+
218
+ /**
219
+ * Validate a BareScript script model
220
+ *
221
+ * @param {Object} script - The [BareScript model]{@link https://craigahobbs.github.io/bare-script/model/#var.vName='BareScript'}
222
+ * @returns {Object} The validated BareScript model
223
+ * @throws [ValidationError]{@link https://craigahobbs.github.io/schema-markdown-js/module-lib_schema.ValidationError.html}
224
+ */
225
+ export function validateScript(script) {
226
+ return validateType(bareScriptTypes, 'BareScript', script);
227
+ }
228
+
229
+
230
+ /**
231
+ * Validate an expression model
232
+ *
233
+ * @param {Object} expr - The [expression model]{@link https://craigahobbs.github.io/bare-script/model/#var.vName='Expression'}
234
+ * @returns {Object} The validated expression model
235
+ * @throws [ValidationError]{@link https://craigahobbs.github.io/schema-markdown-js/module-lib_schema.ValidationError.html}
236
+ */
237
+ export function validateExpression(expr) {
238
+ return validateType(bareScriptTypes, 'Expression', expr);
239
+ }
240
+
241
+
242
+ /**
243
+ * Lint a BareScript script model
244
+ *
245
+ * @param {Object} script - The [BareScript model]{@link https://craigahobbs.github.io/bare-script/model/#var.vName='BareScript'}
246
+ * @returns {string[]} The array of lint warnings
247
+ */
248
+ export function lintScript(script) {
249
+ const warnings = [];
250
+
251
+ // Empty script?
252
+ if (script.statements.length === 0) {
253
+ warnings.push('Empty script');
254
+ }
255
+
256
+ // Variable used before assignment?
257
+ const varAssigns = {};
258
+ const varUses = {};
259
+ getVariableAssignmentsAndUses(script.statements, varAssigns, varUses);
260
+ for (const varName of Object.keys(varAssigns)) {
261
+ if (varName in varUses && varUses[varName] <= varAssigns[varName]) {
262
+ warnings.push(
263
+ `Global variable "${varName}" used (index ${varUses[varName]}) before assignment (index ${varAssigns[varName]})`
264
+ );
265
+ }
266
+ }
267
+
268
+ // Iterate global statements
269
+ const functionsDefined = {};
270
+ const labelsDefined = {};
271
+ const labelsUsed = {};
272
+ for (const [ixStatement, statement] of script.statements.entries()) {
273
+ const [statementKey] = Object.keys(statement);
274
+
275
+ // Function definition checks
276
+ if (statementKey === 'function') {
277
+ // Function redefinition?
278
+ if (statement.function.name in functionsDefined) {
279
+ warnings.push(`Redefinition of function "${statement.function.name}" (index ${ixStatement})`);
280
+ } else {
281
+ functionsDefined[statement.function.name] = ixStatement;
282
+ }
283
+
284
+ // Variable used before assignment?
285
+ const fnVarAssigns = {};
286
+ const fnVarUses = {};
287
+ const args = (statement.function.args ?? null);
288
+ getVariableAssignmentsAndUses(statement.function.statements, fnVarAssigns, fnVarUses);
289
+ for (const varName of Object.keys(fnVarAssigns)) {
290
+ // Ignore re-assigned function arguments
291
+ if (args !== null && args.indexOf(varName) !== -1) {
292
+ continue;
293
+ }
294
+ if (varName in fnVarUses && fnVarUses[varName] <= fnVarAssigns[varName]) {
295
+ warnings.push(
296
+ `Variable "${varName}" of function "${statement.function.name}" used (index ${fnVarUses[varName]}) ` +
297
+ `before assignment (index ${fnVarAssigns[varName]})`
298
+ );
299
+ }
300
+ }
301
+
302
+ // Unused variables?
303
+ for (const varName of Object.keys(fnVarAssigns)) {
304
+ if (!(varName in fnVarUses)) {
305
+ warnings.push(
306
+ `Unused variable "${varName}" defined in function "${statement.function.name}" (index ${fnVarAssigns[varName]})`
307
+ );
308
+ }
309
+ }
310
+
311
+ // Function argument checks
312
+ if (args !== null) {
313
+ const argsDefined = new Set();
314
+ for (const arg of args) {
315
+ // Duplicate argument?
316
+ if (argsDefined.has(arg)) {
317
+ warnings.push(`Duplicate argument "${arg}" of function "${statement.function.name}" (index ${ixStatement})`);
318
+ } else {
319
+ argsDefined.add(arg);
320
+
321
+ // Unused argument?
322
+ if (!(arg in fnVarUses)) {
323
+ warnings.push(`Unused argument "${arg}" of function "${statement.function.name}" (index ${ixStatement})`);
324
+ }
325
+ }
326
+ }
327
+ }
328
+
329
+ // Iterate function statements
330
+ const fnLabelsDefined = {};
331
+ const fnLabelsUsed = {};
332
+ for (const [ixFnStatement, fnStatement] of statement.function.statements.entries()) {
333
+ const [fnStatementKey] = Object.keys(fnStatement);
334
+
335
+ // Function expression statement checks
336
+ if (fnStatementKey === 'expr') {
337
+ // Pointless function expression statement?
338
+ if (!('name' in fnStatement.expr) && isPointlessExpression(fnStatement.expr.expr)) {
339
+ warnings.push(`Pointless statement in function "${statement.function.name}" (index ${ixFnStatement})`);
340
+ }
341
+
342
+ // Function label statement checks
343
+ } else if (fnStatementKey === 'label') {
344
+ // Label redefinition?
345
+ if (fnStatement.label in fnLabelsDefined) {
346
+ warnings.push(
347
+ `Redefinition of label "${fnStatement.label}" in function "${statement.function.name}" (index ${ixFnStatement})`
348
+ );
349
+ } else {
350
+ fnLabelsDefined[fnStatement.label] = ixFnStatement;
351
+ }
352
+
353
+ // Function jump statement checks
354
+ } else if (fnStatementKey === 'jump') {
355
+ if (!(fnStatement.jump.label in fnLabelsUsed)) {
356
+ fnLabelsUsed[fnStatement.jump.label] = ixFnStatement;
357
+ }
358
+ }
359
+ }
360
+
361
+ // Unused function labels?
362
+ for (const label of Object.keys(fnLabelsDefined)) {
363
+ if (!(label in fnLabelsUsed)) {
364
+ warnings.push(`Unused label "${label}" in function "${statement.function.name}" (index ${fnLabelsDefined[label]})`);
365
+ }
366
+ }
367
+
368
+ // Unknown function labels?
369
+ for (const label of Object.keys(fnLabelsUsed)) {
370
+ if (!(label in fnLabelsDefined)) {
371
+ warnings.push(`Unknown label "${label}" in function "${statement.function.name}" (index ${fnLabelsUsed[label]})`);
372
+ }
373
+ }
374
+
375
+ // Global expression statement checks
376
+ } else if (statementKey === 'expr') {
377
+ // Pointless global expression statement?
378
+ if (!('name' in statement.expr) && isPointlessExpression(statement.expr.expr)) {
379
+ warnings.push(`Pointless global statement (index ${ixStatement})`);
380
+ }
381
+
382
+ // Global label statement checks
383
+ } else if (statementKey === 'label') {
384
+ // Label redefinition?
385
+ if (statement.label in labelsDefined) {
386
+ warnings.push(`Redefinition of global label "${statement.label}" (index ${ixStatement})`);
387
+ } else {
388
+ labelsDefined[statement.label] = ixStatement;
389
+ }
390
+
391
+ // Global jump statement checks
392
+ } else if (statementKey === 'jump') {
393
+ if (!(statement.jump.label in labelsUsed)) {
394
+ labelsUsed[statement.jump.label] = ixStatement;
395
+ }
396
+ }
397
+ }
398
+
399
+ // Unused global labels?
400
+ for (const label of Object.keys(labelsDefined)) {
401
+ if (!(label in labelsUsed)) {
402
+ warnings.push(`Unused global label "${label}" (index ${labelsDefined[label]})`);
403
+ }
404
+ }
405
+
406
+ // Unknown global labels?
407
+ for (const label of Object.keys(labelsUsed)) {
408
+ if (!(label in labelsDefined)) {
409
+ warnings.push(`Unknown global label "${label}" (index ${labelsUsed[label]})`);
410
+ }
411
+ }
412
+
413
+ return warnings;
414
+ }
415
+
416
+
417
+ // Helper function to determine if an expression statement's expression is pointless
418
+ function isPointlessExpression(expr) {
419
+ const [exprKey] = Object.keys(expr);
420
+ if (exprKey === 'function') {
421
+ return false;
422
+ } else if (exprKey === 'binary') {
423
+ return isPointlessExpression(expr.binary.left) && isPointlessExpression(expr.binary.right);
424
+ } else if (exprKey === 'unary') {
425
+ return isPointlessExpression(expr.unary.expr);
426
+ } else if (exprKey === 'group') {
427
+ return isPointlessExpression(expr.group);
428
+ }
429
+ return true;
430
+ }
431
+
432
+
433
+ // Helper function to set variable assignments/uses for a statements array
434
+ function getVariableAssignmentsAndUses(statements, assigns, uses) {
435
+ for (const [ixStatement, statement] of statements.entries()) {
436
+ const [statementKey] = Object.keys(statement);
437
+ if (statementKey === 'expr') {
438
+ if ('name' in statement.expr) {
439
+ if (!(statement.expr.name in assigns)) {
440
+ assigns[statement.expr.name] = ixStatement;
441
+ }
442
+ }
443
+ getExpressionVariableUses(statement.expr.expr, uses, ixStatement);
444
+ } else if (statementKey === 'jump' && 'expr' in statement.jump) {
445
+ getExpressionVariableUses(statement.jump.expr, uses, ixStatement);
446
+ } else if (statementKey === 'return' && 'expr' in statement.return) {
447
+ getExpressionVariableUses(statement.return.expr, uses, ixStatement);
448
+ }
449
+ }
450
+ }
451
+
452
+
453
+ // Helper function to set variable uses for an expression
454
+ function getExpressionVariableUses(expr, uses, ixStatement) {
455
+ const [exprKey] = Object.keys(expr);
456
+ if (exprKey === 'variable') {
457
+ if (!(expr.variable in uses)) {
458
+ uses[expr.variable] = ixStatement;
459
+ }
460
+ } else if (exprKey === 'binary') {
461
+ getExpressionVariableUses(expr.binary.left, uses, ixStatement);
462
+ getExpressionVariableUses(expr.binary.right, uses, ixStatement);
463
+ } else if (exprKey === 'unary') {
464
+ getExpressionVariableUses(expr.unary.expr, uses, ixStatement);
465
+ } else if (exprKey === 'group') {
466
+ getExpressionVariableUses(expr.group, uses, ixStatement);
467
+ } else if (exprKey === 'function') {
468
+ if (!(expr.function.name in uses)) {
469
+ uses[expr.function.name] = ixStatement;
470
+ }
471
+ if ('args' in expr.function) {
472
+ for (const argExpr of expr.function.args) {
473
+ getExpressionVariableUses(argExpr, uses, ixStatement);
474
+ }
475
+ }
476
+ }
477
+ }