@formaui-net/nexo 0.0.0 → 1.0.0-preview.1
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/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +148 -6
- package/package.json +23 -3
- package/src/common/ArrayUtils.mjs +16 -0
- package/src/common/ObjectUtils.mjs +33 -0
- package/src/language/DefinitionLocations.mjs +41 -0
- package/src/language/DefinitionValidator.mjs +1265 -0
- package/src/language/DefinitionWriter.mjs +475 -0
- package/src/language/Dictionary.mjs +52 -0
- package/src/language/Evaluator.mjs +3480 -0
- package/src/language/ImportedVocabulary.mjs +137 -0
- package/src/language/Imports.mjs +141 -0
- package/src/language/Index.mjs +29 -0
- package/src/language/LanguageResolver.mjs +4068 -0
- package/src/language/Limits.mjs +78 -0
- package/src/language/LineStructure.mjs +385 -0
- package/src/language/Lowering.mjs +453 -0
- package/src/language/MemberSelector.mjs +89 -0
- package/src/language/Names.mjs +65 -0
- package/src/language/NexoError.mjs +145 -0
- package/src/language/Pair.mjs +21 -0
- package/src/language/ParameterBindings.mjs +361 -0
- package/src/language/ParsedTexts.mjs +62 -0
- package/src/language/Parser.mjs +1817 -0
- package/src/language/Range.mjs +51 -0
- package/src/language/RecentlyUsed.mjs +50 -0
- package/src/language/ResolvedDefinition.mjs +219 -0
- package/src/language/ResultPath.mjs +101 -0
- package/src/language/ScopeLocals.mjs +102 -0
- package/src/language/StructureTypes.mjs +48 -0
- package/src/language/SyntaxTree.mjs +203 -0
- package/src/language/TextLocations.mjs +185 -0
- package/src/language/TextPositions.mjs +23 -0
- package/src/language/Tokenizer.mjs +917 -0
- package/src/language/Type.mjs +401 -0
- package/src/language/TypeChecker.mjs +449 -0
- package/src/language/ValueCapture.mjs +315 -0
- package/src/language/Versions.mjs +14 -0
- package/src/language/vocabulary/Declarations.mjs +64 -0
- package/src/language/vocabulary/ItemPositions.mjs +48 -0
- package/src/language/vocabulary/ListOrdering.mjs +145 -0
- package/src/language/vocabulary/MemberFailure.mjs +21 -0
- package/src/language/vocabulary/NexoModule.mjs +1119 -0
- package/src/language/vocabulary/NexoRegistry.mjs +1540 -0
- package/src/language/vocabulary/TypeNameScope.mjs +63 -0
- package/src/language/vocabulary/Vocabulary.mjs +640 -0
- package/src/nexo.d.ts +743 -0
- package/src/nexo.js +338 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Limits — the bounds Nexo holds a script to, so a text or a definition beyond them is refused with its code, rather than overflowing the stack of whatever reads it, and the lower bounds a host sets on the work it evaluates.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { NexoErrors } from './NexoError.mjs';
|
|
6
|
+
|
|
7
|
+
export class Limits {
|
|
8
|
+
/**
|
|
9
|
+
* The most levels a script's expressions nest: a definition's nodes, each within the one holding it, and a text's values, each within the brackets, the braces or the parentheses holding it. Reading text, validating,
|
|
10
|
+
* resolving, evaluating and writing a definition each follow its nesting, and each reads 256 levels with room to spare.
|
|
11
|
+
*/
|
|
12
|
+
static nesting = 256;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The most levels the data a host gives nests, each list, dictionary, pair, object or structure within another a level deeper: data it binds, data a host call supplies, and a result holding data a member answered.
|
|
16
|
+
* Binding, capturing, typing, checking and copying data each follow its nesting, and each reads 1,000 levels with room to spare; data costs less for each level than a script does, so it nests deeper.
|
|
17
|
+
*/
|
|
18
|
+
static dataNesting = 1000;
|
|
19
|
+
|
|
20
|
+
/** The most items a list holds, as JavaScript's arrays hold them. */
|
|
21
|
+
static listLength = 2 ** 32 - 1;
|
|
22
|
+
|
|
23
|
+
/** The limits of an evaluator its host sets none of: a list one of Nexo's own members makes holds as many items as a list can. */
|
|
24
|
+
static none = Object.freeze({ items: null });
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The limits a host sets for the evaluations of an evaluator, frozen, from its options: `items`, the most items a list one of Nexo's own members makes may hold where its size is not its receiver's, a whole number from 0 to
|
|
28
|
+
* the most a list holds, and none where it sets none. Options or limits that are no object, and an `items` that is no such number, are refused (NEXO2049), and a key either takes none of, as any operation's options are (NEXO2048).
|
|
29
|
+
*/
|
|
30
|
+
static hostLimitsOf(options) {
|
|
31
|
+
if (options === undefined || options === null) {
|
|
32
|
+
return Limits.none;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const fail = (message, code = 'NEXO2049') => {
|
|
36
|
+
throw NexoErrors.structure(code, message, {});
|
|
37
|
+
};
|
|
38
|
+
const isObject = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
39
|
+
|
|
40
|
+
if (!isObject(options)) {
|
|
41
|
+
fail('createEvaluator takes its options as an object, as { limits: { items: 10000 } }');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const unknownOption = Object.keys(options).find(key => key !== 'limits');
|
|
45
|
+
|
|
46
|
+
if (unknownOption !== undefined) {
|
|
47
|
+
fail(`createEvaluator takes the options 'limits', and '${unknownOption}' is none of them`, 'NEXO2048');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const { limits } = options;
|
|
51
|
+
|
|
52
|
+
if (limits === undefined) {
|
|
53
|
+
return Limits.none;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!isObject(limits)) {
|
|
57
|
+
fail('An evaluator\'s limits are an object, as { items: 10000 }');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const unknownLimit = Object.keys(limits).find(key => key !== 'items');
|
|
61
|
+
|
|
62
|
+
if (unknownLimit !== undefined) {
|
|
63
|
+
fail(`An evaluator's limits are 'items', and '${unknownLimit}' is none of them`, 'NEXO2048');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const { items } = limits;
|
|
67
|
+
|
|
68
|
+
if (items === undefined) {
|
|
69
|
+
return Limits.none;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!Number.isSafeInteger(items) || items < 0 || items > Limits.listLength) {
|
|
73
|
+
fail(`An evaluator's limit of 'items' is a whole number from 0 to ${Limits.listLength}, and is ${typeof items === 'number' ? items : typeof items}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return Object.freeze({ items });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LineStructure — the layouts a script's text may take: where a line may break between its tokens, where one must, and where none may, checked over its syntax tree, which it never reads otherwise
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { SyntaxTree } from './SyntaxTree.mjs';
|
|
6
|
+
import { Tokenizer } from './Tokenizer.mjs';
|
|
7
|
+
|
|
8
|
+
/** @typedef {import('./SyntaxTree.mjs').SyntaxNode} SyntaxNode */
|
|
9
|
+
/** @typedef {import('./Tokenizer.mjs').Token} Token */
|
|
10
|
+
|
|
11
|
+
/** The binary operators' nodes, each operand and operator a child in turn, a line breaking before an operator only where the expression is enclosed and never after one. */
|
|
12
|
+
const OPERATOR_KINDS = new Set(['disjunction', 'conjunction', 'equality', 'comparison', 'additive', 'multiplicative', 'coalescing']);
|
|
13
|
+
|
|
14
|
+
/** The nodes an enclosure reaches through, the operators of one expression, its prefix operators among them, so an operand of an enclosed expression is enclosed with it. */
|
|
15
|
+
const ENCLOSURE_KINDS = new Set([...OPERATOR_KINDS, 'negation', 'arithmetic-negation']);
|
|
16
|
+
|
|
17
|
+
/** The keyword blocks, each written on one line or, once it spans lines, in block form. */
|
|
18
|
+
const KEYWORD_BLOCK_KINDS = new Set(['conditional-expression', 'do-expression', 'function-expression']);
|
|
19
|
+
|
|
20
|
+
/** The steps of a chain a line may break before, each beginning with `.`, `?.`, `:` or `?:`. */
|
|
21
|
+
const STEP_KINDS = new Set(['dot-property-access', 'member-call', 'computed-property-access', 'extension-access']);
|
|
22
|
+
|
|
23
|
+
/** The calls, each holding its arguments after the index of its `(`. */
|
|
24
|
+
const CALL_OPENINGS = Object.freeze({ 'function-call': 1, 'member-call': 2, 'namespace-access': 3 });
|
|
25
|
+
|
|
26
|
+
/** What alone may follow a keyword block's `end` on its line in block form. */
|
|
27
|
+
const CLOSING_PUNCTUATION = new Set([',', ')', ']', '}']);
|
|
28
|
+
|
|
29
|
+
/** The tokens written directly against the one before them, which a joined form writes with no space. */
|
|
30
|
+
const ATTACHED_AFTER = new Set(['(', '[', '?[', ')', ']', ',', '.', '?.', ':', '?:', '..']);
|
|
31
|
+
|
|
32
|
+
/** The tokens written directly against the one after them. */
|
|
33
|
+
const ATTACHED_BEFORE = new Set(['(', '[', '?[', '(?', '.', '?.', ':', '?:', '-', '^', '..']);
|
|
34
|
+
|
|
35
|
+
/** What a block form gives, which a message about a keyword block that spans lines opens with. */
|
|
36
|
+
const BLOCK_FORM = "A conditional, a do block or a function that spans lines is in block form, each 'then', 'else', 'do' and head ending its line, its content on the lines after, and 'end' on a line of its own";
|
|
37
|
+
|
|
38
|
+
/** A gap where a line may break. */
|
|
39
|
+
const ALLOWED = Object.freeze({ kind: 'allowed' });
|
|
40
|
+
|
|
41
|
+
export class LineStructure {
|
|
42
|
+
#text;
|
|
43
|
+
|
|
44
|
+
/** The index of each token in the text's tokens, by the token. @type {Map<Token, number>} */
|
|
45
|
+
#indexes = new Map();
|
|
46
|
+
|
|
47
|
+
/** @type {Token[]} */
|
|
48
|
+
#tokens;
|
|
49
|
+
|
|
50
|
+
/** @type {(code: string, message: string, offset: number) => never} */
|
|
51
|
+
#fail;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {string} text The text the syntax tree was read from.
|
|
55
|
+
* @param {Token[]} tokens The text's tokens, in order, the tree's among them.
|
|
56
|
+
*/
|
|
57
|
+
constructor(text, tokens) {
|
|
58
|
+
this.#text = text;
|
|
59
|
+
this.#tokens = tokens;
|
|
60
|
+
tokens.forEach((token, index) => this.#indexes.set(token, index));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Validates the script's layout, refusing the first line break, in the order of the text, that a one-line construct holds (NEXO1041), one before a binary operator outside an enclosure (NEXO1042) or after one
|
|
65
|
+
* (NEXO1064), a keyword block spanning lines outside block form (NEXO1043), and a boundary with earlier code on its line (NEXO1044), each through the failure given, which throws.
|
|
66
|
+
* @param {import('./SyntaxTree.mjs').ScriptNode} script
|
|
67
|
+
* @param {(code: string, message: string, offset: number) => never} fail
|
|
68
|
+
*/
|
|
69
|
+
validate(script, fail) {
|
|
70
|
+
this.#fail = fail;
|
|
71
|
+
this.#validateNode(script, false);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Validates each gap between the node's children, and each child within, in the order of the text, a keyword block's `end` and what follows it on its line last. */
|
|
75
|
+
#validateNode(node, isEnclosed) {
|
|
76
|
+
if (SyntaxTree.isToken(node)) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const isBlock = KEYWORD_BLOCK_KINDS.has(node.kind) && Tokenizer.holdsLineBreak(this.#text, SyntaxTree.startOf(node), SyntaxTree.endOf(node));
|
|
81
|
+
|
|
82
|
+
node.children.forEach((child, index) => {
|
|
83
|
+
if (index > 0) {
|
|
84
|
+
this.#validateGap(node, index, isEnclosed, isBlock);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this.#validateNode(child, LineStructure.#isChildEnclosed(node, index, isEnclosed));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (isBlock) {
|
|
91
|
+
this.#validateAfterEnd(node.children.at(-1));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Validates the gap before the node's child at the index, by the rule its position follows. */
|
|
96
|
+
#validateGap(node, index, isEnclosed, isBlock) {
|
|
97
|
+
const before = LineStructure.#lastTokenOf(node.children[index - 1]);
|
|
98
|
+
const after = LineStructure.#firstTokenOf(node.children[index]);
|
|
99
|
+
const breaks = Tokenizer.holdsLineBreak(this.#text, before.end, after.start);
|
|
100
|
+
const rule = this.#ruleOf(node, index, isEnclosed, isBlock);
|
|
101
|
+
|
|
102
|
+
switch (rule.kind) {
|
|
103
|
+
case 'refused':
|
|
104
|
+
if (breaks) {
|
|
105
|
+
this.#fail(rule.code ?? 'NEXO1041', rule.code === undefined ? `${rule.construct} stand on one line: write '${this.#joined(before, after)}'` : rule.message, after.start);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
break;
|
|
109
|
+
case 'required':
|
|
110
|
+
if (!breaks) {
|
|
111
|
+
this.#fail(rule.code, rule.message, after.start);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
break;
|
|
115
|
+
case 'operator':
|
|
116
|
+
if (breaks) {
|
|
117
|
+
this.#fail(rule.code, rule.message, rule.offset);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
break;
|
|
121
|
+
default:
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The rule of the gap before the node's child at the index: refused, a one-line construct's, which names it, or a keyword block's or an operator's, which give their own code; allowed, at a break point; or
|
|
128
|
+
* required, at a boundary. A body that uses `return` starts a line wherever it stands, its locals and its `return` at a boundary each, and a position no rule makes a break point is a one-line construct's.
|
|
129
|
+
*/
|
|
130
|
+
#ruleOf(node, index, isEnclosed, isBlock) {
|
|
131
|
+
const child = node.children[index];
|
|
132
|
+
const previous = node.children[index - 1];
|
|
133
|
+
const is = (part, type) => SyntaxTree.isToken(part) && (part.type === type || (part.type === 'keyword' && part.value === type));
|
|
134
|
+
|
|
135
|
+
if (!SyntaxTree.isToken(child) && child.kind === 'body') {
|
|
136
|
+
return { kind: 'required', code: 'NEXO1044', message: "A body that uses return starts on a line of its own, each of its locals and its 'return' on one of their own" };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
switch (node.kind) {
|
|
140
|
+
case 'script':
|
|
141
|
+
if (!SyntaxTree.isToken(child) && child.kind === 'import-declaration') {
|
|
142
|
+
return { kind: 'required', code: 'NEXO1044', message: 'An import starts on a line of its own' };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return index === node.children.length - 1 ? ALLOWED : { kind: 'required', code: 'NEXO1044', message: 'The body after the imports starts on a line of its own' };
|
|
146
|
+
case 'import-declaration':
|
|
147
|
+
return { kind: 'refused', construct: "An import's words" };
|
|
148
|
+
case 'body':
|
|
149
|
+
return is(previous, 'return') ? { kind: 'refused', construct: "A body's 'return' and the start of its result" } : { kind: 'required', code: 'NEXO1044', message: "Each local and the 'return' of a body that uses return start on a line of their own" };
|
|
150
|
+
case 'local':
|
|
151
|
+
return index === 3 ? ALLOWED : { kind: 'refused', construct: "A local's 'let', its name and its '='" };
|
|
152
|
+
case 'conditional-binding':
|
|
153
|
+
return index === 3 ? ALLOWED : { kind: 'refused', construct: "A conditional binding's 'let', its name and its '='" };
|
|
154
|
+
case 'field':
|
|
155
|
+
return index === 2 ? ALLOWED : { kind: 'refused', construct: "An object field's name and its '='" };
|
|
156
|
+
case 'object':
|
|
157
|
+
case 'list-expression':
|
|
158
|
+
return is(child, ',') ? { kind: 'refused', construct: 'A comma and what it follows' } : ALLOWED;
|
|
159
|
+
case 'dictionary-expression':
|
|
160
|
+
// An empty dictionary's `[=]` is one construct, and any other's entries break as an object's do.
|
|
161
|
+
if (is(child, '=') || is(previous, '=')) {
|
|
162
|
+
return { kind: 'refused', construct: "An empty dictionary's '[=]'" };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return is(child, ',') ? { kind: 'refused', construct: 'A comma and what it follows' } : ALLOWED;
|
|
166
|
+
case 'dictionary-entry':
|
|
167
|
+
return index === 2 ? ALLOWED : { kind: 'refused', construct: "A dictionary entry's key and its '='" };
|
|
168
|
+
case 'function-call':
|
|
169
|
+
case 'member-call':
|
|
170
|
+
case 'namespace-access':
|
|
171
|
+
return LineStructure.#callRuleOf(node, index, child, is);
|
|
172
|
+
case 'dot-property-access':
|
|
173
|
+
return { kind: 'refused', construct: "A member step's '.' and its name" };
|
|
174
|
+
case 'computed-property-access':
|
|
175
|
+
return index === 1 ? { kind: 'refused', construct: "A computed property access's '.' and its '['" } : ALLOWED;
|
|
176
|
+
case 'extension-access': {
|
|
177
|
+
// Its colon and each dot are member steps, which stand on their name's line and may follow a line break as any step of a chain does, and a call's arguments break as any call's do.
|
|
178
|
+
const opening = node.children.findIndex(part => is(part, '('));
|
|
179
|
+
|
|
180
|
+
if (index === 1) {
|
|
181
|
+
return { kind: 'refused', construct: `A member step's '${node.children[0].type}' and its name` };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (opening === -1 || index < opening) {
|
|
185
|
+
return is(child, '.') ? ALLOWED : { kind: 'refused', construct: "A member step's '.' and its name" };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (index === opening) {
|
|
189
|
+
return { kind: 'refused', construct: "A call's '(' and the name it calls" };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return is(child, ',') ? { kind: 'refused', construct: 'A comma and what it follows' } : ALLOWED;
|
|
193
|
+
}
|
|
194
|
+
case 'chain':
|
|
195
|
+
case 'member-chain':
|
|
196
|
+
if (STEP_KINDS.has(child.kind)) {
|
|
197
|
+
return ALLOWED;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return child.kind === 'object' ? { kind: 'refused', construct: "An object expression's '{' and its receiver" } : { kind: 'refused', construct: "An indexer's '[' and what it indexes" };
|
|
201
|
+
case 'parenthesized-expression':
|
|
202
|
+
case 'receiver':
|
|
203
|
+
case 'optional-enclosed-access':
|
|
204
|
+
case 'indexer':
|
|
205
|
+
case 'interpolated-string':
|
|
206
|
+
case 'interpolation':
|
|
207
|
+
return ALLOWED;
|
|
208
|
+
case 'type-name':
|
|
209
|
+
return { kind: 'refused', construct: "A qualified type name's words" };
|
|
210
|
+
case 'cast':
|
|
211
|
+
return { kind: 'refused', construct: index === 1 ? "A cast's 'as' and what it casts" : "A cast's 'as' and its type" };
|
|
212
|
+
case 'conversion':
|
|
213
|
+
return { kind: 'refused', construct: index === 1 ? "A conversion's 'to' and what it converts" : "A conversion's 'to' and its type" };
|
|
214
|
+
case 'attempt':
|
|
215
|
+
case 'negation':
|
|
216
|
+
case 'arithmetic-negation':
|
|
217
|
+
return { kind: 'refused', construct: 'A prefix operator and the start of its operand' };
|
|
218
|
+
case 'from-end-index':
|
|
219
|
+
return { kind: 'refused', construct: "An index from the end, its '^' and its offset" };
|
|
220
|
+
case 'range-expression':
|
|
221
|
+
return { kind: 'refused', construct: "A range, its boundaries and its '..'" };
|
|
222
|
+
case 'arrow-function':
|
|
223
|
+
return is(previous, '=>') ? ALLOWED : { kind: 'refused', construct: "An arrow function's parameters and its '=>'" };
|
|
224
|
+
case 'function-expression':
|
|
225
|
+
return LineStructure.#functionRuleOf(index, child, previous, is, isBlock);
|
|
226
|
+
case 'do-expression':
|
|
227
|
+
return isBlock ? { kind: 'required', code: 'NEXO1043', message: `${BLOCK_FORM}: write the do block's body on the lines after 'do', and 'end' on a line of its own` } : ALLOWED;
|
|
228
|
+
case 'conditional-expression':
|
|
229
|
+
return LineStructure.#conditionalRuleOf(child, previous, is, isBlock);
|
|
230
|
+
default:
|
|
231
|
+
return OPERATOR_KINDS.has(node.kind) ? this.#operatorRuleOf(child, previous, isEnclosed) : { kind: 'refused', construct: 'These tokens' };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** The rule of a gap in a call: its name's step and its `(` refused a line break, its arguments' break points after `(` and each comma and before `)`, and a comma refused one before it. */
|
|
236
|
+
static #callRuleOf(node, index, child, is) {
|
|
237
|
+
const opening = CALL_OPENINGS[node.kind];
|
|
238
|
+
|
|
239
|
+
if (index < opening) {
|
|
240
|
+
// A namespace's name is a chain's input, which a line may break after, before its dot.
|
|
241
|
+
return node.kind === 'namespace-access' && index === 1 ? ALLOWED : { kind: 'refused', construct: "A member step's '.' and its name" };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (index === opening) {
|
|
245
|
+
return { kind: 'refused', construct: "A call's '(' and the name it calls" };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return is(child, ',') ? { kind: 'refused', construct: 'A comma and what it follows' } : ALLOWED;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** The rule of a gap in a function expression: `function` and its `(` on one line, its parameters' break points, and in block form its body on the lines after its head and its `end` on a line of its own. */
|
|
252
|
+
static #functionRuleOf(index, child, previous, is, isBlock) {
|
|
253
|
+
if (index === 1) {
|
|
254
|
+
return { kind: 'refused', construct: "'function' and its '('" };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (is(child, ',')) {
|
|
258
|
+
return { kind: 'refused', construct: 'A comma and what it follows' };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// The body after the head's `)`, and the `end` after the body.
|
|
262
|
+
if (is(previous, ')') || is(child, 'end')) {
|
|
263
|
+
return isBlock ? { kind: 'required', code: 'NEXO1043', message: `${BLOCK_FORM}: write the function's body on the lines after its head, and 'end' on a line of its own` } : ALLOWED;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return ALLOWED;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* The rule of a gap in a conditional: its `if` or `elseif` and its test's start on one line, and `then` on the line its test ends; in block form, each branch on the lines after its `then` or `else`, and each `elseif`,
|
|
271
|
+
* `else` and `end` starting a line.
|
|
272
|
+
*/
|
|
273
|
+
static #conditionalRuleOf(child, previous, is, isBlock) {
|
|
274
|
+
if (is(previous, 'if') || is(previous, 'elseif')) {
|
|
275
|
+
return { kind: 'refused', code: 'NEXO1043', message: `${BLOCK_FORM}: write its condition's start on the line of its '${previous.value}'` };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (is(child, 'then')) {
|
|
279
|
+
return { kind: 'refused', code: 'NEXO1043', message: `${BLOCK_FORM}: write 'then' at the end of its condition's last line` };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (!isBlock) {
|
|
283
|
+
return ALLOWED;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (is(previous, 'then') || is(previous, 'else')) {
|
|
287
|
+
return { kind: 'required', code: 'NEXO1043', message: `${BLOCK_FORM}: write the branch on the lines after its '${previous.value}'` };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return { kind: 'required', code: 'NEXO1043', message: `${BLOCK_FORM}: write '${child.value}' at the start of a line of its own` };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** The rule of a gap in an operator expression: before an operator, refused outside an enclosure (NEXO1042); after one, refused always (NEXO1064); each refusal at the operator. */
|
|
294
|
+
#operatorRuleOf(child, previous, isEnclosed) {
|
|
295
|
+
if (LineStructure.#isOperator(child)) {
|
|
296
|
+
return isEnclosed ? ALLOWED : { kind: 'operator', code: 'NEXO1042', message: `A line breaks before an operator only inside parentheses, brackets or a condition: enclose the expression in parentheses, or join '${this.#wordOf(child)}' to the line before it`, offset: child.start };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return { kind: 'operator', code: 'NEXO1064', message: `A line breaks before an operator, never after it: start the next line with '${this.#wordOf(previous)}'`, offset: previous.start };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Validates what follows a keyword block's `end` on its line in block form, closing punctuation alone (NEXO1043). */
|
|
303
|
+
#validateAfterEnd(end) {
|
|
304
|
+
let previous = end;
|
|
305
|
+
|
|
306
|
+
for (let index = this.#indexes.get(end) + 1; index < this.#tokens.length; index++) {
|
|
307
|
+
const token = this.#tokens[index];
|
|
308
|
+
|
|
309
|
+
if (token.type === 'end' || Tokenizer.holdsLineBreak(this.#text, previous.end, token.start)) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (!CLOSING_PUNCTUATION.has(token.type)) {
|
|
314
|
+
this.#fail('NEXO1043', `${BLOCK_FORM}: only closing punctuation follows its 'end' on that line, so continue the expression on the line after it, as 'end' then ') + 2'`, token.start);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
previous = token;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Whether the child of an operator expression is one of its operators, a token, rather than an operand. */
|
|
322
|
+
static #isOperator(child) {
|
|
323
|
+
return SyntaxTree.isToken(child) && child.type !== 'literal' && child.type !== 'identifier' && child.type !== 'it' && child.type !== 'script-parameter' && child.type !== 'lexical-reference';
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Whether the node's child at the index stands enclosed: the expression a parenthesized group, an argument, an indexer's key, a computed property's expression or a condition holds directly, or an operand of an
|
|
328
|
+
* enclosed operator expression; any other position starts unenclosed again.
|
|
329
|
+
*/
|
|
330
|
+
static #isChildEnclosed(node, index, isEnclosed) {
|
|
331
|
+
const child = node.children[index];
|
|
332
|
+
|
|
333
|
+
if (SyntaxTree.isToken(child)) {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
switch (node.kind) {
|
|
338
|
+
case 'parenthesized-expression':
|
|
339
|
+
case 'receiver':
|
|
340
|
+
case 'optional-enclosed-access':
|
|
341
|
+
case 'indexer':
|
|
342
|
+
return true;
|
|
343
|
+
case 'computed-property-access':
|
|
344
|
+
return index === 2;
|
|
345
|
+
case 'function-call':
|
|
346
|
+
case 'member-call':
|
|
347
|
+
case 'namespace-access':
|
|
348
|
+
return index > CALL_OPENINGS[node.kind];
|
|
349
|
+
case 'extension-access': {
|
|
350
|
+
// An extension call's opening parenthesis follows its member's name, qualified or not, so its arguments start after it wherever it stands.
|
|
351
|
+
const opening = node.children.findIndex(candidate => SyntaxTree.isToken(candidate) && candidate.type === '(');
|
|
352
|
+
|
|
353
|
+
return opening !== -1 && index > opening;
|
|
354
|
+
}
|
|
355
|
+
case 'conditional-expression': {
|
|
356
|
+
const previous = node.children[index - 1];
|
|
357
|
+
|
|
358
|
+
return previous.type === 'keyword' && (previous.value === 'if' || previous.value === 'elseif');
|
|
359
|
+
}
|
|
360
|
+
case 'conditional-binding':
|
|
361
|
+
return isEnclosed;
|
|
362
|
+
default:
|
|
363
|
+
return ENCLOSURE_KINDS.has(node.kind) && isEnclosed;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** The two tokens around a gap joined, as a message gives the form that stands on one line, with a space between them unless one is written against the other. */
|
|
368
|
+
#joined(before, after) {
|
|
369
|
+
const space = ATTACHED_BEFORE.has(before.type) || ATTACHED_AFTER.has(after.type) ? '' : ' ';
|
|
370
|
+
|
|
371
|
+
return `${this.#wordOf(before)}${space}${this.#wordOf(after)}`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
#wordOf(token) {
|
|
375
|
+
return SyntaxTree.wordOf(token, this.#text);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
static #firstTokenOf(child) {
|
|
379
|
+
return SyntaxTree.isToken(child) ? child : LineStructure.#firstTokenOf(child.children[0]);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
static #lastTokenOf(child) {
|
|
383
|
+
return SyntaxTree.isToken(child) ? child : LineStructure.#lastTokenOf(child.children.at(-1));
|
|
384
|
+
}
|
|
385
|
+
}
|